Integrating Hand-Written Java¶
- 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:
- an
index
node, for each non-unique index - an
unique
node, for each unique index - a
case-sensitive
node, for each case-sensitive property - an
encoded
node, for each binary property - 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:
- 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);
- If added field is unique then you should add
unique
section to the target DMO into thedmo/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>
- 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>
- 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); }
- 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:
- 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 andrtrim
-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);
- Add DMO declaration section to
cfg/dmo_index_merge.xml
if you want to be able to reconvert the project or tosrc/<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>
- 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 thesrcnew
directory if you want to be able to reconvert the project or to thesrc
directory otherwise (see “Including Java classes into converted project” chapter for more information about thesrcnew
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>
- Create the DMO interface. You should put it under the
srcnew
directory if you want to be able to reconvert the project or to thesrc
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); }
- 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 thesrc
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 Blocks → External 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 Blocks → User-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 Blocks → Triggers 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 Blocks → REPEAT 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 Blocks → FOR 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 Blocks → DO 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 Blocks → REPEAT 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. PassingTransactionType.FULL
is equivalent to the 4GLTRANSACTION
option.blockLabel
- defines the block label (e.g.“cycle1”
), can be further used by statements likeundo
,next
,leave
andretry
.toClause
- can be applied only to*To
and*ToWhile
APIs. Represents an incrementing or decrementing loop construct. It is equivalent to 4GL'svariable = expression1 TO expression2 [ BY k ] clause
. It is handled by aToClause
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 4GLWHILE expression
clause. In order to define an expression, you should define a newLogicalExpression
anonymous class, with itsexecute()
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 4GLON { 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 usingOnPhrase
instances. The Looping Blocks → ON Phrase section of the Blocks chapter in the FWD Conversion Reference book descries how this statement gets converted and how it works in FWD. EachOnPhrase
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:
- 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. - 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>;
- Use in the enclosed block the
leave("<target label>")
in order to leave the Java-style outer block andnext("<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; } }
ThedeferredNext
anddeferredLeave
APIs defined inBlockManager
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) ORnew date() (used for the 4GL TODAY function) |
date.instantiateUnknownDate() |
decimal | 0 | new decimal(double) ORnew decimal(int) |
new decimal() |
handle | ? (unknown value) | n/a | new handle() |
integer | 0 | new integer(int) ORnew 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 Definitions → DEFINE 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 Definitions → DEFINE 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 theBlock.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 inTransactionManager.register(<var1>, <var2>, ..., <varN>).
- when the variable is shared as global (i.e. duplicates a
DEFINE NEW GLOBAL SHARED
statement), aTransactionManager.register(true, var
) call needs to be addedBlock.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 aGenericFrame
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 Conversion → DEFINE 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 Support → Frame 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 thebody()
method of the procedure/function block. - add a
frame.openScope()
in theBlock.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:
- 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
. - 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 likeDISPLAY num[i] ...,
to provide access to the element at the needed position in the extent field. - Define a widget (and associated setters/getters) for each extent element, as is usually done for a normal widget.
- 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 Blocks → Triggers 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 useANYWHERE
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 Statements → EDITING 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 Streams → Defining 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 Streams → Opening 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 theterminal
,term
,TERMINAL
orTERM
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 theterminal
,term
,TERMINAL
orTERM
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 theterminal
,term
,TERMINAL
orTERM
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 theBINARY
mode for the stream, has no parameters.Stream.setEcho
- set theECHO
orNO-ECHO
flags for the stream, with this parameter:echo
:true
if echo mode is set.Stream.setConvert
- set theCONVERT
orNO-CONVERT
flags for an input stream, with this parameter:convert
:true
if the stream's character conversion mode is enabled.Stream.setUnbuffered
- set theUNBUFFERED
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 Streams → Closing 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 Statements → EXPORT Statement. | Used for writing data in a standard format, compatible with the IMPORT statement. |
IMPORT |
Stream.import |
Stream Only I/O Statements → IMPORT Statement. | Used to read data previously written by the EXPORT statement |
PUT |
Stream.put |
Stream Only I/O Statements → PUT 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 allWHERE
andORDER BY
clauses used by HQL queries, in which this buffer is involved. Also, this is the variable name used by theTransactionManager
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
orTransactionType.NONE
value to the actual block in case of internal/external procedure, function,FOR
,REPEAT
orDO
blocks; for example, when starting a transaction for aREPEAT
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
- aSUB-TRANSACTION
- all types of
FOR EACH
- aSUB-TRANSACTION
- all types of
DO
- no transaction. This contradicts the 4GL behavior, which states that aDO
block (with aON ENDKEY
orON ERROR
clause and it reads records using anEXCLUSIVE-LOCK
or it modifies the database in any way) needs to start aSUB-TRANSACTION
if a transaction is active or aTRANSACTION
if no transaction is active. This means that its up to the developer to set the appropriate transaction for such kind ofDO
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-likeWHERE
clause, which filters the temporary records to be deleted, without DMO alias prefix. - the
<args>
is anObject
array which holds the parameter for theWHERE
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 copycopy(<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 aFOR FIRST
orFOR LAST
query - a
BlockManager.forEach*()
block, in case of aFOR EACH
query - a
BlockManager.repeat*()
block, in case of aREPEAT PRESELECT
query - a
BlockManager.do*()
block, in case of aDO PRESELECT
query - the
FIND
query andCAN-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()
andBlock.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 toOPEN QUERY
queries.Joinable
- query types which can be added as components to aCompoundQuery
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
- allCAN-FIND
calls will be converted to this query. For aFIND
call, if the conversion rules don't decide that theFIND
is used to navigate records (i.e. a loop which callsFIND 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 convertFIND
statements which are used for record navigation. This operates on a single table and uses the same behavior as theAdaptiveQuery
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 theFOR EACH
orDO/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, theAdaptiveQuery
will wrap its record retrieval to aRandomAccessQuery
instance or to aCompoundQuery
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. LikeRandomAccessQuery
, this query type was designed to support legacy behavior and should not be used for new development. Also, note that the multi-table support forAdaptiveQuery
is not fully implemented by FWD at the time of this writing, so this query type currently works in single-table mode (switchCompundQuery
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 theBREAK BY
groups are specified for the query or when the sort order specified by the used index is different then the one specified in theBY
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 theCompoundQuery.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 aQueryOffEndException
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 theCompoundQuery
behavior to allow join support with thePresortQuery
behavior to allow client-side (relative to database level) sorting, plusBREAK BY
support. This query should be avoided for new development.QueryWrapper
- a type of query to which the queries defined byDEFINE QUERY
statement convert to. This is a delegating container which delegates all query navigation to internal query components of following types:
PreselectQuery
AdaptiveQuery
AdaptiveFind
PresortQuery
RandomAccessQuery
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 recordsWHERE clause
- for each type of query, a HQLWHERE
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 thedmo_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, theWHERE
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-levelORDER BY
clause can be provided. On conversion, theORDER 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 thedmo_index.xml
file during conversion. Similar to theWHERE
clause, the fields which appear in theORDER BY
clause must be prefixed by the buffer alias. When multiple query components are joined together via foreign relations, theORDER BY
clause will contain the mode to which the records will be sorted, for all components; so, theORDER BY
clause may contain fields from multiple buffers.- Client-side (relative to database-level)
WHERE
expression
This is a specialWHERE
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-sideWHERE
expression is used in FWD by providing an implementation for following methods inWhereExpression
class:
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.- The parameters for this method must be a 4GL variable instance, a
FieldReference
or aResolvable
instance. 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-levelWHERE
clause- In a case when the
WHERE
clause is missing but there is a client-sideWHERE
expression, it will result in fetching all the records in that table and each record matched against the client-sideWHERE
expression. This can result in poor execution times, so this should be used only in cases when the expression is not compatible with the HQLWHERE
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 HQLWHERE
clause, a value must be provided for it. To do this, an order dependentObject[]
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 thebegins(source, target[, start[, case sensitive]])
API'sMATCHES
- mapped to thematches(source, target[, case sensitive])
API's to match a string against a pattern and thematchesList(list, item)
API to match an item in a comma-separated list against a given value.INDEX
- mapped to theindexOf(source, target[, start[, case sensitive]])
API'sSUBSTRING
- mapped to thesubstringOf(text, pos[, len[, type]]
) API'sLENGTH
- mapped to thelengthOf(text)
APIsENTRY
- mapped to theentry(index, list[, delimiter])
APIsLOOKUP
- mapped to theentry(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 dategetMonth
- retrieve the month (1-based) from a dategetDay
- retrieve the day of the month (1-based) from a date
4. decimal functions
ROUND
- mapped toFunctions.roundDec(value[, precision])
API'sreportPrecisionScale
- 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 integertoDec(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 operatorne(value1, value2)
- the not-equal operatorgt(value1, value2)
- the greater-than operatorgte(value1, value2)
- the greater-or-equal operatorlt(value1, value2)
- the less-than operatorlte(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 theBlock.init()
method, to enable theBREAK
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 anOPEN QUERY
statement are neededpreselect
parameter is true only in cases when a query backed by aOPEN QUERY PRESELECT
,DO PRESELECT
or aREPEAT PRESELECT
statement is needed.
CompoundQuery.setScrollable()
- used only with aOPEN QUERY ... SCROLLABLE
statementCompoundQuery.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).
- The query parameter can be a query of any
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
andprevious
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 notassign(query)
- assign a query to which the navigation is delegated to. The query can be only one ofCompoundQuery
,RandomAccessQuery
orPreselectQuery
(sub)typessetScrolling()
- set the query asSCROLLABLE
addComponent(query, ... )
- add other components to the query, only when the assigned query is aCompoundQuery
addComponent(buffer, ...)
- add other components to the query, used only when the assigned query is a@ PreselectQuerysetIterationType(iteration)
- when the assigned query is aPreselectedQuery
, this is used to set the iteration type (fromQueryConstants
) for the last added query componentaddSortCriterion()
- when the assigned query is aPresortQuery
, use it to add a sort criterionenableBreakGroups()
- when the assigned query is aPresortQuery
, enable break group supportaddAccumulator(accumulator, {deferred})
- register an accumulator with the query. When thedeferred
parameter is true, calls toQueryWrapper.iterate()
will postpone accumulation until anAccumulator.accumulate()
call. Else, it will accumulate data immediately.iterate()
- advance a@ CompoundQuery delegaterepositionByID(recid)
- reposition the query to the record with specified recid (i.e. REPOSITION 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. theOPEN QUERY
statement)close()
- use it to close the query (i.e. theCLOSE 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. theNUM-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 delegatenext(iterating, {<lock type>})
- used only by aCompoundQuery
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 aRandomAccessQuery
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 specifiedORDER BY
andWHERE
clauses and the currently referenced record as a reference point.previous()
- retrieve the previous record, using the specifiedORDER BY
andWHERE
clauses and the currently referenced record as a reference point.first()
- retrieve the first record, using the specifiedORDER BY
andWHERE
clauseslast()
- retrieve the last record, using the specifiedORDER BY
andWHERE
clausescurrent()
- refresh the current referenced recordunique()
- retrieve an unique record using the specifiedWHERE
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 theCAN-FIND FIRST/LAST
function. The <values> parameter hold the substitution values for this queryhasOne({<values>, {<lock type>}})
- corresponds with theCAN-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>])
orconnect(“-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>)
andpdbName(<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 anyBREAK group
changes.Accumulator.reset()
- in theBlock.init()
method, all accumulators registered with the query needs to be reset by adding a call to this method.Accumulator.accumulate()
- called in theBlock.body()
method, after the query retrieved the next record(s).Accumulator.addBreakGroup(Resolvable)
- in theBlock.init()
method, this method is used to register theBY 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 inFOR EACH ... BREAK BY
blocks, its recommended to use the accumulators with break support only when they are scoped to aBlockManager.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.