Project

General

Profile

Direct Java Access

Introduction

4GL code in FWD can directly reference Java classes, interfaces, enums and their members. Methods can be called, data members can be read and written. The conversion also provides some basic boxing/unboxing of primitive Java types to the standard 4GL data types, making it easier to cleanly mix 4GL and Java constructs.

This direct access to Java code from within 4GL code is a simple yet flexible mechanism for exploiting the full power of the Java platform. Any Java resource can be accessed or invoked directly from 4GL code. This can be used with resources from the J2SE platform itself, any 3rd party Java technology, and your own custom Java code.

This feature is similar to how the 4GL supports direct access to .NET classes, except that there is no "bridge" needed since the converted code is already in the JVM and is real Java code. One simply uses the normal 4GL OO syntax with Java resources instead of 4GL OO resources, with some slight syntax enhancements (e.g. the USING statement).

This feature was implemented in #3867 which was first committed to trunk as revision 11339 and then the final improvements were done in branch 4335a. The first public release is FWD v4.0.

CLASSPATH Implications

Usage of this feature has implications for the CLASSPATH at both conversion and runtime. You must know the exact list of Java dependencies (the classes referenced). This is no different from any other OO implementation.

Conversion

At conversion time, FWD uses Java reflection to parse/convert direct references to any Java class. This means that the classes (or more likely the jars in which the classes reside) must be included in the CLASSPATH for the ConversionDriver. Often this is something that may be scripted with ant or gradle but it can also be manually run or a shell script that executes the ConversionDriver. Find the places it is executed and update the CLASSPATH as needed.

This extra configuration is not needed for code that converted from traditional 4GL (e.g. a RUN or DISPLAY statement). It is only needed for direct Java references, which are usually to classes that have been hand-written in Java. If there is a need to directly reference converted code, then there are 2 options:

  • Is there a way to rewrite the reference as real 4GL code and let it convert naturally? That is the preferred approach. OR
  • Copy existing converted jars into the conversion environment to bootstrap the build. This is awkward and requires some level of manual intervention whenever the accessed APIs are changed. It can work, but this is something to avoid if possible.

Runtime

When the converted code is executed, any referenced Java classes will need to be available via the runtime CLASSPATH. These could be added to the CLASSPATH of the FWD server when it is started, but the best/simplest approach is to add the dependencies to the converted application's manifest file. In the standard project setup, there is a manifest/<application_name>.mf file which is the jar manifest. Find that file wherever it exists in the project and update the Class-Path setting inside the file to include the additional dependencies. See Working with Manifest Files for some details on the syntax.

Implementation

Syntax and Capabilities

  • The syntax is the same as the 4GL OO syntax. There is no difference except a minor change in the USING statement (see below).
  • Features such as CAST(), NEW and so forth all will work as you expect.
  • Java object-based type names (classes, interfaces and enums) can be used wherever a 4GL type name can be used.
  • Java object types can be passed and used just as 4GL types are used. This means you can define and instantiate variables and members as Java types, you can read the values, pass them as parameters and use them on the left or right side of an assignment.
  • Java primitive types cannot be used directly. These are not needed because the 4GL primitive types exist and are supported. Combined with the auto-boxing/auto-unboxing (see below), it is easy to use the 4GL primitive types in contexts that would otherwise require a Java primitive type.
  • Any Java object type can be de-referenced using the : operator.
    • static and instance method calls
    • static and instance member field references
  • Chaining of : is supported.
  • 4GL code must use case-sensitive names for all Java names (classes/interfaces, methods and fields).
  • When passing Java types to a 4GL function, method or procedure, only INPUT mode parameters are supported (you can leave off the mode or specify INPUT, but any use of INPUT-OUTPUT or OUTPUT will generate an error).

Example

This code uses Java's built-in trigonometric functions and constants (see abl/ladder-safety.p in Hotel GUI).

using java.lang.* from java.

def input  parameter ladder-len           as decimal.
def input  parameter max-safe-angle-deg   as integer.
def output parameter wall-height          as decimal.
def output parameter ground-distance      as decimal.
def output parameter ok                   as logical.

def var max-safe-angle-rad   as decimal.

ok = false.

// convert our angle from degrees to radians
max-safe-angle-rad = max-safe-angle-deg * Math:PI / 180.

if ladder-len eq ? then
do:
   message "Unknown value is a very unique length.  Good luck with that!".
end.
else if ladder-len <= 0 then
do:
   message "Your ladder is useless, get a new ladder!".
end.
else
do:
   ok = true.

   // opposite = hypotenuse * sin(max-safe-angle)
   wall-height = ladder-len * Math:sin(max-safe-angle-rad).

   // adjacent = hypotenuse * cos(max-safe-angle)
   ground-distance = ladder-len * Math:cos(max-safe-angle-rad).
end.

Example for how to use Java's collection classes:

using java.util.* from java.
using java.lang.* from java.

def var hmap as HashMap.
def var whatever as char init "yep".

hmap = new HashMap().

hmap:put("something", whatever).

if hmap:containsKey("nothing") then
do:
   message "not here".
end.

USING Statement

4GL code can use fully qualified Java type names without any USING statement. For example, it is OK to write this:

DEFINE VARIABLE excellent-map AS java.util.HashMap.

excellent-map = new java.util.HashMap().

// now use the map...

In order for unqualified Java type names to be referenced, a USING statement is needed.

USING java.util.HashMap FROM JAVA.

DEFINE VARIABLE excellent-map AS HashMap.

excellent-map = new HashMap().

// now use the map...

It is also OK to use wildcard imports as in:

USING java.util.* FROM JAVA.

DEFINE VARIABLE excellent-map AS HashMap.

excellent-map = new HashMap().

// now use the map...

These statements follow some rules:

  • You must include FROM JAVA in any USING statement that references either a Java package or an explicit Java class.
  • All USING statements must be explicit. There is no implicit USING java.lang.* FROM JAVA.
  • There is no equivalent to the PROPATH search for a Java class. Any search from USING some.package.* FROM JAVA will add that package to the list of package prefixes that will be prepended to possible Java class names to try to get the Class instance for that name. FWD uses Class.forName() to obtain the class instance. This will lookup from whatever sources are in the Java classpath for the ConversionDriver. This is not the same idea as a PROPATH lookup which is inherently file-system based.
  • Any Java classes that are referenced must be present in the classpath of both conversion and the FWD server runtime.

Automatic Boxing/Unboxing

In order to simplify the integration of Java and 4GL code, FWD provides implicit conversion between common 4GL types and their closest Java equivalents. The implicit conversion is often referred to as boxing (when a Java type is "wrapped" into a 4GL type) and unboxing (when a 4GL type is "unwrapped" to a Java type).

These conversions will occur for:

  • return values (e.g. 4GL function return values, Java method return values)
  • parameters (e.g. 4GL function parameters, RUN statement parameters, Java method parameters)
  • reading a data member or local variable
  • assignment of a member or local variable

Supported Conversions

These conversions can occur in either direction (boxing or unboxing).

4GL Type Java Type
character java.lang.String
date java.util.Date
datetime java.sql.Timestamp
decimal double, java.lang.Double, java.lang.BigDecimal
integer int, java.lang.Integer, long, java.lang.Long, double, java.lang.Double, java.lang.BigDecimal
int64 long, java.lang.Long, java.lang.BigDecimal
logical boolean, java.lang.Boolean
longchar java.lang.String
raw byte[]

Both array and non-array forms are handled.

There is a special case for arrays defined like def var arr as java.lang.Object extent 5.. For this case, where the array's type is a Java type:
  • there will be no conversion of the array's element type (if is a BaseDataType) to its Java equivalent. So, any array where a Java type is specified, that value will be passed exactly to the argument.
  • arrays with Java native types are not supported.

Examples

TBD

Exceptions

Handling Java exceptions using the 4GL CATCH block are fully supported. However, the conversion is different from how 4GL exceptions are handled. The reason is that the 4GL exceptions must be delegated to the FWD runtime where there are additional compatibility behaviors implemented. This is provided by using lambdas for the catch blocks and calling BlockManager.catchError(Class<T> type, Consumer<object<T>> catchBlock) to register the catch block with the runtime. The try/catch is in the BlockManager in this case and the lambda is passed to the BlockManager in the init() of the containing Block instance.

Java exceptions do not have the additional 4GL compatibility requirements, so they are handled "inline" using the Java try/catch.

All Java exceptions are converted in the same way. This means that unchecked exceptions (anything deriving from Error or RuntimeException) will emit the same way as any checked exception (e.g. IOException). In both cases, the conversion emits a try and catch into the Java code. In the Java catch block, the logic is emitted as a lambda that is passed to BlockManager.processCatch(Body body). The block manager does not catch the exception directly. The business logic catches it and then calls to processCatch() inside the catch block itself.

It is perfectly valid to include catch blocks for both Java and 4GL exceptions. The conversion emits each according to the type as described above.

To illustrate the results, see this 4GL code:

using java.lang.* from java.

def var i as int.

function f returns int ():
   message "whatever".

   catch e as java.lang.RuntimeException:
      message "user-defined function".
   end.
end.

/* CATCH block may only be associated with an undoable block. (14140) */
/*
do:
   run bogus.

   catch e as Progress.Lang.AppError:
      message "simple do".
   end.
end.
*/

do transaction:
   run bogus.

   catch e as java.lang.RuntimeException:
      message "transaction do".
   end.
end.

repeat: 
   i = i / 9000.
   leave.

   catch e as java.lang.RuntimeException:
      message "repeat".
   end.   
end.

on f10 anywhere
do:
   message "whatever".

   catch e as java.lang.RuntimeException:
      message "UI trigger".
   end.
end.

/* CATCH block may only be associated with an undoable block. (14140) */
/*
update i editing:
   readkey.
   apply lastkey.

   catch e as Progress.Lang.AppError:
      message "editing block".
   end.   
end.
*/

catch e as java.lang.RuntimeException:
   message "external proc".
end.

catch e2 as Progress.Lang.Error:
   message "external proc 2".
end.

procedure bogus:
   message "whatever".

   catch e as java.lang.RuntimeException:
      message "internal proc".
   end.
end.

will generate this Java code:

package com.goldencode.testcases.oo;

import com.goldencode.p2j.util.*;
import java.lang.*;
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.util.MathOps.*;
import static com.goldencode.p2j.ui.LogicalTerminal.*;

/**
 * Business logic (converted to Java from the 4GL source code
 * in oo/catch_blocks_in_many_variations_with_direct_java.p).
 */
public class CatchBlocksInManyVariationsWithDirectJava
{
   /**
    * External procedure (converted to Java from the 4GL source code
    * in oo/catch_blocks_in_many_variations_with_direct_java.p).
    */
   public void execute()
   {
      integer i = UndoableFactory.integer();

      externalProcedure(CatchBlocksInManyVariationsWithDirectJava.this, new Block((Init) () -> 
      {
         catchError(com.goldencode.p2j.oo.lang.LegacyError.class, 
         e2 -> 
         {
            message("external proc 2");
         });
      }, 
      (Body) () -> 
      {
         try
         {
            doBlock(TransactionType.FULL, "blockLabel0", new Block((Body) () -> 
            {
               try
               {
                  ControlFlowOps.invoke("bogus");
               }
               catch (java.lang.RuntimeException e)
               {
                  processCatch(() -> 
                  {
                     message("transaction do");
                  });
               }
            }));

            repeat("loopLabel0", new Block((Body) () -> 
            {
               try
               {
                  i.assign(divide(i, 9000));
                  leave("loopLabel0");
               }
               catch (java.lang.RuntimeException e_1)
               {
                  processCatch(() -> 
                  {
                     message("repeat");
                  });
               }
            }));

            registerTrigger(new EventList("f10", true), CatchBlocksInManyVariationsWithDirectJava.this, () -> new Trigger()
            {
               public void pre()
               {
               }

               public void init()
               {
               }

               public void body()
               {
                  try
                  {
                     message("whatever");
                  }
                  catch (java.lang.RuntimeException e_2)
                  {
                     processCatch(() -> 
                     {
                        message("UI trigger");
                        /* CATCH block may only be associated with an undoable block. (14140) */
                        /*
                        update i editing:
                           readkey.
                           apply lastkey.

                           catch e as Progress.Lang.AppError:
                              message "editing block".
                           end.   
                        end.
                        */
                     });
                  }
               }
            });
         }
         catch (java.lang.RuntimeException e_2)
         {
            processCatch(() -> 
            {
               message("external proc");
            });
         }
      }));
   }

   @LegacySignature(type = Type.FUNCTION, name = "f")
   public integer f()
   {
      return function(this, "f", integer.class, new Block((Body) () -> 
      {
         try
         {
            message("whatever");
         }
         catch (java.lang.RuntimeException e)
         {
            processCatch(() -> 
            {
               message("user-defined function");
               /* CATCH block may only be associated with an undoable block. (14140) */
               /*
               do:
                  run bogus.

                  catch e as Progress.Lang.AppError:
                     message "simple do".
                  end.
               end.
               */
            });
         }
      }));
   }

   @LegacySignature(type = Type.PROCEDURE, name = "bogus")
   public void bogus()
   {
      internalProcedure(new Block((Body) () -> 
      {
         try
         {
            message("whatever");
         }
         catch (java.lang.RuntimeException e_3)
         {
            processCatch(() -> 
            {
               message("internal proc");
            });
         }
      }));
   }
}

Security Considerations

Direct Java Access Exposes Danger

Converted 4GL code is quite safe:

  • Dangerous aspects have been pushed into runtime code that is well vetted and sandboxed such that the features are safe.
  • The code generated by the conversion itself is created in a manner that is designed to never provide access to anything that is dangerous, nor anything that could not be accessed from the 4GL.
  • The error processing for the converted 4GL code uses exceptions that are designed to match 4GL error behavior. Any cases where there is an unexpected Java exception raised will be treated as an abend and the session will safely exit as if a kind of uncatchable QUIT condition has occurred. The session level transaction processing will rollback/undo as you would expect, leaving the system in a consistent state.
  • In all these cases, all conversion processing occurs inside a dedicated security context and the application server process itself is isolated from the session-level
  • No access to the server file system or server resources is possible from converted 4GL code.

The direct Java access is an exception. It can call anything that is accessible from Java code which means it is a full bypass of all of the above careful sandboxing and planning.

With great power comes great responsibility.

If you use direct Java access you must consider the security implications of any and all such access. You could easily destabilize or crash the server, corrupt your data or open up security flaws that can be exploited by malicious actors. Be very careful. Ask for help if you aren't sure if something is safe.

Security Framework

Java itself includes a low-level security framework which can be used to implement some secure facilities. The primary problem with the Java security framework is that it is not designed for application-level usage.

In contrast, FWD includes a complete security framework that is designed to provide a consistent, pluggable/extendable facility for securing applications. For more details, please see:

SecurityManager Architecture
Security Framework
Runtime Hooks and Plug-Ins

Functional Limitations

Known Bugs and Problem Areas

Use of Java directly in 4GL control flow structures probably needs some work. An example is #5558.

The auto-boxing/auto-unboxing is another area needing some work. #4620 shows an example related to arrays of boxed types.

Inheritance

OO 4GL classes cannot inherit from Java classes.

While it is possible to have a Java class that inherits from an OO 4GL class, this comes with some trickiness at build time since the converted code must be available for javac compile.

Reserved Keyword Conflicts

Java classnames and member names which are reserved keywords might need to be qualified to work as a standalone reference or as the leftmost reference (the "anchor") of a chain of object invocations.

As an example, the java.awt.Color class has a set of members of well known colors. It would be a common use case to reference these using syntax like Color:RED. The problem here is that when you use Color:RED, it cannot parse because Color will lex as the KW_COLOR which is a reserved 4GL keyword. The core issue here is that 4GL keywords take precedence over Java type names. Anything that is the "leftmost" token of an expression or sub-expression cannot have any conflicts with reserved 4GL keywords. The simple solution here is to use a fully qualified classname java.awt.Color:RED instead of the unqualfied Color:RED.

Java Language Features

As a general rule, features that exist in the Java language but which have no counterpart in OO 4GL cannot be used.

For example, there is no support for:

  • generics
  • varargs
  • multi-dimensional arrays
  • lambdas

It is possible for FWD to be extended to add these features to the OO 4GL itself, but this has not been done at this time.


© 2019-2021 Golden Code Development Corporation. ALL RIGHTS RESERVED.