Project

General

Profile

Integrating External Applications

Introduction

It is a common requirement for external applications to need access to services running in the FWD Application Server. These services can largely be organized into 2 groupings:

  • 4GL-compatible (converted 4GL code)
  • pure Java (not converted code, usually but not always hand written)

To integrate with external applications, some network accessible API must be used to access these services. This chapter will provide details on how to build and expose these remotely accessible APIs in a FWD Application Server.

4GL-Compatible REST and SOAP Services

Introduction

FWD provides support for converting and running/exporting 4GL code that expose REST and SOAP APIs.

Conversion Support

See Converting REST and SOAP for details.

Configuration and Setup

TBD: The details should be in the Installation, Configuration and Administration Guide but some summary and links should be here.

Post-Conversion Development

TBD: Provide examples of the conversion output, describe how things map and how changes can be made without breaking the functionality.

4GL Appserver API

Introduction

FWD provides support for the conversion and execution of 4GL code via appserver. This can be used for integrating with external systems, but some considerations/effort is needed.

Configuration and Setup

TBD: The details should be in the Installation, Configuration and Administration Guide but some summary and links should be here. We also need to ensure that the security implications/configuration is considered (accounts for remote access, permissions...).

Replacing 4GL Access

TBD: This can only be done from converted code and it is transparent when inside a single server. We need to detail this and also explain how cross-server access can be made to work.

Replacing the Open Client for Java

Introduction

Remote access to 4GL code from Java is supported using a replacement for the Open Client for Java (an OpenEdge facility). This replacement is a set of classes in FWD. These classes are not binary-compatible with the OpenEdge code (Open Client for Java). Instead, the equivalent features exist and existing code must be reworked to use different classes.

Guide

TBD: We need to write a guide on how to do this including some example code.

TBD: Link to the API reference to the FWD classes that are the replacements (this probably can just link to our existing javadoc).

Configuration and Setup

TBD: This is mostly about ensuring the jar exists on the remote system and having the appropriate security setup and configuration needed for creating a session.

Replacing the Open Client for .NET

FWD does not have a replacement for the Open Client for .NET, but such a replacement is possible. See #3326.

Java REST API

FWD supports REST services written directly in Java, with no runtime constraints related to the legacy converted application. Although these will be deployed in the same FWD server, they will be mapped to the same basepath as the legacy REST services.

These Java REST services can be written as any Java method, instance or static. For both cases, both the declaring class and the methods must be public; for instance methods, the declaring class must be non-abstract and must have a no-argument constructor (implicit or explicit) - an instance of this class will be created for each invocation of a REST service mapped to an instance method.

Once a Java class has defined REST services, the cfg/name_map_merge.xml must register it, like this:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<name-map>
  <rest-service jname="JavaRestTest" /> 
  <rest-service jname="SimpleRestTest" /> 
</name-map>

This file must be configured in cfg/p2j.cfg.xml, using this node:
<cfg>
   <global>
      ...
      <parameter name="name_map_merge"   value="${P2J_HOME}/cfg/name_map_merge.xml" />

Here, we have two classes - JavaRestTest and SimpleRestTest, which reside in the converted application's base package (i.e. com.goldencode.p2j.testcases, pkgroot configuration in p2j.cfg.xml or directory). You must specify the:
  • jname - the Java class name, a relative package to the converted application's base package. Full packages are not supported.
The Java class will have a LegacyService annotation, with these settings:
Annotation Attribute Required Data Type Description
type = "REST" yes String Mark this class as a REST service.
executionMode = "java" yes String Mark this as a REST service implemented in Java.
address no String A common prefix for all services defined in this class.
produces yes String The content-type for the response, usually "application/json".
consumes yes String The content-type for the request, usually @"application/json".
Each Java method which exposes a REST service must be annotated with a LegacyService annotation; some settings, if not specified, will be inherited from the defining class LegacyService annotation:
Annotation Attribute Required Data Type Description
type = "REST" yes String Marks this method as a REST service.
address no String If not specified, is inherited from the defining class LegacyService annotation.
produces no String If not specified, are inherited from the defining class LegacyService annotation.
consumes no String If not specified, are inherited from the defining class LegacyService annotation.
name yes String A description of this service.
path yes String The path of this service, relative to the path resolved after combining the REST service basepath and the address specified at the annotation.
verb yes String The HTTP method associated with this service.
parameters array no The list of the service parameters, each a LegacyServiceParameter instance
Each LegacyServiceParameter can have:
Annotation Attribute Required Data Type Description
ordinal yes int The 1-based index of this argument in the method's signature. Must be set to 0 for a parameter mapping to the method's return value.
returnValue = true no boolean Mandatory for the argument which is mapped to the method's return value. This argument will have ordinal = 0.
extent no int The array's length, only if the parameter is a Java array.
source Required if input = true. String The source of this argument, as it will be parsed from the request, for input arguments.
target Required if output = true. String The target of this argument, as it will be serialized to the response, for output arguments.
input Required if output is not set or set to false. boolean Flag indicating this argument will be populated from the request.
output Required if input is not set or set to false. boolean Flag indicating this argument will be written to the response. For an output argument to be written to the response, it must be mutable. Any change performed to a mutable output argument will be reflected in the response. If the parameter's reference gets changed, the response will not be able to be aware of these changes, and the original argument (if the argument was also input) will be written to the response. If both input = true and output = true are specified at the annotation, then this argument is in INPUT-OUTPUT mode.
The source and target follow a specific format. input arguments must always have the source specified, which can be:
Name Parameter Data Type Description
rest.verb String The argument will be parsed from the HTTP method. The parameter must be a string.
http.body Any The argument will be parsed from the request body. The parameter can be of any type.
http.headers String The argument will be initialized with all the HTTP headers at the request. The parameter must be a string.
http.uristring String The argument will be initialized with the URI string. The parameter must be a string.
http.header['<name>'] String The argument will be initialized the header <name>.
rest.queryparam['<name>'] Any The argument will be initialized with the query parameter <name>.
rest.formparam['<name>'] Any The argument will be initialized with the form parameter <name>.
rest.cookieparam['<name>'] Any The argument will be initialized with the cookie <name>.
rest.pathparam['/this/is/the/api/path/{<name>};<name>'] Any The argument will be initialized from a API path parameter.
json.object['request'].['<name>'] Any The argument will be initialized with the JSON value from request.<name>. Note that all request JSON payloads (used for mapping the arguments) have this format:
{
   "request" : 
   {
      "param1" : "value1",
      "param2" : 1234,
      "param3" : 2022-03-22
   }
}

In other words, there is always a root request node, under which the parameters are mapped. This constraint does not exist if the parameter is mapped to the http.body.
The target must be specified always for output parameters, and it can be:
Name Parameter Data Type Description
http.body Any The parameter will be written to the HTTP response body.
http.statuscode int The parameter will be set as the HTTP Status Code.
http.header['<name>'] Any The string representation of the parameter will be set as header <name>.
rest.cookieparam['<name>'] Any The string representation of the parameter will be set as cookie <name>.
json.object['response'].['<name>'] Any The parameter will be set to the JSON response payload, using the <name> key. Similar for the request, the response payload will have all output parameters rooted to a response node, like:
{
   "response" : 
   {
      "param1" : "value1",
      "param2" : 1234,
      "param3" : 2022-03-22
   }
}
The parameters will be parsed and serialized depending on their type. FWD supports these types by default, parsing and serializing them in the following way:
  • Java native types, should be used only for input. If the parameter does not have a corresponding argument, it will be initialized to the native type's default value. The serialization will be done considering their JSON corresponding type.
  • java.math.BigInteger, java.math.BigDecimal, java.lang.String and the java.lang counterparts for the Java native types: these instances are immutable, so they should be used only for input. By default, they will be initialized to null, if there is no argument corresponding for this parameter.
  • Java collections are supported, with explicit serializers added for Deque, SortedSet, Queue and Set. The serializer will be chosen based on the closest match: if you have a SortedSet instance, then the SortedSet serializer will be used. If there is no match at all, it will default to a standard Collection serializer. Map and SortedMap also have explicit serializers in FWD. If the parameter's type is using generics, then the elements in that collection will be serialized using that type; otherwise, the serialization will be done using each element's type, as it exists in the collection.

Following is a comprehensive list of the supported types (or their sub-types):

Data type Input Output Initial Value Serialization details
boolean yes no false JSON boolean type
byte yes no 0 JSON number type
short yes no 0 JSON number type
int yes no 0 JSON number type
long yes no 0 JSON number type
float yes no 0.0 JSON number type
double yes no 0.0 JSON number type
char yes no "\0" JSON text
java.lang.Boolean yes no null JSON boolean type
java.lang.Byte yes no null JSON number type
java.lang.Short yes no null JSON number type
java.lang.Integer yes no null JSON number type
java.lang.Long yes no null JSON number type
java.lang.Float yes no null JSON number type
java.lang.Double yes no null JSON number type
java.lang.Character yes no null JSON text
java.math.BigInteger yes no null JSON number type
java.math.BigDecimal yes no null JSON number type
java.lang.String yes no null JSON text
java.util.Date yes yes Current date JSON text, ISO-8601 format
java.util.Calendar yes yes Current date JSON text, ISO-8601 format
java.lang.Object no yes null Serialization depends on the concrete type. Can be used as output only if the concrete type is mutable (and the reference isn't changed). Can be used for collection's or Java array's element type.
Java arrays yes yes An array with the length specified at the annotation JSON array, each element serialized according to element's type. Can't be resized. For output, it is mandatory to specify its length, via the extent setting. Its component can be of any supported type, which is documented in this section (so it can be parsed and serialized by FWD).
byte[] yes yes null JSON text, base64-encoded byte array
java.util.Deque yes yes java.util.ArrayDeque JSON array
java.util.SortedSet yes yes java.util.TreeSet JSON array
java.util.Queue yes yes java.util.ArrayDeque JSON array
java.util.Set yes yes java.util.HashSet JSON array
java.util.Map yes yes java.util.HashMap JSON object
java.util.SortedMap yes yes java.util.TreeMap JSON object
java.util.Collection yes yes java.util.ArrayList JSON array
com.goldencode.p2j.rest.serializers.PojoType yes yes of parameter's type JSON object. Any POJO type must be marked by implementing the com.goldencode.p2j.rest.serializers.PojoType interface. The serialization format follows a JSON object with keys having the Java field's name and as values the actual field value. All fields which need to be part of the JSON request or response must have a getter and setter defined, and not be transient.
com.goldencode.p2j.persist.DataModelObject yes yes of parameter's type JSON object. Legacy records can be either read or returned if a parameter is defined as the DMO interface for that table. They are serialized as an JSON object, having as keys the legacy field name - keep in mind that the keys are case-sensitive. These DMOs can be used in collections and Java arrays, too.
character yes yes unknown JSON text
longchar yes yes unknown JSON text
clob yes yes unknown JSON text
date yes yes unknown JSON text, ISO-8601 format
datetime yes yes unknown JSON text, ISO-8601 format
datetime-tz yes yes unknown JSON text, ISO-8601 format
logical yes yes unknown JSON boolean
decimal yes yes unknown JSON number
integer yes yes unknown JSON number
int64 yes yes unknown JSON number
blob yes yes unknown base-64 encoded byte array
raw yes yes unknown base-64 encoded byte array
recid yes yes unknown JSON number
rowid yes yes unknown JSON text
comhandle n/a n/a n/a unsupported
object n/a n/a n/a unsupported
jobject n/a n/a n/a unsupported
handle n/a n/a n/a unsupported
table n/a n/a n/a unsupported
table-handle n/a n/a n/a unsupported
dataset n/a n/a n/a unsupported
dataset-handle n/a n/a n/a unsupported
memptr n/a n/a n/a unsupported
Custom serializers can be defined, and:
  • they all must be placed in the same package, the one defined in the rest/custom-java-serializers node in directory.xml.
  • must extend the com.goldencode.p2j.rest.serializers.JavaTypeSerializer class and have this structure:
    public class FooSerializer
    extends JavaTypeSerializer<Foo>
    {
       public FooSerializer()
       {
          super(Foo.class);
       }
    
       @Override
       public Foo initialize(Class<? extends Foo> definitionType) 
       throws InstantiationException, 
              IllegalAccessException
       {
          Foo foo; 
          // if the type is mutable, create an initial instance for it; otherwise, return null.
          return foo;
       }
    
       @Override
       public Foo parse(Foo arg, String sval) 
       throws RequestArgumentError
       {
          // explicit parsing of this type; the instance returned by the 'parse' method will be passed as the argument for the method call.
          return arg;
       }
    
       @Override
       public JsonNode toJson(Foo val)
       {
          // explicit serialization of this type
          return res;
       }
    }
    

    where:
  • Foo is the type for which a custom serializer is added
  • initialize is a method used to create an initial instance for mutable types. The following APIs can be used:
    • if this is a DMO interface, then use JavaTypeSerializer.newRecord to create a new instance of this record.
    • JavaTypeSerializer.newInstance(Class, to create a new instance of type T, or if the type is abstract, use the supplier function to create one.
  • parse(T arg, String sval), used to parse the JSON string representation (in sval) and initialize the argument. The arg parameter is the one returned by initialize, but is not mandatory to return it - another instance can be returned. There are helper APIs in JavaTypeSerializer, to allow you to:
    • load the JSON object in a map, via readMap. The key and value's serializers can be specified, but they are not mandatory.
    • load the JSON array in a Java list, via readArray. The array's component serializer can be specified, but it is not mandatory.
  • JsonNode toJson(T val), used to serialize a certain instance to JSON. JavaTypeSerializer provides helper APIs to:
    • toJsonArray(Collection c, JavaTypeSerializer serializer), serialize the Java collection as a JSON array.
    • toJsonObject(Map map, JavaTypeSerializer pkey, JavaTypeSerializer pval), serialize the map as a JSON object.

No more than one serializer can be defined for a certain exact type. If there are multiple serializers matching a certain type, the closest serializer in the type's hierarchy will be used.

Java types

Any Java types (native or Object) can be used:
  • as a standalone INPUT parameter (OUTPUT or INPUT-OUTPUT is not supported, as these require the instance to be mutable)
  • as a collection's or array's element, or a POJO property.
  • method's return.

Following example shows how these can be used as standalone INPUT parameters; the parameter's are read from the request JSON, and are returned in a map, as a JSON object:

Service Definition
   @LegacyService(type = "REST", name = "test java types", path = "/javaTypes", verb = "POST", parameters = 
   {
      @LegacyServiceParameter(ordinal = 0, target = "json.object['response'].['returnValue']  ", returnValue = true),
      @LegacyServiceParameter(ordinal = 1, source = "json.object['request'].['p1']", input = true),
      @LegacyServiceParameter(ordinal = 2, source = "json.object['request'].['p2']", input = true),
      @LegacyServiceParameter(ordinal = 3, source = "json.object['request'].['p3']", input = true),
      @LegacyServiceParameter(ordinal = 4, source = "json.object['request'].['p4']", input = true),
      @LegacyServiceParameter(ordinal = 5, source = "json.object['request'].['p5']", input = true),
      @LegacyServiceParameter(ordinal = 6, source = "json.object['request'].['p6']", input = true),
      @LegacyServiceParameter(ordinal = 7, source = "json.object['request'].['p7']", input = true),
      @LegacyServiceParameter(ordinal = 8, source = "json.object['request'].['p8']", input = true),
      @LegacyServiceParameter(ordinal = 9, source = "json.object['request'].['p9']", input = true),
      @LegacyServiceParameter(ordinal = 10, source = "json.object['request'].['p10']", input = true),
      @LegacyServiceParameter(ordinal = 11, source = "json.object['request'].['p11']", input = true),
      @LegacyServiceParameter(ordinal = 12, source = "json.object['request'].['p12']", input = true),
      @LegacyServiceParameter(ordinal = 13, source = "json.object['request'].['p13']", input = true),
      @LegacyServiceParameter(ordinal = 14, source = "json.object['request'].['p14']", input = true),
      @LegacyServiceParameter(ordinal = 15, source = "json.object['request'].['p15']", input = true),
      @LegacyServiceParameter(ordinal = 16, source = "json.object['request'].['p16']", input = true),
      @LegacyServiceParameter(ordinal = 17, source = "json.object['request'].['p17']", input = true),
      @LegacyServiceParameter(ordinal = 18, source = "json.object['request'].['p18']", input = true),
      @LegacyServiceParameter(ordinal = 19, source = "json.object['request'].['p19']", input = true),
      @LegacyServiceParameter(ordinal = 20, source = "json.object['request'].['p20']", input = true),
      @LegacyServiceParameter(ordinal = 21, source = "json.object['request'].['p21']", input = true),
      @LegacyServiceParameter(ordinal = 22, source = "json.object['request'].['p22']", input = true),
   })
   public Map<String, Object> javaTypes(BigDecimal p1,
                                        BigInteger p2,
                                        Boolean    p3,
                                        boolean    p4,
                                        byte[]     p5,
                                        byte       p6,
                                        Byte       p7,
                                        Calendar   p8,
                                        Character  p9,
                                        char       p10,
                                        Date       p11,
                                        double     p12,
                                        Double     p13,
                                        float      p14,
                                        Float      p15,
                                        Integer    p16,
                                        int        p17,
                                        long       p18,
                                        Long       p19,
                                        short      p20,
                                        Short      p21,
                                        String     p22)
   {
      Map<String, Object> res = new LinkedHashMap<>();
      res.put("p1", p1);
      res.put("p2", p2);
      res.put("p3", p3);
      res.put("p4", p4);
      res.put("p5", p5);
      res.put("p6", p6);
      res.put("p7", p7);
      res.put("p8", p8);
      res.put("p9", p9);
      res.put("p10", p10);
      res.put("p11", p11);
      res.put("p12", p12);
      res.put("p13", p13);
      res.put("p14", p14);
      res.put("p15", p15);
      res.put("p16", p16);
      res.put("p17", p17);
      res.put("p18", p18);
      res.put("p19", p19);
      res.put("p20", p20);
      res.put("p21", p21);
      res.put("p22", p22);
      return res;
   }

Request
{
    "request":
    {
        "p1": 12345.6789,
        "p2": 1234567890,
        "p3": null,
        "p4": true,
        "p5": "enhjdmJubQ==",
        "p6": 1,
        "p7": 125,
        "p8": "2022-04-20T12:57:48.323+03:00",
        "p9": "Z",
        "p10": "A",
        "p11": "2022-12-25",
        "p12": 2,
        "p13": 12345.6789,
        "p14": 3.3,
        "p15": 123.456,
        "p16": 123,
        "p18": 123456,
        "p19": null,
        "p20": 12,
        "p21": null,
        "p22": "abcdefgh" 
    }
}

Response
{
    "response": 
    {
        "returnValue": 
        {
            "p1": 12345.6789,
            "p2": 1234567890,
            "p3": null,
            "p4": true,
            "p5": "enhjdmJubQ==",
            "p6": 1,
            "p7": 125,
            "p8": "2022-04-20T10:57:48.323+02:00",
            "p9": "Z",
            "p10": "A",
            "p11": "2022-12-25",
            "p12": 2.0,
            "p13": 12345.6789,
            "p14": 3.3,
            "p15": 123.456,
            "p16": 123,
            "p17": 0,
            "p18": 123456,
            "p19": null,
            "p20": 12,
            "p21": null,
            "p22": "abcdefgh" 
        }
    }
}

Legacy 4GL types

The legacy 4GL types can be used the same way as Java native types. As they are mutable, they can be used for OUTPUT and INPUT-OUTPUT parameters, too. In the example bellow, all parameters are received as INPUT-OUTPUT, and they are changed - this change is reflected back in the response.

Service Definition
   @LegacyService(type = "REST", name = "test legacy types", path = "/legacyTypes", verb = "POST", parameters = 
   {
      @LegacyServiceParameter(ordinal = 1, target = "json.object['response'].['p1']", source = "json.object['request'].['p1']", input = true, output = true),
      @LegacyServiceParameter(ordinal = 2, target = "json.object['response'].['p2']", source = "json.object['request'].['p2']", input = true, output = true),
      @LegacyServiceParameter(ordinal = 3, target = "json.object['response'].['p3']", source = "json.object['request'].['p3']", input = true, output = true),
      @LegacyServiceParameter(ordinal = 4, target = "json.object['response'].['p4']", source = "json.object['request'].['p4']", input = true, output = true),
      @LegacyServiceParameter(ordinal = 5, target = "json.object['response'].['p5']", source = "json.object['request'].['p5']", input = true, output = true),
      @LegacyServiceParameter(ordinal = 6, target = "json.object['response'].['p6']", source = "json.object['request'].['p6']", input = true, output = true),
      @LegacyServiceParameter(ordinal = 7, target = "json.object['response'].['p7']", source = "json.object['request'].['p7']", input = true, output = true),
      @LegacyServiceParameter(ordinal = 8, target = "json.object['response'].['p8']", source = "json.object['request'].['p8']", input = true, output = true),
      @LegacyServiceParameter(ordinal = 9, target = "json.object['response'].['p9']", source = "json.object['request'].['p9']", input = true, output = true),
      @LegacyServiceParameter(ordinal = 10, target = "json.object['response'].['p10']", source = "json.object['request'].['p10']", input = true, output = true),
      @LegacyServiceParameter(ordinal = 11, target = "json.object['response'].['p11']", source = "json.object['request'].['p11']", input = true, output = true),
      @LegacyServiceParameter(ordinal = 12, target = "json.object['response'].['p12']", source = "json.object['request'].['p12']", input = true, output = true),
      @LegacyServiceParameter(ordinal = 13, target = "json.object['response'].['p13']", source = "json.object['request'].['p13']", input = true, output = true),
      @LegacyServiceParameter(ordinal = 14, target = "json.object['response'].['p14']", source = "json.object['request'].['p14']", input = true, output = true),
   })
   public void legacyTypes(blob       p1,
                           clob       p2,
                           character  p3,
                           date       p4,
                           datetime   p5,
                           datetimetz p6,
                           decimal    p7,
                           integer    p8,
                           int64      p9,
                           logical    p10,
                           longchar   p11,
                           raw        p12,
                           recid      p13,
                           rowid      p14)
   {
      if (!p1.isUnknown())
      {
         int length = BinaryData.length(p1).intValue();
         byte[] p1bytes = new byte[length * 2];
         System.arraycopy(p1.asByteArray(0, length), 0, p1bytes, 0, length);
         System.arraycopy(p1.asByteArray(0, length), 0, p1bytes, length, length);
         p1.assign(new blob(p1bytes));
      }

      p2.assign(p2.getValue() + p2.getValue());
      p3.assign(p3.getValue() + p3.getValue());
      p4.increment();
      p5.increment();
      p6.increment();
      p7.increment();
      p8.increment();
      p9.increment();
      p10.assign(!p10.getValue());
      p11.assign(p11.getValue() + p11.getValue());

      if (!p12.isUnknown() && BinaryData.length(p12).intValue() > 0)
      {
         int length = BinaryData.length(p12).intValue();
         byte[] p12bytes = new byte[length * 2];
         System.arraycopy(p12.getByteArray(), 0, p12bytes, 0, length);
         System.arraycopy(p12.getByteArray(), 0, p12bytes, length, length);
         p12.assign(p12bytes);
      }

      p13.increment();
      p14.setUnknown();
   }

Request
{
    "request": 
    {
        "p1": "enhjdmJubQ==",
        "p2": "abc",
        "p3": "def",
        "p4": "2022-01-03",
        "p5": "2022-01-04T01:02:03.005",
        "p6": "2022-01-05T02:03:04.006+02:00",
        "p7": 12345.789,
        "p8": 123456789,
        "p9": 123456789,
        "p10": true,
        "p11": "zxc",
        "p12": "111222333",
        "p13": 1234,
        "p14": 5678
    }
}

Response
{
    "response": 
    {
        "p1": "enhjdmJubXp4Y3Zibm0=",
        "p2": "abcabc",
        "p3": "defdef",
        "p4": "2022-01-04",
        "p5": "2022-01-05T01:02:03.005",
        "p6": "2022-01-06T02:03:04.006+02:00",
        "p7": 12346.789,
        "p8": 123456790,
        "p9": 123456790,
        "p10": false,
        "p11": "zxczxc",
        "p12": "1112223311122233",
        "p13": 1235,
        "p14": null
    }
}

Java Arrays

Java arrays can be used as INPUT, OUTPUT or INPUT-OUTPUT parameters. The example bellow copies the input p1 argument to p2 array, after it multiplies all elements with 100. For the p3 argument, which is INPUT-OUTPUT, all its content is doubled.

The return is a 'object' array, so elements can be of different type, and the serialization is done by each element's type.

Service Definition
   @LegacyService(type = "REST", name = "test Java arrays", path = "/array", verb = "POST", parameters = 
   {
      @LegacyServiceParameter(ordinal = 0, target = "json.object['response'].['returnValue']", returnValue = true),
      @LegacyServiceParameter(ordinal = 1, source = "json.object['request'].['p1']", input = true, extent = 3),
      @LegacyServiceParameter(ordinal = 2, target = "json.object['response'].['p2']", output = true, extent = 3),
      @LegacyServiceParameter(ordinal = 3, target = "json.object['response'].['p3']", source = "json.object['request'].['p3']", input = true, output = true),
   })
   public Object[] arrays(int[] arr1, int[] arr2, String[] arr3)
   {
      for (int i = 0; i < arr1.length; i++)
      {
         arr1[i] *= 100;
      }

      System.arraycopy(arr1, 0, arr2, 0, arr1.length);

      for (int i = 0; i < arr3.length; i++)
      {
         arr3[i] += arr3[i];
      }

      Object[] ret = new Object[4];
      ret[0] = 1;
      ret[1] = "abc";
      ret[2] = new byte[] {1, 2, 3, 4};
      ret[3] = new date(12, 25, 2021);

      return ret;
   }


Request
{
    "request": 
    {
        "p1": [ 1, 2, 3 ],
        "p3": [ "a", "b", "c", "d"]
    }
}

Response
{
    "response": 
    {
        "returnValue": [ 1, "abc", "AQIDBA==", "2021-12-25" ],
        "p2": [ 100, 200, 300 ],
        "p3": [ "aa", "bb", "cc", "dd" ]
    }
}

Legacy records

Records of any converted legacy table (permanent or temporary) can be used as parameters, either standalone, in an array, collection, etc. The example bellow copies the record from argument p1 to argument p2, while it also alters argument p3, to show how all INPUT, OUTPUT, and INPUT-OUTPUT modes work.

Keep in mind that a record received as an argument to a REST API call, should not be attached to a database session or otherwise persisted; instead, use the FWD ORM framework to create open a transaction, create a new record, populate it from the argument and after that persist it as needed.

Service Definition
   @LegacyService(type = "REST", name = "test records", path = "/record", verb = "POST", parameters = 
   {
      @LegacyServiceParameter(ordinal = 1, source = "json.object['request'].['p1']", input = true),
      @LegacyServiceParameter(ordinal = 2, target = "json.object['response'].['p2']", output = true),
      @LegacyServiceParameter(ordinal = 3, target = "json.object['response'].['p3']", source = "json.object['request'].['p3']", input = true, output = true),
   })
   public void records(Item p1, Item p2, Item p3)
   {
      p2.setItemname(p1.getItemname());
      p2.setItemnum(p1.getItemnum());
      p2.setPrice(p1.getPrice());
      p2.setWeight(p1.getWeight());

      p3.setItemname(new character("this has changed"));
      p3.setPrice(new integer(12345));
   }

Request
{
    "request": 
    {
        "p1": 
        {
            "itemName": "copied to p2",
            "itemNum": 1,
            "price": 2,
            "weight": 3
        },
       "p3": 
       {
            "itemName": "this is new",
            "itemNum": 1234,
            "price": 9999,
            "weight": 12
        }
    }
}

Response
{
    "response": 
    {
        "p2": 
        {
            "itemName": "copied to p2",
            "itemNum": 1,
            "price": 2,
            "weight": 3
        },
        "p3": 
        {
            "itemName": "this has changed",
            "itemNum": 1234,
            "price": 12345,
            "weight": 12
        }
    }
}

POJOs

POJOs (Plain Old Java Objects) are classes which implement the com.goldencode.p2j.rest.serializers.PojoType interface and have a public getter and a setter for each non-transient field which needs to be serialized. In the example bellow, there is a Country POJO with this structure:

public class Country
implements PojoType
{
   private String name;

   private String code;

   private int idx = 0;

   private boolean visit = false;

   public String getName()
   {
      return name;
   }

   public String getCode()
   {
      return code;
   }

   public void setName(String name)
   {
      this.name = name;
   }

   public void setCode(String code)
   {
      this.code = code;
   }

   public int getIdx()
   {
      return idx;
   }

   public void setIdx(int idx)
   {
      this.idx = idx;
   }

   public boolean isVisit()
   {
      return visit;
   }

   public void setVisit(boolean visit)
   {
      this.visit = visit;
   }
}

and the REST service (as in the previous examples), copies parameter p1 to parameter p2, while altering parameter p3, to show the INPUT, OUTPUT and INPUT-OUTPUT modes:

Service Definition
   @LegacyService(type = "REST", name = "test POJOs", path = "/pojo", verb = "POST", parameters = 
   {
      @LegacyServiceParameter(ordinal = 1, source = "json.object['request'].['p1']", input = true),
      @LegacyServiceParameter(ordinal = 2, target = "json.object['response'].['p2']", output = true),
      @LegacyServiceParameter(ordinal = 3, target = "json.object['response'].['p3']", source = "json.object['request'].['p3']", input = true, output = true),
   })
   public void pojos(Country p1, Country p2, Country p3)
   {
      p2.setCode(p1.getCode());
      p2.setName(p1.getName());
      p2.setIdx(p1.getIdx());
      p2.setVisit(p1.isVisit());

      p3.setName("United States");
      p3.setVisit(true);
   }

Request
{
    "request": 
    {
       "p1": 
       {
            "code": "GB",
            "idx": 2,
            "name": "Great Britain",
            "visit": false
        },
        "p3": 
        {
            "code": "US",
            "idx": 1,
            "name": null,
            "visit": false
        }
    }
}

Response
{
    "response": 
    {
        "p2": 
        {
            "code": "GB",
            "idx": 2,
            "name": "Great Britain",
            "visit": false
        },
        "p3": {
            "code": "US",
            "idx": 1,
            "name": "United States",
            "visit": true
        }
    }
}

Java lists

Any Java collection can be used as a parameter. The serialization will always be done using JSON arrays, and each element will be serialized using its type. The example bellow copies argument p1 to argument p2, and alters p3:

Service Definition
   @LegacyService(type = "REST", name = "test lists", path = "/list", verb = "POST", parameters = 
   {
      @LegacyServiceParameter(ordinal = 1, source = "json.object['request'].['p1']", input = true),
      @LegacyServiceParameter(ordinal = 2, target = "json.object['response'].['p2']", output = true),
      @LegacyServiceParameter(ordinal = 3, target = "json.object['response'].['p3']", source = "json.object['request'].['p3']", input = true, output = true),
   })
   public void lists(List<Integer> p1, List<Integer> p2, List<Integer> p3)
   {
      p2.addAll(p1);

      if (!p3.isEmpty())
      {
         p3.remove(0);
      }
      p3.add(12345);
   }

Request
{
    "request": 
    {
        "p1": [ 1, 2, 3, 4 ],
       "p3": [ 9999, 8888 ]
    }
}

Response
{
    "response": 
    {
        "p2": [ 1, 2, 3, 4 ],
        "p3": [ 8888, 12345 ]
    }
}

Java maps

Any Java collection can be used as a parameter. The serialization will always be done using JSON objects, and each key and value will be serialized using its type. The example bellow copies argument p1 to argument p2, and alters p3:

Service Definition
   @LegacyService(type = "REST", name = "test maps", path = "/map", verb = "POST", parameters = 
   {
      @LegacyServiceParameter(ordinal = 1, source = "json.object['request'].['p1']", input = true),
      @LegacyServiceParameter(ordinal = 2, target = "json.object['response'].['p2']", output = true),
      @LegacyServiceParameter(ordinal = 3, target = "json.object['response'].['p3']", source = "json.object['request'].['p3']", input = true, output = true),
   })
   public void maps(LinkedHashMap p1, LinkedHashMap p2, LinkedHashMap p3)
   {
      p2.putAll(p1);

      if (!p3.isEmpty())
      {
         Iterator iter = p3.keySet().iterator();
         iter.next();
         iter.remove();
      }
      p3.put("something", "added");
   }


Request
{
    "request": 
    {
        "p1": 
        {
            "v1" : 1,
            "v2": 2
        },
        "p3": 
        {
            "this will be removed" : 999,
            "this remains": 1234
        }
    }
}

Response
{
    "response": 
    {
        "p2": 
        {
            "v1": 1,
            "v2": 2
        },
        "p3": 
        {
            "this remains": 1234,
            "something": "added" 
        }
    }
}

Java sets

Any Java set can be used as a parameter. The serialization will always be done using JSON arrays, and each element will be serialized using its type. The example bellow copies argument p1 to argument p2, and alters p3:

Service Definition
   @LegacyService(type = "REST", name = "test sets", path = "/set", verb = "POST", parameters = 
   {
         @LegacyServiceParameter(ordinal = 1, source = "json.object['request'].['p1']", input = true),
         @LegacyServiceParameter(ordinal = 2, target = "json.object['response'].['p2']", output = true),
         @LegacyServiceParameter(ordinal = 3, target = "json.object['response'].['p3']", source = "json.object['request'].['p3']", input = true, output = true),
   })
   public void sets(LinkedHashSet p1, LinkedHashSet p2, LinkedHashSet p3)
   {
      p2.addAll(p1);

      if (!p3.isEmpty())
      {
         Iterator iter = p3.iterator();
         iter.next();
         iter.remove();
      }
      p3.add("something added");
   }

Request
{
    "request": 
    {
       "p1": [ "v1", "v2", 1, 2, 3 ],
       "p3": [ "this will be removed", "this remains" ]
    }
}

Response
{
    "response": 
    {
        "p2": [ "v1", "v2", 1, 2, 3 ],
        "p3": [ "this remains", "something added" ]
    }
}

Securing the Web Services (REST, SOAP, WEBHANDLER)

All web services supported by FWD (REST, SOAP, WEBHANDLER) can be secured, to authenticate and execute the request using a certain FWD user account. This allows authenticating the API request to a certain FWD user, and after that authorizing the API request, so:
  • it will be allowed if the specified FWD user is permitted to access this API, and denied otherwise.
  • it will execute the target API implementation using that FWD user's context. Any other security plugins checking for access to a resource (like the FWD directory) will be done using that FWD user's ACLs. The limitation at this time is that the target API must not use any client-side features like memptr, file access, or any other I/O.

This authentication and authorization is not mandatory; to enable it, the following configuration changes must be done to the FWD directory:

1. Add the resource plugin

In the FWD directory, find the /security/config/resource-plugins node and check that it contains this plugin:

        <node class="strings" name="resource-plugins">
          <node-attribute name="values" value="com.goldencode.p2j.security.WebServiceResource"/>
        </node>

2. Enable authentication at each web service.

The authentication and authorization must be enabled for each web service (REST, SOAP, WEBHANDLER), individually. Find the web service node in the directory, add this section:
          <node class="container" name="authentication">
            <node class="boolean" name="enabled">
              <node-attribute name="value" value="TRUE"/>
            </node>
            <node class="string" name="login_path">
              <node-attribute name="value" value="/fwdlogin"/>
            </node>
            <node class="string" name="logout_path">
              <node-attribute name="value" value="/fwdlogout"/>
            </node>
            <node class="string" name="type">
              <node-attribute name="value" value="basic"/>
            </node>
            <node class="integer" name="timeout">
              <node-attribute name="value" value="0"/>
            </node>
          </node>

and configure the following:
  • enabled, to enable or disable the authentication for this web service. If the authentication node is not present at the web service node, then the authentication is disabled for that web service.
  • login_path and logout_path. The authentication can happen:
    • for each request. In this case, login_path and logout_path must not be specified, and it means each HTTP request will contain the authentication details (these depend on the authentication type being used). In this case, the timeout must be set to zero, as the FWD user's context will be destroyed after each call.
    • if the login_path is specified, then authentication will be done once, as the login response will return a token, in the FwdSessionId response header. This token (returned by login) will need to be specified to all further requests, using the FwdSessionId header (case sensitive) at the request. The logout_path (if not set) defaults to /fwdlogout, and requires the FwdSessionId header with the token, at the HTTP request, to perform the logout.
  • type, the authentication type. Currently, FWD supports only basic authentication mode.
  • timeout (in seconds), which represents the maximum lifetime of the token created by a login API call. After this timeout expires, the FWD context is destroyed automatically and the token can no longer be used for performing requests. If set to 0, the FWD context will live indefinitely, or until the explicit logout is performed.

If authentication fails, then the web service will return a 401/UNAUTHORIZED status code; if the authentication is OK but authorization fails, then FWD will return a 403/FORBIDDEN status code.

Both the login_path and logout_path are relative, to a basepath computed depending on the web service type:
  • for REST, this is the basepath and any address configured at the rest service annotation. So, if you have a /rest basepath and a /foo address for a REST service, and the login_path configured as /login, then the API request will need to use the /rest/foo/login to perform the login, and /rest/foo/logout for the logout call.
  • for WEBHANDLER, this is just the basepath, like /web - so you will have /web/login and /web/logout paths.
  • for SOAP, this is the basepath (like /ws) and the address for each port, configured in the wsdl:
      <wsdl:service name="fwdService">
        <wsdl:port binding="tns:fwdObj" name="fwdObj">
          <wsdl:documentation/>
          <soap:address location="/fwd"/>
        </wsdl:port>
      </wsdl>
    

    This results in /ws/fwd/login and /ws/fwd/logout paths.

3. Authentication types

FWD currently only supports the Basic Authentication mode. In this mode, the request will need to have a HTTP header named Authorization, and with its value following the Basic HHH format, where HHH is the base64-encoded version of the user:token string.

In basic mode, for authentication, you will need to provide the username of a FWD user account, and a password (token), which will be checked against the webServiceToken configured at that FWD user account. If this matches, then the user will be authenticated.

Each FWD user which can be authenticated via a web service request must have webServiceToken specified at its FWD user definition in the directory, like this:

            <node-attribute name="webServiceToken" value="some-random-string"/>

This will be compared exactly with the token set at Authorization header.

WARNING: any user account with webServiceToken configured is dedicated for web service requests - it will be prohibited to login into the application, even if a password is set for it.

4. Configuring permissions

When authentication is enabled, permissions must be added for that web service type. This is done using ACLs, in a similar way as for other FWD resources which require security configuration.

In the directory, there will be a acl/webservice node, where ACL instances can be defined, in this format:
  • resource-instance - the name of the resource.
  • rights - true to enable access, false to deny access.
  • subjects - the list of subjects (groups, accounts, etc) to which this ACL instance applies
The resource-instance is the most important. This can be either a regular expression (when reftype is false) or an exact match, and is case-sensitive. The structure for this instance name must follow this format:
  • for REST, the REST:HTTP-METHOD:/path/to/api format
  • for WEBHANDLER, the WEBHANDLER:HTTP-METHOD:/path/to/api format
  • for SOAP, the SOAP:HTTP-METHOD:/path/to/endpoint:namespace/binding/operation format
and:
  • REST, WEBHANDLER, SOAP represent the web service type, in uppercase
  • HTTP-METHOD represents the HTTP method (POST, GET, DELETE, PUT, etc), in uppercase
  • path/to/api is the API target, relative to the web service basepath and any other custom address (for REST), as described at the login_path and logout_path above.
  • path/to/endpoint for SOAP follows the same rules as above.
  • namespace/binding/operation represents the SOAP operation details.

An example of ACLs which deny access to any /simple/test POST target but allow to /simple/test2 and /simple/test3 is this:

        <node class="container" name="webservice">
          <node class="container" name="000100">
            <node class="resource" name="resource-instance">
              <node-attribute name="reference" value="REST:POST:/simple/test.*"/>
              <node-attribute name="reftype" value="FALSE"/>
            </node>
            <node class="webServiceRights" name="rights">
              <node-attribute name="allow" value="false"/>
            </node>
            <node class="strings" name="subjects">
              <node-attribute name="values" value="all_others"/>
            </node>
          </node>
          <node class="container" name="000200">
            <node class="resource" name="resource-instance">
              <node-attribute name="reference" value="REST:POST:/simple/test2"/>
              <node-attribute name="reftype" value="TRUE"/>
            </node>
            <node class="webServiceRights" name="rights">
              <node-attribute name="allow" value="true"/>
            </node>
            <node class="strings" name="subjects">
              <node-attribute name="values" value="test1"/>
            </node>
          </node>
          <node class="container" name="000300">
            <node class="resource" name="resource-instance">
              <node-attribute name="reference" value="REST:POST:/simple/test3"/>
              <node-attribute name="reftype" value="TRUE"/>
            </node>
            <node class="webServiceRights" name="rights">
              <node-attribute name="allow" value="true"/>
            </node>
            <node class="strings" name="subjects">
              <node-attribute name="values" value="test1"/>
            </node>
          </node>
        </node>

Java Remote Object API

Introduction

FWD provides a remote object protocol that allows the development and deployment of a distributed system using simple Java classes. From a programming perspective, the FWD class that provides the remote object facility is com.goldencode.p2j.net.RemoteObject. The RemoteObject protocol allows a developer to code a Java interface that is exported as an application programming interface (API) on one or more systems and which is accessed remotely from another system. The services that are exported and the calling code that accesses those services are just “plain old Java obejcts” (POJOs).

At the core of this approach is a Java interface that represents the API to be provided by the FWD server. On the FWD server, an implementation of that interface is registered as an external API. On the remote side (the client), a network session is established with the FWD server. Once authenticated, the client obtains a local object with the same interface that is the API. That local object is a proxy, such that any calls to the local object are transparently redirected to the FWD server where they are routed to the registered implementation code. Any parameters of the method that is called are included with that method call and will be delivered to the server code. The server code executes and provides any return value or throws an exception, either of which are transparently returned back to the remote client. This natural approach makes it extremely simple to program servers and clients. No complicated network logic is needed, the developer can focus exclusively on the functionality of the services being provided.

Using the FWD remote object approach, anything you can do with a regular Java object is naturally available via this mechanism. The only limitation is that anything sent as a parameter or returned as a return value MUST be Serializable. Even exceptions are properly forwarded as needed, so the entire design is really just a remote Java object API.

Hosting Custom Remote Object Services

Design the API

Each exported API is defined as a Java interface. The methods included in the interface are the methods that will be callable via the FWD remote object protocol.

The more important limitation is that all method parameters and all return values must be of a type that is Serializable.

Example:

package com.acme.corp.something;

public interface SomeQueue
{
   public void clear();

   public boolean isEmpty();

   public int getSize();

   public SomeEvent next();

   public SomeEvent peek();

   public SomeEvent peek(int index);

   public SomeEvent[] filter(SomeEventCriteria[] criteria, boolean direction)
   throws SomeCheckedException;
}

It is valid to have void return values and an empty parameters list for any method. All of the primitive Java types (e.g. int, long, float, double, boolean...) are Serializable and thus they can be used freely as parameters and return types.

Many Java object types are Serializable (e.g. java.lang.Integer or java.util.ArrayList), so they too can be used freely. Any custom classes (SomeEvent or SomeEventCriteria in the example above) that are used as parameters or return types must be Serializable. See below for some tips on optimizing performance.

There is no specific limit to the number of parameters, primitives and Serializable objects of any type can be intermixed as needed.

Array types can be used as parameters and as a return type.

Exceptions (including checked exceptions) can be specified and thrown. Unchecked exceptions don't have to be listed, but checked exceptions do need to be specified as part of the method signature. Only instances descended from java.lang.Exception are forwarded over the remote object protocol. The Exception class is Serializable so any custom exceptions will naturally work with the protocol.

Method overloading is supported, but the method parameter lists must be different. The return type is not considered in matching the proper method signature.

Always remember that the code that implements the API is always in a separate JVM process if not also on a separate machine. When a method is called, any object instances passed as parameters will be delivered to the implementing method as a copy of that instance. That is how serialization works: it renders the instance into a form that can be transmitted over the network, transfers it and uses that data to create a new instance of that same object on the remote side, then passes that new instance to the method implementation. On return, the same behavior occurs for the return value. The caller receives a copy of the return value instance from the server side, it doesn't receive the same instance. This means that any given instance passed as parameter or return value is not shared between the caller and service, the two sides instead each have a copy of the instance that was sent by the other side. This means that subsequent changes to the state of an instance won't be visible on the other side of the session. For example, calling a setter of the class passed as a parameter only changes that instance (on the server side), it does not change the state of the caller's original instance. If changed state is needed on both sides, the API methods must return the changed instance or include that changed data as part of the data in some larger aggregating class which is the return value.

The process of serialization can be expensive in terms of performance. For complex objects (which may contain references to many other complex objects), leaving the serialization to the JVM is usually a bad idea. For simple data, use primitives instead of the Java wrapper classes (e.g. use int instead of Integer) wherever possible. The primitives serialize faster than the wrappers. Likewise, for performance reasons, use arrays instead of Java collections as parameters or return values.

Implementing Externalizable in custom data objects (which are transferred as parameters or return values) is a way to improve performance. Externalizable is a sub-interface of Serializable, which means that the result meets the requirements of the remote protocol. The difference is that with Externalizable must create methods to read and write instances of the class to a stream. In effect, the developer must manually write the code to serialize (writeExternal()) and deserialize (readExternal()) the instance. Many of the techniques described above can be used to render the instance data much more efficiently than would occur by default by the JVM's serialization processing. For large objects or even for small objects that are sent frequently, this can make a measurable difference in performance of the remote object protocol.

Implement the Server Code

The server side will be in one of two forms: a traditional Java class that implements the interface of the hosted service OR a Java class that has a exactly matching static method for each of the methods in the interface.

The first approach means that the hosted service is provided by a single non-static instance which can contain instance data just as any other object instance. That single instance is used for all method calls, no matter how many simultaneous sessions are making calls to the service. The following is an example:

package com.acme.corp.something.server;

import com.acme.corp.something.*;
import java.util.*;

public class ServerQueue
implements SomeQueue
{
   private Deque queue = new ArrayDeque<SomeEvent>();

   public synchronized void clear()
   {
      queue.clear();
   }

...

}

This example uses instance data and synchronizes access to it to ensure data integrity when accessed concurrently by multiple threads. The example is abbreviated (using ellipses) but in practice the entire interface must be implemented as is normal in Java.

The second approach allows a static “implementation” of the interface. There is no Java language feature that allows an interface to be implemented using static methods. FWD mimics the behavior that would occur if it was possible to implement an interface using static methods. The code must exactly duplicate all methods in the interface with the only difference being that each method is implemented using the static keyword.

Example:

package com.acme.corp.something.server;

import com.acme.corp.something.*;
import java.util.*;

public class ServerQueue
{
   private static Deque queue = new ArrayDeque<SomeEvent>();

   public static synchronized void clear()
   {
      queue.clear();
   }

...

}

In this case the data must be static so that the methods can access the data. There will not be any instances of the class created. Instead the hosted service will be called on a static basis, directly on the class itself. Class-level synchronization is used in this case to ensure data integrity.

Neither implementation approach requires the use of Serializable for the class that implements the hosted service itself since that class is not ever sent over the protocol. But any data that is received or sent, must be Serializable as is noted in the section above entitled Design the API.

See the chapter on Integrating Hand-Written Java Code for details on how to write code on the server-side. If it is necessary to call converted 4GL code from the hosted service, examine the section entitled Progress-Compatible Top-Level Session Wrapper.

If the hosted service will be accessed by multiple simultaneous clients, this means that there may be a need to store session-specific state. For details on how to safely store and access session-specific state, see the section on Context-Local Data in the chapter on Runtime Hooks and Plug-Ins. If there is only ever a single FWD session accessing the API, then this may seem less important, but it is not possible through configuration to ensure that this assumption is always honored. It is unsafe to store state across API calls unless specific precautions are taken to protect the data and ensure its integrity. All access to any shared data must be synchronized or otherwise protected with a locking mechanism to ensure that only one thread will ever have access to reading or writing the data at any given time. By making the access mutually exclusive, it is ensured that the data's integrity will be maintained. By using non-shared data (session-specific state such as is documented in the Context-Local Data section), this issue is eliminated since each session has an independent version of the data.

If the API implementation is completely stateless, then there is no data to store and the session-specific storage of state is not needed.

Modify the FWD Server Classpath

The resulting interface, implementation class and all other supporting classes must be accessible in the FWD server's classpath. If the classes are in a jar file, that jar must be explicitly listed in the classpath. If the class files are “loose", then the root path to the top-most package must be included in the classpath.

The FWD server must be able to load all of the needed classes from the file system or a jar in the file system.

Register the API at FWD Server Startup

Once the interface and implementation classes are written (and compiled) and are available to be loaded in the FWD server, the FWD server must be configured to export that API when when the server starts. This exporting process is known as registration.

The following code must be executed to register the API when the implementation class is non-static (when the interface is implemented directly):

import com.acme.corp.something.*;
import com.acme.corp.something.server.*;
import com.goldencode.p2j.net.*;

...

RemoteObject.registerNetworkServer(SomeQueue.class, new ServerQueue());

In the case where the static “implementation” has been used, the following code is used to register the API:

import com.acme.corp.something.*;
import com.acme.corp.something.server.*;
import com.goldencode.p2j.net.*;

...

Class<?>[] ifaces = new Class[] { SomeQueue.class };
RemoteObject.registerStaticNetworkServer(ifaces, ServerQueue.class);

There is a startup hook available that allows custom logic to run when the FWD server is starting. That is the optimal location to place any registration calls. That ensures that the API is loaded before any clients can connect to the server. For details, please see the section entitled Server Initialization and Termination Hooks in the Runtime Hooks and Plug-Ins chapter.

Creating a Process Account and Credentials

The external application is represented on the FWD server as a process account. The external application must have exclusive and private access to a unique private encryption key and a signed certificate that are stored in a key-store file. That key store will be encrypted and password protected, but the contents of that key-store allow the external application to authenticate with the FWD server. The code to access the FWD server references the key-store in the bootstrap configuration, providing the FWD session establishment code with the proper aliases and passwords to decrypt and use the contents of the key-store file. This certificate is sent to the server as the external application's credentials.

To validate that the FWD server is the expected server, the external application uses a trust-store. The trust-store provides public certificates for the known FWD servers. The code to access the FWD server references the trust-store in the bootstrap configuration, providing the FWD session establishment code with the proper alias that represents the server's certificate. If that certificate matches the server's certificate as sent during the SSL handshaking process, then the connection is allowed.

The important thing here is to ensure that the external application has access to its specific key-store and to the trust-store for the server (which is not a private file). For details on how to create these files, please see the chapter entitled Key-Stores and Trust-Stores in the FWD Runtime Installation, Configuration and Administration Guide.

The certificate for the external application must be created as a “Peer Certificate” in the FWD server's configuration. This adds the certificate to the list of known certificates that can be associated with an account. For details on how to create the peer certificate on the server, please see the chapter entitled Creating a Peer Certificate in the FWD Runtime Installation, Configuration and Administration Guide.

On the FWD server, a valid process account is required to exist. For details on how to create the process account on the server, please see the chapter entitled Creating a Process Account in the FWD Runtime Installation, Configuration and Administration Guide. As part of that task, the process account must be associated with the peer certificate previously created. That ensures that when the external application sends the certificate as its credentials, that the FWD server will match up the proper process account and allow access. This process account MUST be in the list of subjects supplied in the net ACL for the exported API. See the section below entitled Modify the FWD Server Security Configuration.

Once these tasks are complete, the external application should be able to successfully access the server.

Modify the FWD Server Security Configuration

After the code is written and the API exported, that API will not be accessible unless the FWD server's security configuration allows access to the account(s) that need to use that API. By default, an exported API is not accessible to any accounts. To resolve this, at least one Access Control List (ACL) entry must be added to the FWD server's configuration.

There are two mechanisms to make the change. The first method is the graphical administrative user-interface (the “admin interface”) which is a Java based applet that runs in a browser. The second method is direct editing of the server's directory file (which is the backing configuration for the server).

For details, please see the section entitled Adding a NET Resource ACL in the book entitled FWD Runtime Installation, Configuration and Administration Guide.

The following information will be required:

  • The fully qualified package and class name of the interface being exported: com.acme.corp.something.SomeQueue in this example. This will become the name of the net resource that is being protected, otherwise known as the Resource Instance Name.
  • The list of accounts that must have access. This will become the Subjects list in the ACL.
  • The Rights needed will be read and execute.

The key to all of this is that the new ACL configures the net resource to allow a specific list of subjects a particular set of rights (permissions) for a specific set of exported APIs.

Don't make the mistake of using the implementing class name for the Resource Instance Name. In the example above, it would be an error to specify com.acme.corp.something.server.SomeServer since that is not the interface class name. Whatever class defines the interface you are exporting must be used both in the ACL as the Resource Instance Name and in the RemoteObject.obtainNetworkInstance() on the client (see the section below entitled Accessing Services. The client just gets a local proxy that represents the interface and which redirects any calls to the server. Only the server knows the specific class name of the implementation, the client has no idea what implementation class is used on the server. They rendezvous based on the interface name and its contained method signatures.

Debugging Tips

Problems with the mechanics of the remote object protocol can be debugged using logging at the FINEST level for the com.goldencode.p2j.net package. All exported APIs (each method in each exported interface) will be listed when it is exported. Every inbound and outbound method call via the protocol will have detailed log entries, including timing information. See the chapter entitled Logging Infrastructure for details on how to change the logging settings.

Security-related problems (ACLs, account authentication, security decision failures) can be debugged using logging that is specific to the com.goldencode.p2j.security.SecurityManager class. Set logging to DATA for the security manager, by adding the following section to the /security/config section of the FWD directory:

<node class="string" name="debug-level">
   <node-attribute name="value" value="DATA"/>
</node>

All logging output (for both the standard net package logging and the security manager logging) will appear in the server log.

Accessing the Remote Object API

Introduction

This section describes how an external application (one that runs outside of the FWD Application Server) can call a remote object API.

Establishing a Session with the FWD Server

External applications access the FWD server through the remote object protocol. Before that can be used, the external application must establish a session with the FWD server. This involves writing custom Java code to connect to the FWD server and then authenticate using a known account. Once complete, the session can be used to obtain access to the exported APIs of the FWD server.

An external application can establish multiple sessions to the same FWD server. These sessions can be either direct or virtual; although their usage is the same (when writing code to invoke a remote API, there is no difference on the type of session used), there are some differences in what and how many resources are used in each case. When using direct sessions, FWD establishes an unique queue for each created session. So, all traffic for that session will use its own queue (and its own opened socket). On the other hand, the virtual sessions use only one queue, and all communication is safely multiplexed over this queue.

When virtual sessions are used, there may be a bandwidth issue at some point depending on transaction volumes, since only a single socket will be used for all traffic and all communication is done using a single pair of reader/writer threads, on each end. The protocol works by sending messages which are made to look like API calls. Only one message can be sent on the socket at a time (in one direction). Once that message is sent, the next can be sent and so on, but they will be serialized. There are reading and writing sides to the socket and these operate independently. In addition, the processing is asynchronous between two requests, so one can start and another can start and either one can finish before the other. The only thing that is synchronous is that the start of an API call will always come before the response to that API call. But the API calls themselves will be independent, except they would share the transport.

On the other hand, when using direct sessions, each session will have its own queue and its own pair of reader/writer threads, on each side. This will provide more bandwidth and resources, as compared to the virtual sessions case. But, as the number of created direct sessions increases, the number of open sockets and thread count on the FWD server will increase; depending on the configuration of the machine on which the FWD server is running, you may reach the limit of possible opened sockets or starve other applications which need to use sockets.

To conclude, when deciding to use either virtual or direct session, you should take into account the amount of remote API requests executed by the external application and how fast the response is needed. If the external application needs to execute the remote APIs very frequently and the response needs to be received in real time, then direct sessions should be used. Else, using virtual sessions will suffice and unnecessary resources will not be occupied.

The following code shows how to connect (by creating a direct session) via SSL to the FWD server using an X.509 certificate to establish the external application's identity:

import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.net.*;
import com.goldencode.p2j.security.SecurityManager;

...

Session session = null;

try
{
   BootstrapConfig bc = new BootstrapConfig();

   bc.setServer(false);
   bc.setConfigItem("net", "server", "host", hostname);
   bc.setConfigItem("net", "server", "port", port);
   bc.setConfigItem("net", "connection", "secure", "true");
   bc.setConfigItem("net", "dispatcher", "threads", "2");
   bc.setConfigItem("security", "truststore", "filename", truststore);
   bc.setConfigItem("security", "truststore", "alias", alias);
   bc.setConfigItem("security", "certificate", "validate", "true");
   bc.setConfigItem("security", "keystore", "filename", keystore);
   bc.setConfigItem("security", "keystore", "processalias", processalias);
   bc.setConfigItem("access", "password", "truststore", tspasswd);
   bc.setConfigItem("access", "password", "keystore", kspasswd);
   bc.setConfigItem("access", "password", "keyentry", kepasswd);

   SecurityManager secMgr  = SecurityManager.createInstance(bc);
   SessionManager  sessMgr = SessionManagerFactory.createLeafNode(bc);
   session = sessMgr.connectDirect(null, null);

   if (session == null)
   {
      // Houston, we have a problem!
   }
   else
   {
      // Use the session here.
   }
}

catch (Exception excpt)
{
   // Unexpected failure, deal with it.
}

The BootstrapConfig class encodes the configuration of the FWD environment including how to contact the server. Once that configuration is setup, the SecurityManager and SessionManager are initialized. Then the connection is established (via LeafSessionManager.connectDirect()). Authentication occurs during connectDirect() and it is completely non-interactive.

In addition to the above code, the external application will need access to the p2j.jar so that the various FWD classes can be used. In addition a key-store and trust-store will be required. The key-store contains the private key/certificate for the identity of the external application. It is matched up with a specific FWD account by the FWD server. The trust-store contains the certificate for the FWD server and it allows the SSL handshake process to validate that the server is legitimate.

For details on how to create and configure a new process account (along with the creation of the key-store and trust-store files), please see the section below on Creating a Process Account and Credentials.

When using virtual sessions, the only difference is related to the code which creates the session. So, the following line in the above code:

session = sessMgr.connectDirect(null, null);

needs to be replaced with this one:

session = sessMgr.connectVirtual(bc, null);

At the time the sessMgr.connectVirtual() call ends (and no exceptions were thrown), a new virtual session will have been created. If this is the first attempt to connect to this FWD server, then the queue is started. Note that once a virtual session is established to a FWD server, all the subsequent virtual sessions will authenticate using the same credentials as for the first session. This is done automatically by FWD.

When connecting to a FWD server, the external application is known as a “leaf” node in FWD terms. From this, it is possible to create multiple direct or multiple virtual sessions to another router node. But, it is not possible to create direct and virtual sessions at the same time, to the same router node. So, you need to use either direct or virtual sessions; in case the client tries to create a virtual session once a direct one has been established or a direct session once a virtual session has been established, FWD will fail with an IllegalStateException exception. Note that once all sessions are disconnected, the next created session one can be either way - virtual or direct.

The limitation above is per each router node; one might decide to create multiple virtual sessions to a router and multiple direct sessions to another router node. As FWD manages the created sessions by router nodes, the only thing you need to consider is to be consistent - once a direct or virtual session is established to a router node, all the other sessions established to the same router node must be of the same type.

An important note is that the SecurityManager.createInstance and the SessionManagerFactory.createLeafNode must be called only once, before the creation of any session. When all created sessions are terminated, as there is no other active connection, these methods can be safely executed again; the result will be that the the FWD client state is reinitialized.

When virtual sessions are created, a BootstrapConfig instance is passed to the connectVirtual call each time a new session is needed; so, the configuration can point to the same router node or to a completely different one, with the limitation that all authentication is done using the credentials used when the first virtual connection was made to that router node. In the direct sessions case, if a session needs to be established to the same router node using different credentials or to another router node, then instead of using:

session = sessMgr.connectDirect(null, null);

use:

session = sessMgr.connectDirect(config, null, null);

where config is another instance of the BootstrapConfig class. If the config parameter is null, then the default configuration (used when the SessionManager and SecurityManager were initialized) will be used.

The code in the previous example shows how to establish the session just before executing the remote API, and each time the remote API is executed. This approach is time consuming, as each time a session is created the socket needs to be opened, the queue needs to be started, authentication must be done, etc. Instead we recommend using a pooling approach: sessions are opened and saved in a pool; when a remote API needs to be executed, a session is retrieved from the pool; the session is put back in the pool when all work with it is done. This way, a number of sessions are always opened and used as necessary; if the pool goes out of sessions, you can decide to wait until another session becomes available or to create another session (which will be added to the pool when the work is finished with it). Following is an example of how a session pool can be defined:

import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.net.*;
import com.goldencode.p2j.security.SecurityManager;

public class FWDSessionPool
{
   private static final int POOL_SIZE = 20;

   private static List<Session> pool = null;

   private static SessionManager sessMgr;

   public static Session getSession()
   {
      Session sess = null;
      while (sess == null)
      {
         synchronized (pool)
         {
            if (!pool.isEmpty())
            {
               // We have a session, check if it is valid
               sess = pool.get(0);
               if (sess.isRunning())
               {
                  // The session is still active, we can use it
                  break;
               }
               // If the session is not active, establish another one
               sess = createSession();
            }
         }

         // The pool is empty, wait for a while, maybe a session is released
         try
         {
            Thread.sleep(1000);
         }
         catch (InterruptedException e)
         {
            e.printStackTrace();
         }
      }

      return sess;
   }

   public static void freeSession(Session sess)
   {
      synchronized (pool)
      {
         // add the session back to the pool
         pool.add(sess);
      }
   }

   public static Session createSession()
   {
      Session sess = null;

      while (sess == null)
      {
         try
         {
            sess = sessMgr.connectDirect(null, null);
            break;
         }
         catch (Exception e)
         {
            // There was a problem when connecting, wait a while for the
            // remote server to get back up
            e.printStackTrace();
         }

         try
         {
            // wait a while before trying again
            Thread.sleep(1000);
         }
         catch (InterruptedException e)
         {
            e.printStackTrace();
         }
      }

      return sess;
   }

   public static void initPool()
   {
      if (pool != null)
      {
         // The pool was already initialized
         return;
      }

      try
      {
         BootstrapConfig bc = new BootstrapConfig();

         bc.setServer(false);
         bc.setConfigItem("net", "server", "host", hostname);
         bc.setConfigItem("net", "server", "port", port);
         bc.setConfigItem("net", "connection", "secure", "true");
         bc.setConfigItem("net", "dispatcher", "threads", "2");
         bc.setConfigItem("security", "truststore", "filename", truststore);
         bc.setConfigItem("security", "truststore", "alias", alias);
         bc.setConfigItem("security", "certificate", "validate", "true");
         bc.setConfigItem("security", "keystore", "filename", keystore);
         bc.setConfigItem("security", "keystore", "processalias", processalias);
         bc.setConfigItem("access", "password", "truststore", tspasswd);
         bc.setConfigItem("access", "password", "keystore", kspasswd);
         bc.setConfigItem("access", "password", "keyentry", kepasswd);

         SecurityManager secMgr  = SecurityManager.createInstance(bc);
         sessMgr = SessionManagerFactory.createLeafNode(bc);

         pool = new LinkedList<Session>();

         for (int i = 0; i < POOL_SIZE; i++)
         {
            Session session = createSession();
            pool.add(session);
         }
      }

      catch (Exception excpt)
      {
         // Unexpected failure, deal with it.
      }
   }
}

In this example, the initPool method is used to populate the pool with 20 direct sessions; the pool can be initiated either when your application is started or the first time the pool is used, in the class's static constructor. When a session is needed, the getSession method will be used to retrieve a session from the pool; when all work with the session is done, don't forget to add it back to the pool, using the freeSession method. The following construct can be useful when dealing with pools of objects:

Session sess = null;
try
{
   sess = FWDSessionPool.getSession();
   // invoke the remote APIs
}

finally
{
   // Add the session back to the pool
   FWDSessionPool.freeSession(sess);

}

In this example, if the remote FWD server goes down or there is a network problem, the next getSession call will block and try to establish a new session until the FWD server gets back online.

Another important note about the external application is that it can host multiple FWD nodes. When the SecurityManager and SessionManager are initialized, a single instance of each class is created and available to the external application. This is true in the default case, when each class is loaded by the same class loader, the one used to start the application (the primordial class loader). If multiple class loaders are used, one could initiate multiple instances of the security and session managers, each one being a different node; this node would be isolated within the context of the class loader used to load the manager's classes, allowing different singleton instances of the security and session managers to exist in the same VM.

There is no restriction on the type of node started within the context of a certain class loader. Depending on what the external application needs, different modules of it could initiate themselves as leaf nodes, while other modules could initiate as router nodes. This is useful because, even if leaf nodes can communicate with multiple routers (by passing a different bootstrap configuration instance at the create session method), a router node, once the security and session managers are initialized, can't be downgraded to a leaf node unless the managers are re-initialized. The idea is, using multiple class loaders, one may host in the same VM multiple nodes, of different types - routers or leaves.

Accessing Services

The external application must have access to both the p2j.jar file and to the jar or class files needed to access the application-specific interface. In both cases, the classpath must be modified and the jar files or class files must be available via that classpath.

Java's serialization is used by the FWD remote object protocol to turn object instances into a stream of bytes that can be sent over the network and converted back into an equivalent object instance on the other side. This is known as marshalling and unmarshalling a method's parameters. The use of serialization means that the version of the shared interface and all shared classes must be the same on both sides of the network connection. It is best to use the exact same jar file on both sides. To do otherwise will often cause problems.

Assuming a valid FWD session has been established, it is very easy to use that session to call the hosted service (the exported APIs) on the FWD server. The following is an example:

import com.acme.corp.something.*;
import com.goldencode.p2j.net.*;

...

// The FWD Session object must be working by this point.  This
// example assumes that it exists as an instance variable named
// “session”.

SomeQueue queue = (SomeQueue) RemoteObject.obtainNetworkInstance(SomeQueue.class, session);
// From here the SomeQueue instance is used, just as if it was local.
if (!queue.isEmpty())
{
   SomeEvent event = queue.peek(0);
   ...
}

The object returned by the RemoteObject.obtainNetworkInstance() method is a proxy. That means it transparently redirects method calls on the local instance to the FWD server where those are turned into method calls on the implementation instance or class. Parameters, return values and exceptions are moved over the network as expected, so long as all of the types are Serializable. The method calls are synchronous (the thread does not return until the called code returns or throws an exception), just as a local method call is synchronous. The remote object protocol hides the complexity of what is effectively a “remote procedure call” or RPC mechanism.

If any of the method calls can throw checked exceptions, then the Java compiler (usually javac) will force the calling code to handle those exceptions just as if they were being generated locally. All normal exception handling works and is made transparent via the remote object protocol.

The session can be shared by more than one thread. This means that 2 or more threads can each obtain a network instance (a proxy) and make concurrent and independent calls to the server. Each simultaneous thread's usage will be executed on different threads on the server but each of the threads will share the same security context. For this reason the use of context-local or shared data must be handled very carefully. Context-local data is not thread-specific, so access via multiple threads must be synchronized by the user.

In addition, if the same session is used to execute remote APIs concurrently, as the context-local data is not thread specific, these APIs will not be able to use the FWD's framework safely. The context-local data is not thread specific because FWD runtime was built to simulate the 4GL's single-threaded behavior; that is, at any time, only a single thread can access this context-local data. By using the same session to execute the remote APIs concurrently, access to the context-local data in the FWD runtime will not be “single threaded”, so errors and unexpected behavior may and will occur. If the remote APIs use any parts of the 4GL replacement APIs in the FWD runtime and the same session is used to execute them, then all API invocations will share the same context-local data and will concurrently update it; for example, the FWD runtime needs to keep context-local data for all frame processing, persistence processing and block/transaction processing.

A first example of possible unexpected behavior is the persistence framework. This framework can't be used safely (by remote APIs which get executed concurrently using the same session) because the undo data is kept by each session's context, so executing concurrent API calls which use the same table will generate errors, if you want to undo or rollback certain changes. More, if the session is assumed to only read records using FWD persistence runtime, it will not work either. This is because by using a single FWD session, there is a chance that the same Hibernate session will be used by two concurrent API calls, and Hibernate sessions are not thread safe. More, the Hibernate session might get closed by an API call while another API call uses it, thus preventing the second API call to complete its query. Beside these reasons, the FWD's Persistence class is context safe; but, as in many other places in FWD, the context-local data which is needed by this class is not thread safe.

Another example is the FWD's block processing - for each block, context-local data will need to be saved. But, as in the persistence case, this context-local data is not thread-safe; so, executing 4GL-type blocks using the same session concurrently, will not be thread safe and errors may occur.

The conclusion is that you don't have a safe way of using the same FWD session to execute multiple API calls in a thread-safe manner, considering that these API calls use the 4G: replacement APIs in the FWD runtime. If you use JDBC to access the data, and no 4GL behavior is needed, then you are outside the FWD runtime; so, the above issues do not apply.

Terminating a Session with the FWD Server

FWD sessions can and probably should be long-lived. There is a cost to the setup of a session which may not amortize well on a per-call or per-transaction basis. It is best to organize usage in longer sessions, optimally keeping the session open for the lifetime of the usage.

When all usage of a given FWD session is complete, the session is safely ended using Session.terminate() on the session instance that was obtained using connectDirect() or connectVirtual(). In the case of a catastrophic failure, the protocol will attempt to cleanup on both sides of the connection, but that is not a good practice.

For direct sessions, calling terminate() is the equivalent to logging off the server, disconnecting the socket and stopping the queue. For the virtual sessions case, the terminate() call will not disconnect the socket nor stop the queue, unless this is the last remaining virtual session connected to that server.

If a SessionListener implementation is specified at the connectDirect() or connectVirtual() call, then the SessionListener.terminate() will be executed just before the queue is stopped and just before the session is ended. During the SessionListener.terminate() call, execute only any additional needed cleanup - do not attempt to create another session, as this will have unexpected results on the SessionManager's state.

If the leaf node disconnects abnormally from the server (i.e. the network goes down), then the first components which notices this are the reader and the writer threads. As the socket is no longer active, it generates an exception which gets caught in the reader and writer threads associated with this socket and queue. This exception triggers the queue termination; in turn, the queue terminates all sessions associated with it, before terminating itself. If there was an active API call when the queue disconnected, then a SilentUnwindException will be generated. This is why it is recommended to bracket the API call with a try-catch clause and handle this exception.

try
{
   // invoke the remote APIs
}

catch (SilentUnwindException e)
{
   // the connection went down, API call was unsuccessful

}

On the FWD server side, this is noticed by the Reader and Writer threads too. The behavior is the same as on the leaf node - it terminates all sessions registered with that queue -, except for one difference: if there was an active API call before the leaf node disconnected or not. In the case the active API call uses the FWD runtime, then it will notice the leaf node termination at the next block iteration and will abort the processing; else, the Dispatcher thread will finish the API execution, but will drop the result.


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