RESTful services

RESTful services

Introduction

Business Entities of the SmartComponent Library can be exposed as RESTful services. RESTful services provide an alternative HTTP and JSON based interface which is very common for providing interfaces to 3rd party applications or mobile applications (the JSDO generic service is specialized to be used by JSDO based clients).

A few useful articles on the subject:

We support exposing Business Entities to provide resource collections and individual resources through simple HTTP URI's. We support GET (read), POST (create), PUT (update), DELETE and PATCH (partial update) methods.

Setting up the PASOE environment

We support exposing Business Entities as RESTful services through a PASOE Web Handler.

Registering the Web Handler

The Web Handler must be registered in the openedge.properties file:

handler28=Consultingwerk.OERA.RestResource.RestEntitiesWebHandler: /Entities

Note, that OpenEdge does not seem to support gaps in the numbering of Web Handlers. The handler28= ... is only supposed to serve as an example.

The entry registers the RestEntitiesWebHandler for the /web/Entities URI. From OpenEdge 11.7 on, we support using any other URI prefix instead of /Entities. OpenEdge 11.6 does only support the /Entities URI prefix.

Initializing the REST Address Service

While starting the PASOE ABL sessions, we need to initialize the RestResourceService (as an Service Instance of the IRestResourceService). This service is responsible to map the request URI to a specific Business Entities and provides the configuration to build a FetchDataRequest. The Service can be loaded through the provided services.xml file:

Consultingwerk/OERA/RestResource/services.xml

The contents of the services.xml file can also be merged with any other services.xml file if preferred. The Service follows the CCS IService Interface and requires the initialize() method to be executed when launched (the Consultingwerk.Framework.ServiceLoader will call the initialize() method). During the start, the RestAddressService will provide logging output like this.

AS-7 RestResour ### Seeking for ISupportsRestAddress implementations AS-7 RestResour ### Registering REST Resource: Consultingwerk.SmartComponentsDemo.OERA.Sports2000.OrderBusinessEntity AS-7 RestResour ### Number of Addresses: 4 AS-7 RestResour ### Registering REST Address: Record : /Orders/{OrderNum} Consultingwerk.SmartComponentsDemo.OERA.Sports2000.OrderBusinessEntity eOrder,eCustomer,eOrderLine,eItem OrderNum eOrder.*,eCustomer.Name,eCustomer.City,eCustomer.Country,eOrderLine.*,eItem.ItemName,eItem.Price AS-7 RestResour ### Registering REST Address: Record : /Orders/{OrderNum}/OrderLines/{LineNum} Consultingwerk.SmartComponentsDemo.OERA.Sports2000.OrderBusinessEntity eOrderLine,eItem LineNum * AS-7 RestResour ### Registering REST Address: Collection: /Customers/{CustNum}/Orders Consultingwerk.SmartComponentsDemo.OERA.Sports2000.OrderBusinessEntity eOrder OrderNum OrderDate,ShipDate,OrderStatus AS-7 RestResour ### Registering REST Address: Collection: /Orders/{OrderNum}/OrderLines Consultingwerk.SmartComponentsDemo.OERA.Sports2000.OrderBusinessEntity eOrderLine LineNum ItemNum,Qty,Price AS-7 RestResour ### --- AS-7 RestResour ### Registering REST Resource: Consultingwerk.SmartComponentsDemo.OERA.Sports2000.SalesRepBusinessEntity AS-7 RestResour ### Number of Addresses: 2 AS-7 RestResour ### Registering REST Address: Record : /Salesreps/{SalesRep} Consultingwerk.SmartComponentsDemo.OERA.Sports2000.SalesRepBusinessEntity eSalesrep SalesRep eSalesRep.* AS-7 RestResour ### Registering REST Address: Collection: /Salesreps Consultingwerk.SmartComponentsDemo.OERA.Sports2000.SalesRepBusinessEntity eSalesRep SalesRep SalesRep,RepName,Region AS-7 RestResour ### ---

The logging output requires the RestResource custom log entry type to be active.

Configuring Business Entities as RESTful resources

There are two requirements for Business Entities as RESTful resources:

ISupportsRestAddress Interface

Business Entities exposed as RESTful resources must implement the Consultingwerk.OERA.RestResource.ISupportsRestAddress interface. The interface requires the GetRestAddress() method. This method is already implemented by the Consultingwerk.OERA.BusinessEntity base class - so that basically the ISupportsRestAddress is a marker interface.

The GetRestAddress() method is used at runtime to register URI's of RESTful resources with Business Entities. The Business Entity returns the RESTful URI's it supports along with the meta data defining the details of the associated requests.

@RestAddress annotations

@RestAddress (type="record", address="/Customers/~{CustNum}", tables="eCustomer,eSalesrep", id="CustNum", fields="eCustomer.*,eSalesrep.RepName,eSalesrep.Region", canRead="true", canUpdate="true", canDelete="true", childAddresses="eSalesrep:/Salesreps/~{SalesRep}", links="orders:/Customers/~{CustNum}/Orders,salesrep:/Salesreps/~{SalesRep}"). @RestAddress (type="collection", address="/Customers", tables="eCustomer", id="CustNum", fields="Name,City,Country", canCreate="true", links="orders:/Customers/~{CustNum}/Orders,salesrep:/Salesreps/~{SalesRep}").

Business Entities define the details of the RESTful access through annotations (see The Annotation based Type Descriptor).

Overview of Annotation Parameters

The attributes for the RestAddress annotation are listed in the table below.

Parameter Name

Description

Parameter Name

Description

type

The type of the request, either "record" for a single record, or "collection" for list of records

address

The URI template. "/Customers/{CustNum}" means that the resources can be accessed using the resulting URI like http://localhost:8820/web/Entities/Customers/1 - The {CustNum} placeholder represents a field used in the resulting QueryString (FOR EACH eCustomer WHERE eCustomer.CustNum = 1). There can be multiple placeholders in a single URI template (e.g. /Customers/{CustNum}/Orders/{OrderNum}/OrderLines/{OrderLine} Collection or singleton (a single record resource that does not require any further selection criteria) may not require any value placeholder at all.

tables

The comma delimited list of tabels requested from the Business Entity (FetchDataRequest:Tables property) to serve the request

id

The comma delimited list of fields used to return the ID of a resource

fields

The comma delimited list of fields returned to the caller (by default), possible values may be field names, tablename.fieldname's or tablename.*

canRead

true/false, logical value determing of the URI supports read (GET) requests

canUpdate

true/false, logical value determing of the URI supports update requests (PUT and PATCH) - supported for individual records only

canCreate

true/false, logical value determing of the URI supports create requests (POST) - supported for collections only

canDelete

true/false, logical value determing of the URI supports delete (DELETE) requests - supported for collections and individual records

updateBusinessEntity

(optional, collections only) Fully qualified name of a separate "maintenance" Business Entity that CREATE (POST) and DELETE requests for this collection are redirected to. Use when the collection is served by a read-only "query" Business Entity but create/delete must be handled by the Business Entity that maintains the records. GET (read) requests continue to be served by the entity carrying this annotation; canCreate / canDelete still gate the endpoints. See Separate query and maintenance Business Entities for a collection below.

childAddresses

links to resource URI's for the child records returned by the request, comma delimited list, entries are in the form of {tablename}:{uri pattern}, e.g. eSalesrep:/Salesreps/~{SalesRep}

links

HATEOAS style links (to other business entities) in the form of {rel}:{uri pattern}, e.g. orders:/Customers/~{CustNum}/Orders,salesrep:/Salesreps/~{SalesRep}

tags

Comma-separated list of tags used for grouping endpoints in Swagger documentation.

methodDescriptionGet

(optional) A description for Swagger documentation for GET operations for this address.

methodDescriptionPut

(optional) A description for Swagger documentation for PUT operations for this address.

methodDescriptionPost

(optional) A description for Swagger documentation for POST operations for this address.

methodDescriptionPatch

(optional) A description for Swagger documentation for PATCH operations for this address.

methodDescriptionDelete

(optional) A description for Swagger documentation for DELETE operations for this address.

accept

(optional, SCL-2349) Comma-delimited list of media types (Accept header values) served by this address, e.g. application/vnd.consultingwerk.customer.v2+json. When set, the address is only used 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. When not set, the address matches any Accept header (current behavior). The same address may be registered multiple times - even by different Business Entities - with different accept media types. See REST API Versioning.

staticQuery

(optional, SCL-5708, record addresses only) true/false, default false. When true, the record GET and the record pre-fetch of PUT, PATCH and DELETE requests are served by a static data access query (SCL-2349) instead of a dynamically built query. Requires the Business Entity to provide a matching strong-typed static query request through its GetStaticQueryRequest method, which the Business Entity Designer StaticDataAccessQueryPlugin generates for unique indexes with static query generation enabled. When no matching static query is available, the request transparently falls back to the dynamic query. See Serving record addresses through static data access queries below.

A Business Entity may also use an ApiDoc annotation to provide additional information for Swagger documentation.

Separate query and maintenance Business Entities for a collection

By default the same Business Entity serves both the collection endpoint (GET list, POST create) and the record endpoint (GET / PUT / PATCH / DELETE of a single record). Sometimes it is preferable to split these responsibilities across two Business Entities:

  • a read-only query Business Entity that serves the collection endpoint - often optimized for reading and listing, and exposing only a slim set of fields, and

  • a maintenance Business Entity that owns the persistence logic (validation, defaults, related-table updates) for individual records.

The updateBusinessEntity parameter on a collection @RestAddress connects the two: CREATE (POST) and DELETE requests against the collection are redirected to the named maintenance Business Entity, while GET (read) requests keep being served by the query Business Entity that carries the annotation.

@RestAddress (type="collection", address="/Customers", tables="eCustomer", id="CustNum", fields="Name,City,Country", canCreate="true", canDelete="true", updateBusinessEntity="Consultingwerk.SmartComponentsDemo.OERA.Sports2000.CustomerBusinessEntity"). class Consultingwerk.SmartComponentsDemo.OERA.Sports2000.CustomerQueryBusinessEntity
@RestAddress (type="record", address="/Customers/~{CustNum}", tables="eCustomer", id="CustNum", fields="eCustomer.*", canRead="true", canUpdate="true", canDelete="true"). class Consultingwerk.SmartComponentsDemo.OERA.Sports2000.CustomerBusinessEntity

Notes:

  • canCreate and canDelete on the collection still control whether the POST and DELETE endpoints are exposed at all. updateBusinessEntity only changes which Business Entity performs the operation.

  • The maintenance Business Entity may expose a different (typically richer) dataset schema than the query Business Entity. The generated Swagger / OpenAPI document reflects this: the collection's POST request body and its /InitialValues endpoint reference the maintenance Business Entity's record schema, while the collection's GET response keeps the query Business Entity's read schema.

  • As a side effect, this also generates and resolves the record schema (-RecordTemplate) referenced by the maintenance Business Entity's own record endpoint (PUT / PATCH).

Versioning REST endpoints using the Accept header

Since SCL-2349, the same address may be served in multiple versions, distinguished by the media type in the request's Accept header (media type versioning). The optional accept annotation argument (see the parameter table above) assigns one or more media types to an address; different versions of the same address may be served by different Business Entities. Requests are routed to the address that serves a media type accepted by the request; addresses without the accept argument keep the current (Accept header agnostic) behavior. When the request URI matches only addresses whose media types the request does not accept, HTTP status code 406 (Not Acceptable) is returned.

See REST API Versioning for a detailed description including the content negotiation rules and the impact on the generated Swagger documentation.

Serving record addresses through static data access queries

Since SCL-5708, record type addresses can opt into being served by the static data access queries introduced with SCL-2349 (the FetchDataByStaticQuery API and the generated Fetch<Table>By<Fields>Request classes). A static query avoids building and preparing a dynamic query for every request, which benefits frequently requested record endpoints - and it matches the intent of a record address, which is to return a single record.

The feature is activated per address with the staticQuery="true" annotation parameter:

@RestAddress (type="record", address="/Salesreps/~{SalesRep}", tables="eSalesrep", id="SalesRep", fields="eSalesRep.*", canRead="true", staticQuery="true").

Requirements and behavior:

  • The Business Entity must provide a strong-typed static query request for the address' id field combination through the GetStaticQueryRequest method (interface Consultingwerk.OERA.ISupportsStaticQueryRequests). The Business Entity Designer StaticDataAccessQueryPlugin generates this method into the Business Entity (include file <Entity>BusinessEntity_staticqueryrequests.i) together with the static queries of the DataAccess class.

  • Only unique indexes with static query generation enabled are mapped by the generated method. On a unique index a single record match is guaranteed by the database, so the single-record semantics of a record address are preserved.

  • The static query serves the record GET as well as the record pre-fetch of PUT, PATCH and DELETE requests.

  • When no matching static query is available - the Business Entity does not implement the method, the id field combination is not mapped, or the DataAccess does not (or no longer) support the request - the handler transparently falls back to the dynamically built query. Addresses with a namedQuery always use the dynamic query.

  • StaticQueryRecordNotFoundException and StaticQueryRecordAmbiguousRecordFoundException raised by DataAccess classes that opted into the ThrowNotFoundException / ThrowAmbiguousRecordFoundException generation options are mapped to the same HTTP responses (404 Not Found / multiple resources found) as in the dynamic query path.

  • The static query always fills all generated fields; the fields annotation parameter and the ?fields= query string parameter are applied when the response is serialized (the dynamic query path restricts the FILL itself).

As with any @RestAddress change, adding the parameter requires regenerating the class' .annotations file and deleting the restresources-cache.json file.

Query String Parameters

RESTful requests for collections can be filtered using query string parameters.

Parameter Name

Description

Parameter Name

Description

fields

comma delimited list of field names to be returned

sort

The sort criteria as a comma delimited list in the form of +fieldname or -fieldname, e.g. sort=+country,-city

limit

The maximum number of records returned from a collection

offset

The first record to be returned, allows paging in combination with the limit parameter, e.g. limit=20&offset=20

{fieldname}

filter criteria, e.g. salesrep=BBB&creditlimit>=10000 - supported operators are =, >=, <=, >, <>, <

Supported Operators

The following query operators are supported by the RESTful interface:

  • =, [=], [EQ]

  • <>, [<>], [NE]

  • <, [<], [LT]

  • <=, [<=], [LE]

  • [BEGINS]

  • [CONTAINS]

  • [MATCHES]

Invoking Business Entity and Business Tasks Methods through the RESTful interface

See Support for RESTful invocation of Business Task and Business Entity Methods.

Samples

http://localhost:8820/web/Entities/Customers?limit=3&Country=DE&City=Dresden&sort=-CreditLimit&fields=Name,CreditLimit

Returns the first three customers by CreditLimit descending. Returns also links to orders of that customer and the salesrep.

Annotation:

@RestAddress (type="collection", address="/Customers", tables="eCustomer", id="CustNum", fields="Name,City,Country", canCreate="true", links="orders:/Customers/~{CustNum}/Orders,salesrep:/Salesreps/~{SalesRep}").

Response:

[ { "id": 242770, "url": "http://localhost:8820/web/Entities/Customers/242770", "Name": "Mikro Designs", "CreditLimit": 83500.0, "links": [ { "rel": "orders", "href": "http://localhost:8820/web/Entities/Customers/242770/Orders" }, { "rel": "salesrep", "href": "http://localhost:8820/web/Entities/Salesreps/GPE" } ] }, { "id": 1242770, "url": "http://localhost:8820/web/Entities/Customers/1242770", "Name": "Mikro Designs", "CreditLimit": 83500.0, "links": [ { "rel": "orders", "href": "http://localhost:8820/web/Entities/Customers/1242770/Orders" }, { "rel": "salesrep", "href": "http://localhost:8820/web/Entities/Salesreps/GPE" } ] }, { "id": 46030, "url": "http://localhost:8820/web/Entities/Customers/46030", "Name": "Lazysize", "CreditLimit": 58000.0, "links": [ { "rel": "orders", "href": "http://localhost:8820/web/Entities/Customers/46030/Orders" }, { "rel": "salesrep", "href": "http://localhost:8820/web/Entities/Salesreps/JAL" } ] } ]

http://localhost:8820/web/Entities/Customers/3

Returns the customer with the CustNum of 3. Returns links to orders and salesrep (links property) as well as a preview of the eSalesrep record returned by the Business Entity (childAddresses property)

Annotation:

@RestAddress (type="record", address="/Customers/~{CustNum}", tables="eCustomer,eSalesrep", id="CustNum", fields="eCustomer.*,eSalesrep.RepName,eSalesrep.Region", canRead="true", canUpdate="true", canDelete="true", childAddresses="eSalesrep:/Salesreps/~{SalesRep}", links="orders:/Customers/~{CustNum}/Orders,salesrep:/Salesreps/~{SalesRep}").

Response:

{ "id": 3, "url": "http://localhost:8820/web/Entities/Customers/3", "CustNum": 3, "Country": "DE", "Name": "Hoops", "Address": "Suite 415", "Address2": "werwe", "City": "", "State": "GA", "PostalCode": "02112", "Contact": "Michael Traitser", "Phone": "(617) 355-1557", "SalesRep": "HXM", "CreditLimit": 75000.0, "Balance": 1199.95, "Terms": "Net30", "Discount": 10, "Comments": "This customer is now OFF credit hold.", "Fax": "", "EmailAddress": "info@hoops.com", "Flags": "C", "eSalesrep": { "url": "http://localhost:8820/web/Entities/Salesreps/HXM", "RepName": "Harry Munvig", "Region": "West" }, "links": [ { "rel": "orders", "href": "http://localhost:8820/web/Entities/Customers/3/Orders" }, { "rel": "salesrep", "href": "http://localhost:8820/web/Entities/Salesreps/HXM" } ] }

http://localhost:8820/web/Entities/Customers/3/Orders?OrderStatus=Partially%20Shipped

Returns the "Partially Shipped" Orders of Customer number 3. The request is served by the Orders Business Entity, the Customer Business Entity is not involved in the request.

Annotation:

@RestAddress (type="collection", address="/Customers/~{CustNum}/Orders", tables="eOrder", id="OrderNum", fields="OrderDate,ShipDate,OrderStatus", canCreate="true").

Response:

[ { "id": 160, "url": "http://localhost:8820/web/Entities/Customers/3/Orders/160", "OrderDate": "2008-12-02", "ShipDate": "2009-05-23", "OrderStatus": "Partially Shipped" } ]