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.
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:
typeis set toType.PROCEDURE, to identify this as a internal procedure, at runtimenamerepresents 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:
typeis set toType.FUNCTION, to idenfity this as a user-defined function, at runtime.namerepresents the legacy user-defined function name, which may not be the same as the converted Java name.returnsrepresents 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,returnswill be set toOBJECT.
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:
eventsrepresents the list of events and widgets for which this trigger executes.containis 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.transis a flag which is set totrueonly if this trigger should start a full transaction and is set tofalseif the trigger should be a sub-transaction.triggeris:- a singleton
Triggerinstance, 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
- a singleton
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:
procrepresents the internal or external procedure's name to be executed.instrepresents the external program where the lookup for an internal procedure is performedparamsrepresents 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
Triggerinstance, with the constructor receiving a lambdaBody()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
Triggerclass, where the trigger block itself is emitted inside thebody()method. All local resources are promoted to instance members of the anonymousTriggerclass, 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.
- as long as the non-persistent external program is on the stack
- until the persistent program gets deleted
PERSISTENT RUN clause:
PERSISTENT RUN target, wheretargetis 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 specifiedexprhandle. The evaluation of theexprexpression 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.
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
objectfields, 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
getEnummethods 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 specifyFINAL 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:
finalis converted to Javafinalmodifier at the Java class.- an
abstractclass would have the Javaabstractmodifier at the Java class. use-widget-poolis converted as aWidgetPool.create();call for an unnamed pool associated with this class instance.serializableis converted by emitting aLegacySerializableannotation.- 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
executemethod, 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. Theexecutemethod 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.
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:
LegacySignatureis used to mark these methods as aType.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 theLegacySignatureannotation 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:
LegacySignatureis used to mark these methods as aType.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 theLegacySignatureannotation 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:
LegacySignatureis used to mark this field as a variable, with its legacy nameLoadLegacyClassis emitted only for static fields - this is required during Java compile, to apply AspectJ rules directly in the byte code, which emit aObjectOps.loadfor 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
ContextLocalwrapper - 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 loginis 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_loginis a private Java method associated with the event'sPUBLISHmethod for the 4GL class event - the signature is the same as the event's signature, and delegates the call toClassEvent.publish.subscribe_loginis a Java method associated with theSUBSCRIBEmethod 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 extends com.goldencode.p2j.oo.lang._BaseObject_> 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_loginis a Java method associated with theUNSUBSCRIBEmethod 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 extends com.goldencode.p2j.oo.lang._BaseObject_> 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.
loginis the event handler for thesubscribe_logincalled inmloginmethod.
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 theEXTENT 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:
LegacySignatureis used to mark this field as a legacy property. Theextentvalue set to-1represents a dynamic extent.staticproperties need to be enclosed in aContextLocalinstance.- Java arrays are used to represent the extent.
- 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]); })); } setterwhich 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
extentfunction 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
extentstatement 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.
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:
LegacySignatureis used to:- idenfity this Java method as a legacy method
- set its legacy method name
- set the return type, using the
returnsannotation to specify the legacy data type,extentin case of an extent return type andqualifiedin case of anOBJECTtype.
- the method's body is executed:
- using
BlockManager.internalProcedure, for methods returningvoid. - using
BlockManager.functionandextentFunction, for methods returning a non-void type.
- using
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.