Integrating External Applications¶
- 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.
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". |
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 |
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. |
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 . |
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 } } |
- 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 thejava.lang
counterparts for the Java native types: these instances are immutable, so they should be used only forinput
. By default, they will be initialized tonull
, if there is no argument corresponding for this parameter.- Java collections are supported, with explicit serializers added for
Deque
,SortedSet
,Queue
andSet
. The serializer will be chosen based on the closest match: if you have aSortedSet
instance, then theSortedSet
serializer will be used. If there is no match at all, it will default to a standardCollection
serializer.Map
andSortedMap
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 |
- they all must be placed in the same package, the one defined in the
rest/custom-java-serializers
node indirectory.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 addedinitialize
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 extends T, Supplier extends T>
, to create a new instance of type T, or if the type is abstract, use the supplier function to create one.
- if this is a DMO interface, then use
parse(T arg, String sval)
, used to parse the JSON string representation (insval
) and initialize the argument. Thearg
parameter is the one returned byinitialize
, but is not mandatory to return it - another instance can be returned. There are helper APIs inJavaTypeSerializer
, 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.
- load the JSON object in a map, via
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;
}
@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"
}
}
{
"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"
}
}
}
{
"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();
}
@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
}
}
{
"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
}
}
{
"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;
}
@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"]
}
}
{
"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" ]
}
}
{
"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));
}
@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
}
}
}
{
"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
}
}
}
{
"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);
}
@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
}
}
}
{
"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
}
}
}
{
"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);
}
@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 ]
}
}
{
"request":
{
"p1": [ 1, 2, 3, 4 ],
"p3": [ 9999, 8888 ]
}
}
Response
{
"response":
{
"p2": [ 1, 2, 3, 4 ],
"p3": [ 8888, 12345 ]
}
}
{
"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");
}
@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
}
}
}
{
"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"
}
}
}
{
"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");
}
@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" ]
}
}
{
"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" ]
}
}
{
"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 theauthentication
node is not present at the web service node, then the authentication is disabled for that web service.login_path
andlogout_path
. The authentication can happen:- for each request. In this case,
login_path
andlogout_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, thetimeout
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 theFwdSessionId
response header. This token (returned by login) will need to be specified to all further requests, using theFwdSessionId
header (case sensitive) at the request. Thelogout_path
(if not set) defaults to/fwdlogout
, and requires theFwdSessionId
header with the token, at the HTTP request, to perform the logout.
- for each request. In this case,
type
, the authentication type. Currently, FWD supports onlybasic
authentication mode.timeout
(in seconds), which represents the maximum lifetime of the token created by a login API call. After thistimeout
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 thelogin_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 thelogin_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 aacl/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
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
REST
,WEBHANDLER
,SOAP
represent the web service type, in uppercaseHTTP-METHOD
represents the HTTP method (POST, GET, DELETE, PUT, etc), in uppercasepath/to/api
is the API target, relative to the web service basepath and any other custom address (for REST), as described at thelogin_path
andlogout_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 theResource Instance Name
. - The list of accounts that must have access. This will become the
Subjects
list in the ACL. - The
Rights
needed will beread
andexecute
.
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.