Project

General

Profile

Code Conversion

Inputs

The primary input to code conversion is the Progress 4GL source code itself. The code as converted is fully preprocessed, with all include files and other preprocessor references, expansions or other modifications having been honored. The result is expected to be syntactically valid 4GL source code. That code is tokenized by the lexer and then structured into an Abstract Syntax Tree (AST) form without any syntactic sugar (elements of the syntax that are only there to enhance parsing or readability, but which have no semantic value).

The derivation of the P2O documents from the database schema (both temporary tables and permanent tables) is documented in the above section on Schema Conversion. The 4GL source code has direct references to database structure embedded in expressions, language statements and other language facilities. During code conversion the P2O documents are used to provide data on the structure, data types, configuration and naming of the schema elements.

The intermediate result is an AST file for each source procedure, named <source_procedure_file_name>.ast.

Transformation Overview

The following diagram illustrates the high level inputs and outputs of the code conversion process:

On the left side are the possible sources of code conversion input. On the right side of the diagram are the outputs that are generated by the code conversion process.

Outputs

There are 3 outputs from the code conversion process: the name map, the business logic and frame definitions.

The name map is a bidirectional mapping of legacy (Progress 4GL) names and the Java replacements. In Progress, 4GL code can invoke external procedures, internal procedures and functions using names. These names are converted into Java equivalents. Due to the dynamic invocation mechanisms of the 4GL, it is possible to have expressions that calculate these names at runtime. The logic of those runtime calculations is not modified during conversion. That means that if the Progress 4GL code previously had a calculation which generated a legacy name and used that name to invoke the code, then the FWD runtime must be able to dynamically map the legacy name to the Java name in order to invoke it. Likewise, the 4GL provides some features to identify the name of the code that is currently running. This requires that the Java names be converted back to their legacy form to match how the code would be expecting the result.

External procedures are identified by a filename in Progress 4GL and these are mapped into Java class names. Internal procedures and user-defined functions are each identified by a Progress 4GL name which is mapped into a Java method name. All of these mappings are stored in a file name_map.xml which is located in the root of the package for the generated application code. It is included in the application jar file and at runtime is loaded from that archive by FWD.

The Progress 4GL source code (that is not associated with a temp-table definition) is re-factored into one of two possible forms: business logic or frame definition. Both of these forms are Java source code and these are the other primary outputs of the code conversion.

Each statically defined frame and all of the widgets and configuration that is statically defined for that frame is read from the source code (in AST form) and those definitions are separated as a frame definition. A static frame in Progress 4GL is a frame that is recognized as such by the Progress 4GL compiler. The FWD code conversion identifies each of these frames and for each one, a Java source file is created. Each widget defined for the frame is identified and created in Java. All of the frame configuration (e.g. frame phrase contents such as a TITLE clause) is created a Java code in the frame definition. Each widget's configuration (e.g. format phrase contents such as the FORMAT string or HELP string) is likewise moved into the frame definition as Java code. Some of that configuration originally is read from the database schema and some is just hard coded into the 4GL source code. Either way, all static parts of the frame definition are moved into a single Java source file that is specific to that frame.

Each frame definition can be found in a path off the package root (the common directory which is the top-level of where all generated Java code is created) which will have the following relative name ui/<4gl_path>/<program_name><frame_name>.java. The path of the 4GL procedure source file in which the frame was defined is maintained, as a set of sub-package names for the resulting Java classes. All of those paths are under a ui/ directory to organize all the frames for easy access. The program name and frame name are converted to a Java form and then used to create a unique Java class name for each frame.

The rest of the source code that is not part of a static frame definition and which is not part of a temp-table definition, is considered business logic. This includes all of the block structure, variable definitions, control flow, expressions and other logic that defines the application's behavior and functionality. Though the frame definitions (structure, naming and configuration) are separated from the business logic, the usage of those frames is part of the business logic. That result is required to provide compatibility with the original application, since that usage is part of the control flow of the application and the data used in the widgets and/or obtained from the widgets is tightly coupled with the business logic.

Similarly, though the structure, configuration and naming of the database schema (permanent and temporary tables) is defined elsewhere (in the Data Model Objects or DMOs, the Hibernate mapping documents and the database itself via DDL), the usage of those DMOs is tightly coupled to the business logic. That means that the usage of the database is present directly in the control flow and expression processing of the converted business logic and this is required for compatibility. The data regarding the structure, names, data types and other configuration of the converted DMOs is obtained during conversion from the P2O documents as well as an earlier form of the documents called the schema dictionary files. The schema dictionary files are not depicted in the diagram above, but more details can be found in the chapter on Schema Loading in Part 1 of this book.

There is one Java class generated for each Progress 4GL procedure. Each business logic class can be found in a path off the package root which will have the following relative name <4gl_path>/<program_name>.java. The path of the 4GL procedure source file in which the frame was defined is maintained, as a set of sub-package names for the resulting Java classes. The program name is converted to a Java form and then used to create a unique Java class name for each business logic class.

Top-Level Block Conversion

In 4GL, any top-level block will be converted to a Java method. These top-level blocks represent code which will be executed only when the top-level block is called. The 4GL top-level blocks are:
  • external programs
  • internal procedures
  • user defined functions
  • triggers
  • class constructors
  • class destructors
  • class methods
  • class property getters
  • class property setters

This chapter will describe how each top-level block is converted in FWD, and some peculiarities about class data members and properties, when converting in FWD.

External Programs

An external program in 4GL is a associated with a file (usually with a .p or .w extension). This external program can be executed only via a RUN statement, and can have zero or more parameters defined.

In FWD, an external program is converted as a separate Java class. This Java class does not inherit any class or implement any interface - it will just define any methods representing an internal procedure or user defined function from that program, in addition to the external program. Assuming you have a simple_program.p file, with this code:

message "Hello World!".

this will get converted to a SimpleProgram.java file:

package com.goldencode.testcases;

import com.goldencode.p2j.util.*;
import com.goldencode.p2j.ui.*;

import static com.goldencode.p2j.util.BlockManager.*;
import static com.goldencode.p2j.util.InternalEntry.Type;
import static com.goldencode.p2j.ui.LogicalTerminal.*;

/**
 * Business logic (converted to Java from the 4GL source code
 * in simple_program.p).
 */
public class SimpleProgram
{
   /**
    * External procedure (converted to Java from the 4GL source code
    * in simple_program.p).
    */
   @LegacySignature(type = Type.MAIN, name = "simple_program.p")
   public void execute()
   {
      externalProcedure(SimpleProgram.this, new Block((Body) () -> 
      {
         message("Hello World!");
      }));
   }
}

The package com.goldencode.testcases; references the pkgroot configuration from p2j.cfg.xml, which is configurable. In this example, there is no other parent folder for the program, so the Java class will be emitted in the root package.

All the 4GL code which is executed by the external program (outside of any internal procedure or user defined function in that program) will be emitted in a single, execute Java method. This execute method will also be emitted any parameters defined at the 4GL external program. There will be no explicit Java constructors emitted.

All Java converted methods associated with a legacy entry (internal procedure or function) will have a LegacySignature annotation. At the execute method, this signature will define:
  • the type associated with the legacy entry (for an external program, this is Type.MAIN)
  • the 4GL program name (and its full path)
  • the defined parameters - the approach to emit the parameters is the same for all kinds of top-level blocks, and will be described later in this chapter.

For an external program, all code must be enclosed in a BlockManager.externalProcedure method call, to properly emulate the FWD runtime.

In addition to emitting the Java file for the external program, the external program will be registered in the name_map.xml file (which will be emitted to the root Java package), to map its legacy file name and path to its Java name and package (relative to the root package):

  <class-mapping jname="SimpleProgram" pname="simple_program.p"/>

This class-mapping node will also contain any parameters defined at the external program, which will be covered later in this chapter. In addition, it will contain information about any internal procedures and user-defined functions, defined in this external program.

Internal Procedures

Internal procedures can be defined in 4GL like:

procedure inSuperProc in super.
end.

procedure simpleProc.
end.

In the converted Java code, only simpleProc will be emitted, as inSuperProc is a virtual internal procedure, with no body defined in this external program:
   @LegacySignature(type = Type.PROCEDURE, name = "simpleProc")
   public void simpleProc()
   {
      internalProcedure(new Block());
   }

where:
  • type is set to Type.PROCEDURE, to identify this as a internal procedure, at runtime
  • name represents the legacy internal procedure name, which may not be the same as the converted Java name.

In addition to the Java method, the internal procedures will be registered under the class-mapping node of name_map.xml, for the external program defining them (virtual internal procedures will always appear only in name_map.xml via method-mapping nodes at the defining class-mapping):

    <method-mapping in-super="true" jname="inSuperProc" pname="inSuperProc" type="PROCEDURE"/>
    <method-mapping jname="simpleProc" pname="simpleProc" type="PROCEDURE"/>

The method-mapping node will contain also information about any defined parameters, which will be covered later in this chapter.

Any code executed by this internal procedure must be enclosed in the BlockManager.internalProcedure method call.

User Defined Functions

User defined functions can be defined in 4GL like:

function inSuperFunc returns int in super.

function inHandleFunc returns int in h.

function inHandleFunc2 returns int map to inHandleFunc in h.

function simpleFunc returns int.
end.

In the converted Java code, only simpleFunc will be emitted, as inSuperFunc, inHandleFunc and inHandleFunc2 are virtual functions, with no body defined in this external program:
   @LegacySignature(type = Type.FUNCTION, name = "simpleFunc", returns = "INTEGER")
   public integer simpleFunc()
   {
      return function(this, "simpleFunc", integer.class, new Block());
   }

where:
  • type is set to Type.FUNCTION, to idenfity this as a user-defined function, at runtime.
  • name represents the legacy user-defined function name, which may not be the same as the converted Java name.
  • returns represents the legacy 4GL type returned by this function.
  • extent, optional, which is set only when the return type is extent.
  • qualified, optional, which is set only when the return type is a legacy class type. In this case, returns will be set to OBJECT.

In addition to the Java method, the user-defined functions will be registered under the class-mapping node of name_map.xml, for the external program defining them (virtual user-defined functions will always appear only in name_map.xml via method-mapping nodes at the defining class-mapping):

    <method-mapping in-super="true" jname="inSuperFunc" pname="inSuperFunc" returns="INTEGER" type="FUNCTION"/>
    <method-mapping in-handle="true" jname="inHandleFunc" pname="inHandleFunc" returns="INTEGER" type="FUNCTION"/>
    <method-mapping in-handle="true" jname="inHandleFunc2" map-to="inHandleFunc" pname="inHandleFunc2" returns="INTEGER" type="FUNCTION"/>
    <method-mapping jname="simpleFunc" pname="simpleFunc" returns="INTEGER" type="FUNCTION"/>

The method-mapping node will contain also information about any defined parameters, which will be covered later in this chapter.

Any code executed by this user-defined function must be enclosed in the BlockManager.function method call.

UI Triggers

A trigger represents the 4GL's implementation of event handling. FWD supports UI-related events, among database events and others. The 4GL triggers for the UI events can be defined using the ON statement or the TRIGGERS phrase with the DEFINE widget or CREATE WIDGET statement - *FWD does not support currently the TRIGGERS phrase at DEFINE widget. Regular triggers are fully supported but the "inline" triggers implemented in a variable or widget definition are not implemented at this time - there is no "trigger phrase" support for them.

The syntax of the ON statement is:

ON event-list
{
   ANYWHERE |
   { OF widget-list [ OR event-list OF widget-list ] ... [ ANYWHERE ] }
}
{
   trigger-block |
   REVERT |
   { PERSISTENT RUN procedure [ ( input-parameters ) ] [ IN expr ] }
}

ON triggers are blocks that are executed when defined events occur in the user interface. Instead of defining triggers like internal procedures, triggers are defined "in line" with the procedure or function being processed. Though Progress treats a trigger as a distinct block type which has block properties, it is not well separated in terms of location in source code. Triggers take no parameters and are not named. Instead, they are registered as a set of events and a set of widgets which cause the trigger to be called. When any of the listed events occurs for any of the listed widgets, the trigger will be executed. Multiple triggers can be registered for the same event + widget combinations. The last registration "wins" (is active). The scoping for such registrations is at the procedure (internal/external), trigger and function level. When such a new scope is opened, duplicate trigger registrations will hide registrations from previous scopes. Likewise, the closing of a scope will remove all triggers registered in that scope, which will make previously hidden triggers "re-appear". Within a single procedure, function or trigger (yes, triggers can be nested in other triggers), duplicate registrations hide previous registrations and this will not implicitly ever be undone (without the entire scope exiting and thus removing all triggers registered in that scope). The explicit REVERT option can be used to deregister a specified trigger and then the trigger registered most recently (in this scope or a previous scope) will become the active trigger for that given event plus widget combination.

One other interesting feature of trigger registration is that it is strictly based on the flow of control of the registering code. If the flow of control never executes the branch of code in which a trigger is defined (e.g. an ELSE block that is never executed), then that trigger is not registered. This means that the registration of triggers must be kept strictly in line with the matching control flow, even if the trigger block itself is refactored into another location.

To perform trigger registration, FWD will emit calls to the following APIs in LogicalTerminal:

registerTrigger(EventList events, Object contain, boolean trans, Supplier<Trigger> trigger)
registerTrigger(EventList events, Object contain, boolean trans, Trigger trigger)
registerTrigger(EventList events, Object contain, Supplier<Trigger> trigger)
registerTrigger(EventList events, Object contain, Trigger trigger)

while the following APIs in LogicalTerminal will be used to deregister a trigger:

deregisterTrigger(EventList events)

where:

  • events represents the list of events and widgets for which this trigger executes.
  • contain is the containing object (all triggers are implemented as non-static inner classes, so they must have a reference to the containing class), which also must not be null.
  • trans is a flag which is set to true only if this trigger should start a full transaction and is set to false if the trigger should be a sub-transaction.
  • trigger is:
    • a singleton Trigger instance, if the trigger does not define any variables or frames, there is no buffer or query usage
    • a lambda expression which will create a new instance of that trigger, otherwise
For PERSISTENT RUN procedure option, LogicalTerminal provides these APIs:
registerTriggerPersistent(EventList events, String proc)
registerTriggerPersistent(EventList events, String proc, BaseDataType... params)
registerTriggerPersistentIn(EventList events, String proc, handle inst)
registerTriggerPersistentIn(EventList events, String proc, handle inst, BaseDataType... params)

where:
  • proc represents the internal or external procedure's name to be executed.
  • inst represents the external program where the lookup for an internal procedure is performed
  • params represents the parameters to be passed to the procedure (INPUT only). Each parameter is evaluated at trigger definition; the parameters do not get reevaluated on trigger invocation.

The EventList class supports the lists of events and widgets that define the termination of implicit or explicit WAIT-FOR statement and triggers activation. For insight details about trigger registration and deregistration works and how the EventList (associated with the event-list clause) gets converted, please see the Trigger Conversion section in the Events chapter of this book.

In terms of scoping, triggers can access resources (variables, streams, buffers and frames) that are defined in the enclosing scope. For this reason, trigger's body is always defined in the converted code in the same place as where it was defined in the 4GL code.

Triggers are implemented via:
  • a singleton Trigger instance, with the constructor receiving a lambda Body() expression with the trigger's converted code. The structure of this is:
    registerTrigger(new EventList(...), Test.this, new Trigger((Init) () ->
    {
          /* state initialization code */
    }, 
    (Body) () -> 
    {
          /* trigger body */
    }));
    
  • a lambda expressions returning an anonymous Trigger class, where the trigger block itself is emitted inside the body() method. All local resources are promoted to instance members of the anonymous Trigger class, so these resources are directly accessible.
    registerTrigger(new EventList(...), Test.this, () -> new Trigger()
    {
       /* field definitions */
    
       public void init()
       {
       }
    
       public void body()
       {
          /* trigger body */
       }
    });
    

In both cases, the trigger block is treated by default as a top-level sub-transaction block in terms of the TransactionManager. As with the internal procedures or functions, the init() method or Init() lambda expression is emitted when the trigger requires state initialization. When invoking a trigger, the lambda expression will create a new instance, the init() method will be called once to initialize its state and the body() method will be invoked to execute the trigger's code.

A trigger, as is a top-level block, it acts as an external procedure: on each invocation, it must reset its entire state (variables, buffers, etc) as if this is the very first invocation. This means that no trigger state is copied from one invocation to another.

A difference from other top level blocks is the triggers can be nested. In such cases, their definition is still emitted at the exact registerTrigger call. The difference is that the nested trigger registration is performed in the scope of the executing trigger and the nested trigger will listen for events as long as the parent trigger is executing.

Persistent triggers survive the top-level block defining it, and will be available:
  • as long as the non-persistent external program is on the stack
  • until the persistent program gets deleted
FWD supports both forms of PERSISTENT RUN clause:
  • PERSISTENT RUN target, where target is either an internal procedure or an external program.
  • PERSISTENT RUN proc IN expr, where an internal procedure is executed, the lookup being performed in the specified expr handle. The evaluation of the expr expression is done at the trigger registration, only once.

Example 1:

on go anywhere do:
   message "go".
end.

on go anywhere revert.

Converted code:

registerTrigger(new EventList("go", true), TriggerBlock0.this, new Trigger((Body) () -> 
{
   message("go");
}));

deregisterTrigger(new EventList("go", true));

Details:

In this example, notice how the trigger is emitted as a singleton instance, with the trigger's body emitted inside the Body() lambda expression. The trigger's registration is done via the registerTrigger call, which gets a reference to the currently running instance of the external procedure, TriggerTest1.this.

Example 2:

on go anywhere do:
   message "top go".
   on leave anywhere do:
      message "nested leave".
   end.
end.

Converted code:

registerTrigger(new EventList("go", true), TriggerTest2.this, new Trigger((Body) () -> 
{
   message("top go");

   registerTrigger(new EventList("leave", true), TriggerTest2.this, new Trigger((Body) () -> 
   {
      message("nested leave");
   }));
}));

Details:

When nested, the trigger body is still emitted at the registerTrigger call. The difference is on runtime - the enclosed LEAVE trigger will listen for events as long as the enclosing GO trigger is still executing; once the enclosing GO is finished, the LEAVE trigger will pop out of scope and will no longer listen for events.

Example 3:

on go anywhere do:
   def var i as int.
   message i.
end.

Converted code:

registerTrigger(new EventList("go", true), TriggerTest3.this, () -> new Trigger()
{
   integer i = UndoableFactory.integer();

   public void body()
   {
      message(i);
   }
});

Details:

As a variable is defined in this trigger, and each trigger invocation must have a clean state, a singleton can not be used - instead, the lambda expression will be evaluated to create a new instance of this trigger.

Example 4:

on go anywhere persistent run "goProcedure" in source-procedure.

Converted code:

registerTriggerPersistentIn(new EventList("go", true), "goProcedure", sourceProcedure());

Details:

In this example, a persistent trigger is being registered to call the goProcedure in the SOURCE-PROCEDURE - this is done by emulating a RUN "goProcedure" IN SOURCE-PROCEDURE, by the FWD runtime, when the trigger event is raised.

Enums

4GL Enums are supported in FWD, both conversion and runtime. They do not map directly to Java enums, as they have behavior which can't be managed at runtime by the Java enums; for example, 4GL enums inherit from Progress.Lang.Object, and this requires an enum instance to act like a Progress.Lang.Object. For this reason, they convert in FWD as Java classes extending the LegacyEnum class, which is FWD's replacement for Progress.Lang.Enum.

For example, having this 4GL enum:
enum oo.EnumTest:
  define enum
      green
      red
      shining
      black
      white.
end.

will convert like this in FWD:
package com.goldencode.testcases.oo;

import com.goldencode.p2j.util.*;
import com.goldencode.p2j.oo.lang.*;

import static com.goldencode.p2j.report.ReportConstants.*;
import static com.goldencode.p2j.util.InternalEntry.Type;

/**
 * Business logic (converted to Java from the 4GL source code
 * in oo/EnumTest.cls).
 */
public final class EnumTest
extends LegacyEnum
{
   @LegacySignature(type = Type.VARIABLE, name = "green")
   public static final object<? extends com.goldencode.testcases.oo.EnumTest> green;

   @LegacySignature(type = Type.VARIABLE, name = "red")
   public static final object<? extends com.goldencode.testcases.oo.EnumTest> red;

   @LegacySignature(type = Type.VARIABLE, name = "shining")
   public static final object<? extends com.goldencode.testcases.oo.EnumTest> shining;

   @LegacySignature(type = Type.VARIABLE, name = "black")
   public static final object<? extends com.goldencode.testcases.oo.EnumTest> black;

   @LegacySignature(type = Type.VARIABLE, name = "white")
   public static final object<? extends com.goldencode.testcases.oo.EnumTest> white;

   static
   {
      registerEnum(EnumTest.class, EnumTest::new, "oo.EnumTest");

      green = createEnum(EnumTest.class, "green");
      red = createEnum(EnumTest.class, "red");
      shining = createEnum(EnumTest.class, "shining");
      black = createEnum(EnumTest.class, "black");
      white = createEnum(EnumTest.class, "white");
   }

   private EnumTest(final String name, final Long value)
   {
      super(name, value);
   }

   @LegacySignature(type = Type.METHOD, name = "GetEnum", returns = "object", qualified = "oo.enumtest", parameters = 
   {
      @LegacyParameter(name = "val", type = "INT64", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
   public static object<EnumTest> getEnum(int64 val)
   {
      return getEnum(EnumTest.class, val);
   }

   @LegacySignature(type = Type.METHOD, name = "GetEnum", qualified = "oo.enumtest", parameters = 
   {
      @LegacyParameter(name = "val", type = "CHARACTER", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
   public static object<EnumTest> getEnum(character val)
   {
      return getEnum(EnumTest.class, val);
   }
}

where:
  • all enum members are emitted as static object fields, referencing a legacy instance of this enum.
  • these static fields are JVM-wide, as enums are immutable and can't be deleted. This is in constrast with legacy class static fields, as these must be local to the user accessing them, and not JVM-wide.
  • the legacy implicit getEnum methods are emitted at the Java class.

All legacy enums are registered, too, in name_map.xml, via a class-mapping node:

  <class-mapping jname="oo.EnumTest" ooname="oo.EnumTest" pname="oo/EnumTest.cls"/>

Interfaces

An interface in 4GL represents just definition of properties or methods. It can also contain temp-table or dataset definitions, which are not inherited in any classes implementing this interface: instead, they are meant only to allow interface methods to define temp-table or dataset parameters; the implementing class is required to re-define a temp-table/dataset with the same signature as the one in the interface.

Any methods, properties or class events will have associated Java methods in the generated Java interface; for example, this interface:

interface oo.IfaceTest:
   def property p1 as int get. set.

   method public int m1().
end.

will convert like:
package com.goldencode.testcases.oo;

import com.goldencode.p2j.util.*;

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

/**
 * Business logic (converted to Java from the 4GL source code
 * in oo/IfaceTest.cls).
 */
public interface IfaceTest
extends com.goldencode.p2j.oo.lang._BaseObject_
{
   @LegacySignature(type = Type.GETTER, name = "p1", returns = "INTEGER")
   public integer getP1();

   @LegacySignature(type = Type.SETTER, name = "p1", parameters = 
   {
      @LegacyParameter(name = "var", type = "INTEGER", mode = "INPUT")
   })
   public void setP1(final integer _var);

   @LegacySignature(type = Type.METHOD, name = "m1", returns = "INTEGER")
   public integer m1();
}

In FWD, each legacy interface will always extent the com.goldencode.p2j.oo.lang._BaseObject_ interface, with the methods defined by Progress.Lang.Object. The generated Java methods (for legacy methods, property getter/setter, class events, etc) will have no body, and any defined temp-tables or datasets will be dropped.

The details about the generated Java methods will be covered in the next section about legacy classes.

Each interface will also be registered in name_map.xml, via a class-mapping node:

  <class-mapping jname="oo.IfaceTest" ooname="oo.IfaceTest" pname="oo/IfaceTest.cls"/>

When compared with class-mapping nodes associated with an external program, no method mappings will be generated - the FWD runtime will be able to gather all details from the LegacySignature annotations, at the time when the legacy class is first accessed by the runtime.

When a legacy class implements an interface, the implementing methods or properties implemented from the interface will not have the OVERRIDE keyword - thus, FWD (at this time) does not emit a Java @Override annotation for such cases.

Classes

A class in 4GL can define constructors, destructors, methods, properties, variables, class events and even virtual functions. This section will cover how the legacy class its members are converted in FWD.

In 4GL, a class can inherit a single legacy class, and implement zero or more interfaces. It is also possible to specify FINAL or ABSTRACT options (which have a direct equivalent in Java, an abstract or final class), plus use-widget-pool and serializable options. For example, a class like:
class oo.ClassTest
inherits oo.AClassTest
implements oo.IfaceTest
use-widget-pool final serializable: 

end.

will convert in Java like:
package com.goldencode.testcases.oo;

import com.goldencode.p2j.util.*;

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

/**
 * Business logic (converted to Java from the 4GL source code
 * in oo/ClassTest.cls).
 */
@LegacySerializable
public final class ClassTest
extends com.goldencode.testcases.oo.AclassTest
implements com.goldencode.testcases.oo.IfaceTest
{
   public void __oo_ClassTest_execute__()
   {
      externalProcedure(ClassTest.class, ClassTest.this, new Block((Body) () -> 
      {
         WidgetPool.create();
         {
         }
      }));
   }

   @LegacySignature(type = Type.CONSTRUCTOR)
   public void __oo_classTest_constructor__()
   {
      internalProcedure(ClassTest.class, this, "__oo_classTest_constructor__", new Block((Body) () -> 
      {
         __oo_aclassTest_constructor__();
      }));
   }
}

where:
  • final is converted to Java final modifier at the Java class.
  • an abstract class would have the Java abstract modifier at the Java class.
  • use-widget-pool is converted as a WidgetPool.create(); call for an unnamed pool associated with this class instance.
  • serializable is converted by emitting a LegacySerializable annotation.
  • as no default constructor is specified in the 4GL code, FWD will automatically emit a default, no-argument, public, constructor, emulated via a special Java method. It will also automatically emit the 'super' constructor call for AclassTest.
  • FWD will automatically emit a class-level execute method, which will contain any instance-level initialization code. In this example, this includes the unnamed widget pool, but this wil also include any instance level buffer scopes, and other instance state. The execute method will be ran before any legacy constructors are executed.
  • no Java constructors will be emitted - this is required because the instance initialization, super constructor calls, via legacy constructors must follow the same 4GL runtime support as any other procedure or function call.

Each class will also be registered in name_map.xml, via a class-mapping node:

  <class-mapping jname="oo.ClassTest" ooname="oo.ClassTest" pname="oo/ClassTest.cls"/>

When compared with class-mapping nodes associated with an external program, no method mappings will be generated for the class members - the FWD runtime will be able to gather all details from the LegacySignature annotations, at the time when the legacy class is first accessed by the runtime. Only information in the class-mapping associated with a legacy class will be for virtual methods defined at the class.

Constructors

Legacy constructors can be instance or class-level (static). In both cases, they are converted as Java methods, with the name following a specific rule, and a LegacySignature marking them as constructors.

The static constructor is always public; the instance constructors can be private, protected or public. If the class does not define any constructor, an implicit, no-argument, constructor will be emitted during conversion. For example, these constructors:
   constructor ClassTest():
   end.

   constructor static ClassTest():
   end.

will emit like:
   @LegacySignature(type = Type.CONSTRUCTOR)
   public void __oo_classTest_constructor__()
   {
      internalProcedure(ClassTest.class, this, "__oo_classTest_constructor__", new Block((Body) () -> 
      {
         __oo_aclassTest_constructor__();
      }));
   }

   @LegacySignature(type = Type.CONSTRUCTOR)
   public static void __oo_ClassTest_constructor__static__()
   {
      externalProcedure(ClassTest.class, new Block());
   }

where:
  • LegacySignature is used to mark these methods as a Type.CONSTRUCTOR
  • any constructor, if it does not explicitly call a super-constructor, it will call the default constructor for that super-class.
  • the Java method names are emitted using the legacy fully-qualified names and a constructor__ suffix - the runtime does not rely on these names, instead it uses the LegacySignature annotation to identify the Java methods marked as constructors.
  • the static constructor is called only once, when an user access this class the first time. This is done to emulate the 4GL class loading, which can't be JVM-wide, as the static fields must be private for each user. The static constructor does not call any super static constructors.

Destructors

A 4GL class can define a destructor - this is called when an instance is garbage-collected by 4GL. In FWD, this is when the instance is no longer referenced by any property, variable or temp-table field. A destructor is defined like:
   destructor ClassTest():
   end.

and in FWD will be generated as a Java method:
   @LegacySignature(type = Type.DESTRUCTOR)
   public void __oo_ClassTest_destructor__()
   {
      internalProcedure(ClassTest.class, this, "__oo_ClassTest_destructor__", new Block());
   }

where:
  • LegacySignature is used to mark these methods as a Type.DESTRUCTOR.
  • the FWD runtime will take care of calling any super-class destructors in the proper order.
  • the Java method names are emitted using the legacy fully-qualified names and a destructor__ suffix - the runtime does not rely on these names, instead it uses the LegacySignature annotation to identify the Java methods marked as destructors.

Variables

Variables can be defined as instance or static fields. Instance variables can be accessed directly from the defining class, sub-classes (depending on the access modifier), or outside the class, via a reference or the class (again, depending on the access modifier). For example, variables defined like this in a class:
   def public var v1 as int.
   def public static var v2 as int.

will convert in FWD like class fields:
   @LegacySignature(type = Type.VARIABLE, name = "v1")
   public integer v1 = UndoableFactory.integer();

   @LegacySignature(type = Type.VARIABLE, name = "v2")
   @LoadLegacyClass
   public static ContextLocal<integer> v2 = new ContextLocal<integer>()
   {
      protected integer initialValue()
      {
         return UndoableFactory.integer();
      }
   };

where:
  • LegacySignature is used to mark this field as a variable, with its legacy name
  • LoadLegacyClass is emitted only for static fields - this is required during Java compile, to apply AspectJ rules directly in the byte code, which emit a ObjectOps.load for the legacy class, which will automatically load this class (if not already loaded) in the current context, when accesing a static class variable outside of the defining class itself.
  • for instance fields, they are emitted as Java field definitions at the class
  • for static fields, they are enclosed in a ContextLocal wrapper - this is required because static fields can not be JVM-wide defined, as each user (context) requires its own 'instance' of the static class.

Variables access mode (private, @protected, public) directly map with the Java access mode for the fields.

Functions

Only virtual functions can be defined in a Java class, and these can be ran only from within the defining class. For example:

   def private var h as handle.

   function func0 returns int in h.
   function func1 returns int in super.

will emit:
   @LegacySignature(type = Type.VARIABLE, name = "h")
   private handle h = UndoableFactory.handle();

   public void __oo_ClassTest_execute__()
   {
      externalProcedure(ClassTest.class, ClassTest.this, new Block((Body) () -> 
      {
         ProcedureManager.registerFunctionHandle("func0", h);
      }));
   }

where ProcedureManager.registerFunctionHandle is required to register the handle var with this function. Any virtual function call will be emulated in the converted Java code as if DYNAMIC-FUNCTION has been used on that call.

In name_map.xml, the class mapping will register these virtual functions:

  <class-mapping jname="oo.ClassTest" ooname="oo.ClassTest" pname="oo/ClassTest.cls">
    <method-mapping in-handle="true" jname="func0" pname="func0" returns="INTEGER" type="FUNCTION"/>
    <method-mapping in-super="true" jname="func1" pname="func1" returns="INTEGER" type="FUNCTION"/>
  </class-mapping>

Class Events

Class events are a special construct in FWD converted code, they are emulated by the p2j.util.ClassEvent class, with special methods emitted in the Java class, to allow the SUBSCRIBE, UNSUBSCRIBE and PUBLISH 4GL methods for the class events. FWD supports arguments at the event signature, the same as 4GL allows it.

For example, for a Login class event which accepts a uid user ID and a uname username argument:

   define public event Login signature void (input uid as int, input uname as char).

   method private void login(input uid as int, input uname as char).
   end.

   method public void mlogin():
      Login:Subscribe(login). 
      Login:Publish(1, "fwdtest").
   end.

FWD will emit code like this:

   @LegacySignature(type = Type.EVENT, name = "Login", parameters = 
   {
      @LegacyParameter(name = "uid", type = "INTEGER", mode = "INPUT"),
      @LegacyParameter(name = "uname", type = "CHARACTER", mode = "INPUT")
   })
   private ClassEvent login = new ClassEvent(ClassTest.this, "Login", "login");

   @LegacySignature(type = Type.PUBLISH, name = "Login", parameters = 
   {
      @LegacyParameter(name = "uid", type = "INTEGER", mode = "INPUT"),
      @LegacyParameter(name = "uname", type = "CHARACTER", mode = "INPUT")
   })
   private void publish_login(integer uid, character uname)
   {
      login.publish(uid, uname);
   }

   @LegacySignature(type = Type.SUBSCRIBE, name = "Login", parameters = 
   {
      @LegacyParameter(name = "p1", type = "handle", mode = "INPUT"),
      @LegacyParameter(name = "p2", type = "character", mode = "INPUT")
   })
   public void subscribe_login(handle h, character procName)
   {
      login.subscribe(h, procName);
   }

   @LegacySignature(type = Type.SUBSCRIBE, name = "Login", parameters = 
   {
      @LegacyParameter(name = "p1", type = "object", mode = "INPUT", qualified = "progress.lang.object"),
      @LegacyParameter(name = "p2", type = "character", mode = "INPUT")
   })
   public void subscribe_login(object<? extends com.goldencode.p2j.oo.lang._BaseObject_> ref, character methName)
   {
      login.subscribe(ref, methName);
   }

   @LegacySignature(type = Type.UNSUBSCRIBE, name = "Login", parameters = 
   {
      @LegacyParameter(name = "p1", type = "handle", mode = "INPUT"),
      @LegacyParameter(name = "p2", type = "character", mode = "INPUT")
   })
   public void unsubscribe_login(handle h, character procName)
   {
      login.unsubscribe(h, procName);
   }

   @LegacySignature(type = Type.UNSUBSCRIBE, name = "Login", parameters = 
   {
      @LegacyParameter(name = "p1", type = "object", mode = "INPUT", qualified = "progress.lang.object"),
      @LegacyParameter(name = "p2", type = "character", mode = "INPUT")
   })
   public void unsubscribe_login(object<? extends com.goldencode.p2j.oo.lang._BaseObject_> ref, character methName)
   {
      login.unsubscribe(ref, methName);
   }

   @LegacySignature(type = Type.METHOD, name = "login", parameters = 
   {
      @LegacyParameter(name = "uid", type = "INTEGER", mode = "INPUT"),
      @LegacyParameter(name = "uname", type = "CHARACTER", mode = "INPUT")
   })
   private void login(final integer _uid_1, final character _uname_1)
   {
      integer uid_1 = TypeFactory.initInput(_uid_1);
      character uname_1 = TypeFactory.initInput(_uname_1);

      internalProcedure(ClassTest.class, this, "login", new Block());
   }

   @LegacySignature(type = Type.METHOD, name = "mlogin")
   public void mlogin()
   {
      internalProcedure(ClassTest.class, this, "mlogin", new Block((Body) () -> 
      {
         subscribe_login(ObjectOps.thisObject(), new character("login"));
         publish_login(new integer(1), new character("fwdtest"));
      }));
   }

where:

  • ClassEvent login is a class instance field which defines the event - it receives as arguments the defining Java class, its legacy name and also its converted Java field name.
  • publish_login is a private Java method associated with the event's PUBLISH method for the 4GL class event - the signature is the same as the event's signature, and delegates the call to ClassEvent.publish.
  • subscribe_login is a Java method associated with the SUBSCRIBE method for the 4GL class event, using the access mode set at the legacy event definition in the 4GL code. These methods have two signatures:
    • subscribe_login(handle, character), when is called for an external program.
    • subscribe_login(object ref, character methName), when is called for a legacy object.
      In each case, the second argument is the name of the procedure (in case of external program) or method (in case of legacy object) called when the event is published.
  • unsubscribe_login is a Java method associated with the UNSUBSCRIBE method for the 4GL class event, using the access mode set at the legacy event definition in the 4GL code. These methods have two signatures:
    • unsubscribe_login(handle, character), when is called for an external program.
    • unsubscribe_login(object ref, character methName), when is called for a legacy object.
      In each case, the second argument is the name of the procedure (in case of external program) or method (in case of legacy object) called when the event is published.
  • login is the event handler for the subscribe_login called in mlogin method.

Each event-related method above and the original class field associated with the event will have LegacySignature annotations describing how this member associates with the legacy class event.

Scalar Properties

Class properties in converted Java code will never be accessed directly by the callers - explicit getter and setter will be emitted for each property, although the property will still have a Java field associated with it (which can be accessed direclty from the getter/setter). This field will always be private, while the getter and setter methods will follow the legacy property's access mode.

A legacy property can be defined static, abstract or at an interface. In each case, the generated Java methods will have the same signatures. For example:

   def public property p1 as int get. set.
   def public static property p2 as int get. set.

will have this FWD generated code:
   @LegacySignature(type = Type.PROPERTY, name = "p1")
   private integer p1 = UndoableFactory.integer();

   @LegacySignature(type = Type.PROPERTY, name = "p2")
   private static ContextLocal<integer> p2 = new ContextLocal<integer>()
   {
      protected integer initialValue()
      {
         return UndoableFactory.integer();
      }
   };

   @LegacySignature(type = Type.GETTER, name = "p1", returns = "INTEGER")
   public integer getP1()
   {
      return function(ClassTest.class, this, "p1", integer.class, new Block((Body) () -> 
      {
         returnNormal(p1);
      }));
   }

   @LegacySignature(type = Type.SETTER, name = "p1", parameters = 
   {
      @LegacyParameter(name = "var", type = "INTEGER", mode = "INPUT")
   })
   public void setP1(final integer _var)
   {
      integer var = TypeFactory.initInput(_var);

      internalProcedure(ClassTest.class, this, "p1", new Block((Body) () -> 
      {
         p1.assign(var);
      }));
   }

   @LegacySignature(type = Type.GETTER, name = "p2", returns = "INTEGER")
   public static integer getP2()
   {
      ObjectOps.load(ClassTest.class);

      return function(ClassTest.class, "p2", integer.class, new Block((Body) () -> 
      {
         returnNormal(p2.get());
      }));
   }

   @LegacySignature(type = Type.SETTER, name = "p2", parameters = 
   {
      @LegacyParameter(name = "var", type = "INTEGER", mode = "INPUT")
   })
   public static void setP2(final integer _var)
   {
      ObjectOps.load(ClassTest.class);
      integer var = TypeFactory.initInput(_var);

      internalProcedure(ClassTest.class, "p2", new Block((Body) () -> 
      {
         p2.get().assign(var);
      }));
   }

The static property is wrapped in a ContextLocal instance, to allow its value to be isolated to the user (context) using this class. Java LegacySignature annotations are added at both the field associated with the legacy property, and the Java methods associated with the getter and setter.

Any abstract properties (or properties defined at a legacy interface) will have only the getter and setter generated (without the Java field), as Java abstract methods - their signature and annotations is the same, just the body is not emitted. The field associated with the property plus the getter and setter body will only be emitted at the class defining the non-abstract property.

If a custom getter or setter is specified at the property, or the getter or setter is omitted, the FWD will work in the same way - the getter/setter will contain the custom definition, or be omitted, when missing.

Extent Properties

Extent properties work in a similar way as scalar properties - they have getter and setter Java methods generated, plus some additional Java methods to allow the EXTENT function and statement calls on this property. For example, these dynamic extent property definitions:
   def public property p1 as int extent get. set.
   def public static property p2 as int extent get. set.

will convert to these Java fields:
   @LegacySignature(type = Type.PROPERTY, extent = -1, name = "p1")
   private integer[] p1 = UndoableFactory.integerExtent();

   @LegacySignature(type = Type.PROPERTY, extent = -1, name = "p2")
   private static ContextLocal<integer[]> p2 = new ContextLocal<integer[]>()
   {
      protected integer[] initialValue()
      {
         return UndoableFactory.integerExtent();
      }
   };

where:
  • LegacySignature is used to mark this field as a legacy property. The extent value set to -1 represents a dynamic extent.
  • static properties need to be enclosed in a ContextLocal instance.
  • Java arrays are used to represent the extent.
For the instance property, these methods are generated:
  • indexed and bulk getter:
       @LegacySignature(type = Type.GETTER, name = "p1", returns = "INTEGER", parameters = 
       {
          @LegacyParameter(name = "idx", type = "INT64", mode = "INPUT")
       })
       public integer getP1(final int64 _idx)
       {
          int64 idx = TypeFactory.initInput(_idx);
    
          return function(ClassTest.class, this, "p1", integer.class, new Block((Body) () -> 
          {
             returnNormal(subscript(p1, idx));
          }));
       }
    
       @LegacySignature(type = Type.GETTER, name = "p1", extent = -1, returns = "INTEGER")
       public integer[] bulkGetP1()
       {
          return extentFunction(ClassTest.class, this, "p1", integer.class, new Block((Body) () -> 
          {
             returnExtentNormal(p1);
          }));
       }
    
  • indexed and bulk setter:
       @LegacySignature(type = Type.SETTER, name = "p1", parameters = 
       {
          @LegacyParameter(name = "var", type = "INTEGER", mode = "INPUT"),
          @LegacyParameter(name = "idx", type = "INT64", mode = "INPUT")
       })
       public void setP1(final integer _var, final int64 _idx)
       {
          integer var = TypeFactory.initInput(_var);
          int64 idx = TypeFactory.initInput(_idx);
    
          internalProcedure(ClassTest.class, this, "p1", new Block((Body) () -> 
          {
             assignSingle(p1, idx, var);
          }));
       }
    
       @LegacySignature(type = Type.SETTER, name = "p1", parameters = 
       {
          @LegacyParameter(name = "var", type = "INTEGER", extent = -1, mode = "INPUT")
       })
       public void bulkSetP1(final integer[] _var)
       {
          integer[] var[] = 
          {
             TypeFactory.initInput(_var)
          };
    
          internalProcedure(ClassTest.class, this, "p1", new Block((Body) () -> 
          {
             p1 = (integer[]) assignMulti(p1, var[0]);
          }));
       }
    
  • setter which assigns all elements to a certain value:
    @LegacySignature(type = Type.SETTER, name = "p1", parameters = {
    @LegacyParameter(name = "var", type = "INTEGER", mode = "INPUT")
    })
    public void setAllP1(final integer _var) {
    integer var = TypeFactory.initInput(_var);
  • Java method associated with the extent function called on this property, to get its length:
       @LegacySignature(type = Type.LENGTH, name = "p1", returns = "INTEGER")
       public integer lengthOfP1()
       {
          return function(ClassTest.class, this, "length-of-p1", integer.class, new Block((Body) () -> 
          {
             returnNormal(ArrayAssigner.lengthOf(p1));
          }));
       }
    
  • Java method associated with the extent statement called on this property, to resize it:
       @LegacySignature(type = Type.RESIZE, name = "p1", parameters = 
       {
          @LegacyParameter(name = "size", type = "INT64", mode = "INPUT")
       })
       public void resizeP1(final int64 _size)
       {
          int64 size = TypeFactory.initInput(_size);
    
          internalProcedure(ClassTest.class, this, "resize-p1", new Block((Body) () -> 
          {
             p1 = ArrayAssigner.resize(p1, size);
          }));
    
internalProcedure(ClassTest.class, this, "p1", new Block((Body) () -> 
{
assignMulti(p1, var);
}));
}

Each Java method uses a LegacySignature to identify this method as a getter, setter or special-purpose method for resize or length.

In case of a static extent property, the same methods are emitted, but the access to the static field is different (via the ContextLocal instance):


   @LegacySignature(type = Type.GETTER, name = "p2", returns = "INTEGER", parameters = 
   {
      @LegacyParameter(name = "idx", type = "INT64", mode = "INPUT")
   })
   public static integer getP2(final int64 _idx)
   {
      ObjectOps.load(ClassTest.class);
      int64 idx = TypeFactory.initInput(_idx);

      return function(ClassTest.class, "p2", integer.class, new Block((Body) () -> 
      {
         returnNormal(subscript(p2.get(), idx));
      }));
   }

   @LegacySignature(type = Type.GETTER, name = "p2", extent = -1, returns = "INTEGER")
   public static integer[] bulkGetP2()
   {
      ObjectOps.load(ClassTest.class);

      return extentFunction(ClassTest.class, "p2", integer.class, new Block((Body) () -> 
      {
         returnExtentNormal(p2.get());
      }));
   }

   @LegacySignature(type = Type.SETTER, name = "p2", parameters = 
   {
      @LegacyParameter(name = "var", type = "INTEGER", mode = "INPUT"),
      @LegacyParameter(name = "idx", type = "INT64", mode = "INPUT")
   })
   public static void setP2(final integer _var, final int64 _idx)
   {
      ObjectOps.load(ClassTest.class);
      integer var = TypeFactory.initInput(_var);
      int64 idx = TypeFactory.initInput(_idx);

      internalProcedure(ClassTest.class, "p2", new Block((Body) () -> 
      {
         assignSingle(p2.get(), idx, var);
      }));
   }

   @LegacySignature(type = Type.SETTER, name = "p2", parameters = 
   {
      @LegacyParameter(name = "var", type = "INTEGER", extent = -1, mode = "INPUT")
   })
   public static void bulkSetP2(final integer[] _var)
   {
      ObjectOps.load(ClassTest.class);
      integer[] var[] = 
      {
         TypeFactory.initInput(_var)
      };

      internalProcedure(ClassTest.class, "p2", new Block((Body) () -> 
      {
         p2.set(((integer[]) assignMulti(p2.get(), var[0])));
      }));
   }

   @LegacySignature(type = Type.SETTER, name = "p2", parameters = 
   {
      @LegacyParameter(name = "var", type = "INTEGER", mode = "INPUT")
   })
   public static void setAllP2(final integer _var)
   {
      ObjectOps.load(ClassTest.class);
      integer var = TypeFactory.initInput(_var);

      internalProcedure(ClassTest.class, "p2", new Block((Body) () -> 
      {
         assignMulti(p2.get(), var);
      }));
   }

   @LegacySignature(type = Type.LENGTH, name = "p2", returns = "INTEGER")
   public static integer lengthOfP2()
   {
      ObjectOps.load(ClassTest.class);

      return function(ClassTest.class, "length-of-p2", integer.class, new Block((Body) () -> 
      {
         returnNormal(ArrayAssigner.lengthOf(p2.get()));
      }));
   }

   @LegacySignature(type = Type.RESIZE, name = "p2", parameters = 
   {
      @LegacyParameter(name = "size", type = "INT64", mode = "INPUT")
   })
   public static void resizeP2(final int64 _size)
   {
      ObjectOps.load(ClassTest.class);
      int64 size = TypeFactory.initInput(_size);

      internalProcedure(ClassTest.class, "resize-p2", new Block((Body) () -> 
      {
         p2.set(ArrayAssigner.resize(p2.get(), size));
      }));
   }

All Java methods follow the access mode set at the legacy property, while the field being set to private.

Methods

Class methods can be defined as static, abstract or instance. Interface methods convert as public methods, with the same signature as the legacy method definition, but with no body. Abstract methods will have the Java abstract option emitted at the method, while no body is emitted for it. Any override method in the legacy class will have the @Override annotation emitted at the Java method.

For example, these methods defined at a class:
   method public void m1().
   end.

   method public static void m2().
   end.

   method public Progress.Lang.Object m3().
   end.

   method public int extent 5 m4().
   end.

will emit like this in the Java code:
   @LegacySignature(type = Type.METHOD, name = "m1")
   public void m1()
   {
      internalProcedure(ClassTest.class, this, "m1", new Block());
   }

   @LegacySignature(type = Type.METHOD, name = "m2")
   public static void m2()
   {
      internalProcedure(ClassTest.class, "m2", new Block());
   }

   @LegacySignature(type = Type.METHOD, name = "m3", returns = "OBJECT", qualified = "progress.lang.object")
   public object<? extends com.goldencode.p2j.oo.lang._BaseObject_> m3()
   {
      return function(ClassTest.class, this, "m3", object.class, new Block());
   }

   @LegacySignature(type = Type.METHOD, name = "m4", extent = 5, returns = "INTEGER")
   public integer[] m4()
   {
      return extentFunction(ClassTest.class, this, "m4", integer.class, 5, new Block());
   }

where:
  • LegacySignature is used to:
    • idenfity this Java method as a legacy method
    • set its legacy method name
    • set the return type, using the returns annotation to specify the legacy data type, extent in case of an extent return type and qualified in case of an OBJECT type.
  • the method's body is executed:
    • using BlockManager.internalProcedure, for methods returning void.
    • using BlockManager.function and extentFunction, for methods returning a non-void type.

The Java method's access mode follows the access mode specified in the legacy class method.


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