Support for RESTful invocation of Business Task and Business Entity Methods
- 1 Introduction
- 2 Preparing Business Tasks and Entities for RESTful invocation of methods
- 2.1 Business Tasks
- 2.2 Business Entities
- 2.3 Invokable methods
- 3 Defining RESTful interfaces for invokable methods
- 3.1 Arguments of the @RestMethod annotation
- 3.2 Arguments of the @ParameterSchema annotation
- 3.3 Arguments of the @RestMethodParameter annotation
- 3.4 Arguments of the @ApiDoc annotation
- 3.5 Population of the Parameter object
- 3.6 Population of the JSON response of the request
- 3.7 Versioning invokable methods using the Accept header
- 3.8 Documenting RestMethodResponse and IRestMethodResponse return values
- 4 Examples
Introduction
The support for invoking Business Tasks and Business Entity Methods through the RESTful interface is based on the requirements described in this article: RESTful services. Particularly the RestEntitiesWebHandler and the RestResourceService (as a Service Instance of the IRestResourceService).
Business Task methods and Business Entity methods are exposed as RESTful resources using the same set of annotations - and actually share a lot of similarities. Business Entity invokable methods are typically PUBLIC VOID methods with a ProDataset INPUT-OUTPUT parameter and a (JSON serializable) parameter object. Business Task methods are typically PUBLIC VOID methods with zero to five ProDataset INPUT-OUTPUT parameters and a (JSON serializable) parameter object.
Support for return value of Business Task and Business Entity Methods
Also to facilitate the support for RESTful invocation of Business Task and Business Entity methods the Consultingwerk.OERA.ServiceInterface:InvokeTask API has been enhanced with support for returning the return value of a method to the caller. This finally allows developers to separate the request message from the response message.
The InvokeTask API is fully capable of invoking Business Entity methods as well.
Known limitations
Due to limitations of the ABL reflection API, we currently do not support ProDatasets passed as the input to Business Task and Business Entity methods. This limitation has been accepted in our design process as we expect a majority of use-cases for RESTful invocation of Business Task and Business Entity methods just requires to return data in ProDatasets, not to receive data. So, the only input message to the invokable messages supported through the RESTful interface is the parameter object instance.
As ABL reflection is not capable of providing the schema of the ProDataset parameters we've had to balance the requirement to provide the schema of the ProDataset parameters manually (through code in the Business Tasks) with ease of use. The schema of ProDataset parameters is required as the loosely typed JSON messages do not allow the AVM to securely create an input ProDataset "on the fly" without losing schema compatibility with the strongly typed Dataset's used within Business Tasks and Business Entities.
Preparing Business Tasks and Entities for RESTful invocation of methods
Business Tasks
A Business Task needs to implement the ISupportsRestMethods interface in order to support RESTful invocation. This Interface is expected by the IRestResourceService.
CLASS Consultingwerk.SmartComponentsDemo.OERA.SampleBusinessTask
IMPLEMENTS IBusinessService, ISupportsRestMethods:A typical implementation of the required GetRestMethods method looks like this:
/**
* Purpose: Returns the list of supported rest adresses for methods
* to the caller
* Notes:
* @return The list of supported rest methods to the caller
*/
METHOD PUBLIC ListRestMethod GetRestMethods ():
RETURN (NEW RestMethodProvider():GetRestMethods (THIS-OBJECT:GetClass())) .
END METHOD .Business Entities
Business Entities implement the GetRestMethods method by inheriting from the Consultingwerk.OERA.BusinessEntity class. It is however still required to declare the Interface in the Business Entity class header:
CLASS Consultingwerk.SmartComponentsDemo.OERA.Sports2000.CustomerBusinessEntity
INHERITS SmartBusinessEntity IMPLEMENTS IFetchDataByReference, ISupportsRestAddress, ISupportsRestMethods:Invokable methods
Valid signatures for methods that can be invoked using the RESTful interface must have the following characteristics:
Zero to five INPUT-OUTPUT datasets or dataset handles.
Zero or one INPUT parameter object. If used, the parameter object must be the last parameter defined.
A void return value, or an OOABL type that is serializable to JSON (typically using the Consultingwerk.IJsonSerializable interface).
This includes methods that take no inputs and return no values, as well as methods that have only a single dataset INPUT-OUTPUT parameter.
Defining RESTful interfaces for invokable methods
RESTful interfaces for Business Entity methods are defined by a @RestMethod annotations attached to the method (just before the METHOD block).
@RestMethod (address="/Customers/~{CustNum}/PutCustomerOnHold", requestMethod="get", parameterClassName="Consultingwerk.SmartComponentsDemo.OERA.Sports2000.PutOnHoldParameter", response="dsCustomer").
/*------------------------------------------------------------------------------
Purpose: Puts the Customer on hold
Notes: Client callable method
@param dsCustomer INPUT-OUTPUT DATASET
@param poParameter The Parameter Object for this method
------------------------------------------------------------------------------*/
METHOD PUBLIC VOID PutCustomerOnHold (INPUT-OUTPUT DATASET dsCustomer,
poParameter AS PutOnHoldParameter):Arguments of the @RestMethod annotation
The annotation provides the following arguments:
Argument/Attribute | Mandatory | Description |
|---|---|---|
address | no | The URI pattern to invoke the method. When not specified the pattern /{methodName} is used by default |
ParameterClassName | no | The name of the parameter class to instantiate for calling into the method. When not specified, the OOABL type name of the parameter is used. In either case, if the type name is not instantiable - if it's an interface, enum or abstract class - then a Consultingwerk.Exceptions.InvalidValueException will be raised. |
RequestMethod | no | The http method for the call described by this annotation. When not specified, get will be assumed as the default. |
Request | no | Comma delimited list of the components from the JSON request body of the REST request. Components are "parameter" and the names of the ProDataset parameters. |
Response | yes | Comma delimited list of the components for the JSON response of the REST request. Components are "return", the names of the ProDataset parameters and the name of the parameter. If only a single ProDataset is provided, it may have additional modifiers.
|
mapHeaders | no | Comma-delimited list of HTTP header names that are set as values on the method parameter object.. The header names may have an optional mapped property name, delimited by =. For example, a value of Accept-Language=Language,Content-Disposition sets the values of the Accept-Language and Content-Disposition headers from the web request to properties named Language and Content-Disposition. |
mapCookies | no | Comma-delimited list of HTTP cookie names to be set as values on the method parameter object. The cookie names may have an optional mapped property name, similar to the mapHeaders value. For example, a value of JSESSIONID=SessionId will set the value of the JSESSIONID cookie to a property named SessionId on the parameter object. |
tags | no | Comma-separated list of tags used for grouping endpoints in Swagger documentation. |
requestParameterSchemaType | no | An alternate name to use for the (input) parameter class in the Swagger documentation. This allows details of the implementation to be hidden. If not provided, the parameter's OOABL class name is used. |
responseSchemaType | no | An alternate name to use for the return value class in the Swagger documentation. This allows details of the implementation to be hidden. If not provided, the OOABL type name of the return value is used. |
propertyFrom | no | Comma-delimited list of values indicating how property values for a parameter class should be provided. Each entry has the format <property-name>:[query|hidden] . A value of query indicates that a query string is used. A value of hidden indicates that a value for that property is not expected to be sent by a client. |
description | no | A description for the operation used in the Swagger documentation |
contentType | no | The media type served by this method (SCL-5022), used for content negotiation against the request's Accept header and as the Content-Type of the response. Defaults to application/json. Several methods may share the same address and request method with different content types (e.g. application/json, text/xml, text/plain) - the request's Accept header selects the method to invoke. |
accept | no | Comma-delimited list of media types (Accept header values) served by this method (SCL-2349). Used for versioning of REST endpoints using the Accept header: the method is only invoked when the request's Accept header accepts one of the listed media types, and the negotiated media type becomes the Content-Type of the response. The same address may be registered multiple times - by methods of the same class or of different Business Tasks or Business Entities - with different accept media types. When not specified, content negotiation falls back to the contentType argument. See REST API Versioning. |
responseEntityClassName | no | The OOABL type name (class or interface) that describes the response entity (payload) carried by a RestMethodResponse or IRestMethodResponse return value. Both class names and interface names are supported: a class implementing Consultingwerk.IJsonSerializable is documented by its serializable properties, an interface is documented by reflecting over its public properties. The built-in JSON types JsonObject, JsonArray and JsonConstruct are documented as a generic object or array. The generated schema is added to the components.schemas object and referenced via $ref. |
responseEntityFormat | no | The MIME type (content type) of the response entity. When the entity is a JSON type (JsonObject, JsonArray, JsonConstruct or a type implementing Consultingwerk.IJsonSerializable) the format defaults to application/json. This argument overrides that default and becomes the content type of the documented response. A binary MIME type (e.g. application/pdf, image/png) without a responseEntityClassName is documented as a binary string (type: string, format: binary). |
responseStatusCodes | no | A comma-delimited list of HTTP status codes documented for the response. Defaults to 200. When a list is provided, every listed status code is documented with the response body in the generated Swagger document. |
Every invokable method may be annotated with more than a single @RestMethod annotation to provide alternative URI's for the same method.
Arguments of the @ParameterSchema annotation
When Business Task methods are configured to return a Dataset as the method response, it's required to provide an XSD file with the ProDataset schema to be included in the method documentation, e.g.:
Argument/Attribute | Mandatory | Description |
|---|---|---|
datasetname | yes | The name of the dataset |
schemafile | yes | The path reference to the dataset schema. This schema file is an XSD containing the schema of the dataset. Note that if there are fields in the dataset’s temp-table that have been define with a SERIALIZE-HIDDEN or XML-NODE-HIDDEN clause, that the fields are not written to the XSD file. This may result in schema mismatches and errors when calling the method. |
datasetSchemaType | no | An alternative name used to provide the name of the ProDataSet schema generated as properties of the components.schemas object in the generated Swagger document. If not provided, a combination of the Business Entity or Business Task class name and the dataset name is used, in the format <entityname>-<datasetname> . |
The @ParameterSchema annotation is optional.
Arguments of the @RestMethodParameter annotation
Parameters passed into the method may receive a meaningful description for Swagger documentation provided by a RestMethodParameter annotation. Each parameter should have its own annotation.
Argument/Attribute | Mandatory | Description |
|---|---|---|
name | yes | The name of the parameter. May also be parameter if the method’s parameter is a JsonObject. |
description | yes | A description for the parameter |
The @RestMethodParameter annotation is optional.
Arguments of the @ApiDoc annotation
A Business Entity or Business Task may use an ApiDoc annotation to provide additional information for the generated Swagger documentation, for the whole class.
Argument/Attribute | Mandatory | Description |
|---|---|---|
tags | no | Comma-separated list of tags used for grouping endpoints in Swagger documentation. |
entitySchemaType | no | An alternate name to use for the Business Entity or Business Task class name in the Swagger documentation. This allows details of the implementation to be hidden. If not provided, the Business Entity or Business Task class name is used. This value is used to generate schemas that will be re-used: these appear as properties of the components.schemas object in the generated document. |
The @ApiDoc annotation is optional.
Population of the Parameter object
The REST request to the annotated method will be used to populate the properties of the method's parameter object using the following sources:
For GET requests, the query string is first parsed and query string values will be used to populate the same named properties of the parameter object
For all other request methods, e.g. POST, the request entity (request payload) - when the request entity represents a JSON object - is used to deserialize a JSON serializable parameter object.
For all request methods (GET, POST, ...), the path parameter arguments are also assigned to properties of the parameter object. This is performed at the end, so that path parameters, as the strongest component of the request URI, overwrite properties assigned by query string components or deserialized form the request entity (request payload).
Population of the JSON response of the request
The JSON response object can either be populated by the return value of the method alone or a combination of the return value, the parameter object and the ProDataset parameter(s). For the return value we support both Consultingwerk.JsonSerializable derived types and JsonObjects/JsonArrays. For the parameter object we only support JsonSerializable instances.
Versioning invokable methods using the Accept header
Since SCL-2349, the same address and request method may be served by multiple methods - potentially in different Business Tasks or Business Entities - distinguished by the media types listed in the accept annotation argument (media type versioning). The request's Accept header selects the method to invoke, and the negotiated media type becomes the Content-Type of the response:
@RestMethod (address="/Customer", requestMethod="get",
parameterClassName="Consultingwerk.IntegerHolder",
accept="application/vnd.consultingwerk.customer.v1+json",
response="return").
METHOD PUBLIC JsonObject GetCustomerV1 (poParameter AS IntegerHolder):
@RestMethod (address="/Customer", requestMethod="get",
parameterClassName="Consultingwerk.IntegerHolder",
accept="application/vnd.consultingwerk.customer.v2+json,application/vnd.consultingwerk.customer.v2.1+json",
response="return").
METHOD PUBLIC JsonObject GetCustomerV2 (poParameter AS IntegerHolder):
@RestMethod (address="/Customer", requestMethod="get",
parameterClassName="Consultingwerk.IntegerHolder",
response="return").
METHOD PUBLIC JsonObject GetCustomer (poParameter AS IntegerHolder):Methods without the accept argument keep the current behavior and serve as the default when the request does not ask for one of the versioned media types. When the request URI matches only methods whose media types the request does not accept, HTTP status code 406 (Not Acceptable) is returned (Consultingwerk.OERA.RestResource.NotAcceptableException). See REST API Versioning for the complete content negotiation rules, the relationship to the contentType argument and the impact on the generated Swagger documentation.
Documenting RestMethodResponse and IRestMethodResponse return values
A method may return a Consultingwerk.OERA.RestResource.RestMethodResponse instance (or the Consultingwerk.OERA.RestResource.IRestMethodResponse interface) to customize the HTTP status code, status reason and content type of the response in addition to the payload, which is provided through the Entity property. Because the Entity property is typed as Progress.Lang.Object, its schema cannot be derived from the method signature. The responseEntityClassName, responseEntityFormat and responseStatusCodes arguments of the @RestMethod annotation are used to describe such responses in the generated Swagger document. They apply to methods whose return type is either the concrete RestMethodResponse class or the IRestMethodResponse interface.
JSON serializable entity (by class or interface name)
When responseEntityClassName names a class implementing Consultingwerk.IJsonSerializable, the response is documented with application/json and a schema built from the class' serializable properties. When it names an interface, the schema is built by reflecting over the interface' public properties. In both cases the schema is added to components.schemas and referenced via $ref. The built-in JSON types JsonObject, JsonArray and JsonConstruct are documented as a generic object or array.
@RestMethod (address="/AssignmentTypes", requestMethod="get",
response="return",
responseEntityClassName="Consultingwerk.SmartComponentsDemo.OERA.AssignmentTypesResponse").
METHOD PUBLIC RestMethodResponse GetAssignmentTypes ():produces a response like:
{
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Consultingwerk.SmartComponentsDemo.OERA.AssignmentTypesResponse"
}
}
}
}
}
}Binary content (e.g. PDF, images)
The responseEntityFormat argument overrides the default content type. A binary MIME type without a responseEntityClassName is documented as a binary string:
@RestMethod (address="/Invoice/~{InvoiceNum}/Pdf", requestMethod="get",
parameterClassName="Consultingwerk.IntegerHolder",
response="return",
responseEntityFormat="application/pdf").
METHOD PUBLIC RestMethodResponse GetInvoicePdf (poParameter AS IntegerHolder):produces:
{
"responses": {
"200": {
"content": {
"application/pdf": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
}
}
}Multiple status codes
The responseStatusCodes argument documents every listed HTTP status code with the response body. It defaults to 200 and accepts a comma-delimited list:
@RestMethod (address="/Customers/~{CustNum}", requestMethod="get",
parameterClassName="Consultingwerk.IntegerHolder",
response="return",
responseEntityClassName="Consultingwerk.SmartComponentsDemo.OERA.Sports2000.CustomerResponse",
responseStatusCodes="200,201").
METHOD PUBLIC RestMethodResponse GetCustomer (poParameter AS IntegerHolder):documents both the 200 and 201 status codes, each carrying the same response body. The 401 and 404 status codes are always added by the generator.
Examples
Classic invokable method of a Business Entity
@RestMethod (address="/Customers/~{CustNum}/PutCustomerOnHold", requestMethod="get", parameterClassName="Consultingwerk.SmartComponentsDemo.OERA.Sports2000.PutOnHoldParameter", response="dsCustomer").
/*------------------------------------------------------------------------------
Purpose: Puts the Customer on hold
Notes: Client callable method
@param dsCustomer INPUT-OUTPUT DATASET
@param poParameter The Parameter Object for this method
------------------------------------------------------------------------------*/
METHOD PUBLIC VOID PutCustomerOnHold (INPUT-OUTPUT DATASET dsCustomer,
poParameter AS PutOnHoldParameter):This annotation defines a method which will be exposed using a GET request under an URI like this:
The method parameter object (PutOnHoldParameter) is populated with the Comments property as specified in the query string of the get request and the CustNum value of 42 from the path parameter. The request will return the ProDataset dsCustomer to the caller.
Business Task method returning a serializable object to the caller
@RestMethod (address="/CalculateSumTask", requestMethod="get",
parameterClassName="Consultingwerk.SmartComponentsDemo.OERA.CalculateSumParameter",
response="return").
METHOD PUBLIC DecimalHolder CalculateSum (poParameter AS CalculateSumParameter):This annotation defines an endpoint for a Business Task method that will be exposed using a GET request under an URI like this:
http://localhost:8820/web/Entities/CalculateSumTask?Value1=17&Value2=4
The method parameter object properties are populated from the two values in the query string. The request will return the serialized DecimalHolder instance:
{
"Value": 21.0
}The same method can also be exposed like this:
@RestMethod (address="/CalculateSumTask/~{value1}/~{value2}", requestMethod="get",
parameterClassName="Consultingwerk.SmartComponentsDemo.OERA.CalculateSumParameter",
response="return").
METHOD PUBLIC DecimalHolder CalculateSum (poParameter AS CalculateSumParameter):http://localhost:8820/web/Entities/CalculateSumTask2/47/11
Business Task method returning two ProDatasets and optionally the parameter object
@RestMethod (address="/GetCustomerAndInvoices", requestMethod="get",
parameterClassName="Consultingwerk.IntegerHolder",
response="poParameter,dsCustomer,dsInvoice").
@RestMethod (address="/GetCustomerAndInvoices/~{Value}", requestMethod="get",
parameterClassName="Consultingwerk.IntegerHolder",
response="dsCustomer,dsInvoice").
@RestMethod (address="/GetCustomerAndInvoices", requestMethod="post",
parameterClassName="Consultingwerk.IntegerHolder",
response="dsCustomer,dsInvoice").
@ParameterSchema (datasetname="dsCustomer", schemafile="Consultingwerk/SmartComponentsDemo/OERA/Sports2000/dsCustomer.xsd").
@ParameterSchema (datasetname="dsInvoice", schemafile="Consultingwerk/SmartComponentsDemo/OERA/Sports2000/dsInvoice.xsd").
METHOD PUBLIC VOID GetCustomerAndInvoices (INPUT-OUTPUT DATASET dsCustomer,
INPUT-OUTPUT DATASET dsInvoice,
poParameter AS IntegerHolder):This method allows clients to provide the Value property of the IntegerHolder parameter in one of three ways:
GET http://localhost:8820/web/Entities/GetCustomerAndInvoices?Value=1
GET http://localhost:8820/web/Entities/GetCustomerAndInvoices/1
POST http://localhost:8820/web/Entities/GetCustomerAndInvoices with a JSON payload like:
"poParameter": {
"Value": 1
},
"dsCustomer": {
"eCustomer": [
{
"CustNum": 1,
"Country": "USA",
"Name": "Lift Line Skiing",
"Address": "Unter Käster 1",
"Address2": null,
"City": "Chicago",
"State": "MA",
"PostalCode": "01730",
"Contact": "Gloria Shepley",
"Phone": "(617) 450-0086",
"SalesRep": "HXM",
"CreditLimit": 0,
"Balance": 50000,
"Terms": "PREPAID ONLY",
"Discount": 35,
"Comments": "Put on hold: 25.06.2019 07:46:59\ntest",
"Fax": "",
"EmailAddress": "info@lift-tours.com",
"Flags": "C",
"SmartRecordKey": "000000001",
"SmartAttachments": true,
"SmartComments": false,
"SmartCopiedFrom": ""
}
]
},
"dsInvoice": {
"eInvoice": [
{
"Invoicenum": 6,
"CustNum": 1,
"InvoiceDate": "2009-02-09",
"Amount": 1829.5,
"TotalPaid": 1829.5,
"Adjustment": 0,
"OrderNum": 6,
"ShipCharge": 0
},
{
"Invoicenum": 79,
"CustNum": 1,
"InvoiceDate": "2008-11-19",
"Amount": 34707.84,
"TotalPaid": 34707.84,
"Adjustment": 0,
"OrderNum": 79,
"ShipCharge": 0
},
{
"Invoicenum": 146,
"CustNum": 1,
"InvoiceDate": "2009-02-28",
"Amount": 4605.91,
"TotalPaid": 0,
"Adjustment": 0,
"OrderNum": 177,
"ShipCharge": 0
}
]
}