skip to main content
OpenEdge Development: Web Services
Creating OpenEdge REST Web Services : Data Object Services : Coding Business Entities to implement Data Objects : Coding ABL routines to implement a Data Object resource : Updating Business Entities for access by Telerik DataSources and Rollbase external objects
 
Updating Business Entities for access by Telerik DataSources and Rollbase external objects
In both current and prior OpenEdge releases, the Business Entity generated for an OpenEdge Data Object assumes that the filter parameter of the Data Object Read operation can contain a string value of any format that is agreed upon between the developer of the client mobile app and the OpenEdge developer. For an Express project, Progress Developer Studio for OpenEdge generates Business Entity code that looks for an ABL WHERE string as the agreed upon value of this parameter, but otherwise leaves it up to the OpenEdge developer to provide the code required to handle a different format for the filter value.
If you are building a mobile app using the Telerik Platform, to allow the Telerik Kendo UI to access OpenEdge data using the server filtering, sorting, and paging features of the JSDO dialect of the Kendo UI DataSource, you must manually update the ABL code in the Business Entity to handle a new filter string format and add a new Data Object Invoke method. For more information on using the Telerik Platform to build mobile apps using the JSDO dialect of the Kendo UI DataSource, see the online Progress Data Objects Guide and Reference at https://documentation.progress.com/output/pdo/.
If your Business Entity is intended to implement a Rollbase external object (a Rollbase object created to access an OpenEdge Data Object resource), you must provide similar updates for use by the Rollbase server to preprocess data that the Rollbase external object reads from the OpenEdge Data Object. For more information on Rollbase external objects, see the following Rollbase documentation: https://documentation.progress.com/output/rb/doc/index.html#context/rb/rb_oe.
The Developer Studio for OpenEdge does not currently generate all the annotations to configure the Data Service Catalog or the behavior in a Business Entity that are required to provide OpenEdge data to a Kendo UI DataSource using its server preprocessing features, even when you use an Express Data Object project. Therefore, you must manually update the Business Entity with additional annotations and code to:
*Handle the filter parameter value of the Data Object Read operation method as an OpenEdge-proprietary JSON Filter Pattern (described below). This is required to implement the Read operation for any JSDO accessed by the Kendo UI DataSource, allowing it to use its server preprocessing features. Rollbase also relies on a similar JSON Filter Pattern to preprocess the data read by Rollbase external objects.
*Provide a Data Object Invoke operation method that returns the total number of records available for access in the OpenEdge database (that is, in the result set) according to the specified server filtering options. This method is required only if the DataSource uses server paging, because Kendo UI needs to know the total number of records available in the server result set for a given DataSource in order to manage server paging on the client.
The JSON Filter Pattern is a simple JSON object that contains the ablFilter, id, orderBy, skip, and top properties, which are intended to be used as follows:
*ablFilter — Contains the text of an ABL WHERE string (not including the WHERE keyword itself) on which to filter the OpenEdge database query that returns the result set, as in the following examples:
*"ablFilter" : "(State = 'MA') OR (State = 'GA')"
*"ablFilter" : "Name BEGINS 'A'"
A value for this property is always specified for server filtering.
Note: Some Kendo UI widgets make this setting on the Kendo UI DataSource internally.
*id — Specifies a unique logical ID for data on the server, however you choose to implement it. For example, you can specify a string representing the ABL ROWID of a record in an OpenEdge database.
In order for the client to set this id property to a value the Business Entity can use to access data, you need to initially return an appropriate value (or multiple values) for each Read operation on the data. For example, you might return a value that identifies each row of data in the result set. For OpenEdge data, this can be an id field in each temp-table record containing the database ROWID of the corresponding record in the OpenEdge database.
Note: The id property is not currently used by the Kendo UI, but is used, for example, by Rollbase external objects.
*orderBy — A comma-delimited list of the names of fields used to sort the data in the result set (ascending order by default). After any field, desc can be added (case-sensitive) to indicate that sort order is descending for that field. For example:
*"orderBy" : "Balance, State" — The data should be sorted ascending, first by Balance, then within Balance, by State.
*"orderBy" : "Balance desc" — The data should be sorted by Balance descending.
*"orderBy" : "Country, State desc, City" — The data should be sorted first by Country ascending, then within Country, by State descending, then within State, by City ascending.
A value for this property is always specified for server sorting.
Note: Some Kendo UI widgets make this setting on the Kendo UI DataSource internally.
*skip — Specifies how many records in the result set to skip before returning (up to) a page of data in the result set. A value for this property is always specified (along with top) for server paging. For example, if the requested page size (top) is 10 and the request is for the 5th page of data, the value of this property is set to 40.
Note: The Kendo UI DataSource calculates this value for some Kendo UI widgets internally.
*top — Specifies how many records should be returned in a single page (that is, the page size) of the result set after using skip. A value for this property is always specified (along with skip) for server paging. (The final page in the result set can contain a smaller number of records than top specifies.)
Note: Some Kendo UI widgets set this value on the Kendo UI DataSource internally.
The Invoke operation method that you need to add in order for server paging to return the total number of records in the server result set must have the following ABL method signature:
Syntax:
METHOD PUBLIC VOID countFnName (
INPUT filter AS CHARACTER,
OUTPUT numRecs AS INTEGER
)
Where:
countFnName
Specifies a name for the method. You then assign this name as the value of the countFnName configuration property that you specify in the transport object when you instantiate the Kendo UI DataSource in the mobile app. For more information, see the sections on using the JSDO dialect of the Kendo UI DataSource in the Progress Data Objects Guide and Reference: https://documentation.progress.com/output/pdo/.
filter
Specifies a string containing the same JSON Filter Pattern value that is passed to the Read operation method.
numRecs
Specifies an integer that you assign the total number of records in the server result set identified by filter.
Note that the URI annotation for the Invoke operation must specify the filter input parameter for this method as a URL query parameter, similar to the URI annotation for the Read operation, as follows:
Syntax:
URI="/countFnName?filter=~{filter~}"
Where countFnName is the name of your ABL Invoke operation method. Unlike for the Read operation, you might have to change the default URI when you use Developer Studio for OpenEdge to annotate this ABL method as an Invoke operation. The sample Business Entity (shown later in this section) shows an example of both this method and its annotations.
The Kendo UI DataSource calls this Invoke operation method on the JSDO whenever it calls the JSDO fill( ) method to return a page of data from the server. This method is called only when the DataSource has server paging enabled.
Following is a sample Business Entity showing the method generated to implement the Data Object Read operation, with manual annotation and code changes required both to implement a JSDO for access by the Kendo UI DataSource and to implement a Rollbase external object.
This is the include file (customer.i) that is referenced by the Business Entity, including a ProDataSet (dsCustomer) that contains a single temp-table (ttCustomer), with fields that you add to the fields that correspond to the existing database table fields indicated in bold, and an additional index you must also add shown in bold:
DEFINE TEMP-TABLE ttCustomer BEFORE-TABLE bttCustomer
FIELD id AS CHARACTER
FIELD seq AS INTEGER INITIAL ?

FIELD CustNum AS INTEGER INITIAL "0" LABEL "Cust Num"
FIELD Name AS CHARACTER LABEL "Name"
FIELD Address AS CHARACTER LABEL "Address"
FIELD Address2 AS CHARACTER LABEL "Address2"
FIELD Balance AS DECIMAL INITIAL "0" LABEL "Balance"
FIELD City AS CHARACTER LABEL "City"
FIELD Comments AS CHARACTER LABEL "Comments"
FIELD Contact AS CHARACTER LABEL "Contact"
FIELD Country AS CHARACTER INITIAL "USA" LABEL "Country"
FIELD CreditLimit AS DECIMAL INITIAL "1500" LABEL "Credit Limit"
FIELD Discount AS INTEGER INITIAL "0" LABEL "Discount"
FIELD EmailAddress AS CHARACTER LABEL "Email"
FIELD Fax AS CHARACTER LABEL "Fax"
FIELD Phone AS CHARACTER LABEL "Phone"
FIELD PostalCode AS CHARACTER LABEL "Postal Code"
FIELD SalesRep AS CHARACTER LABEL "Sales Rep"
FIELD State AS CHARACTER LABEL "State"
FIELD Terms AS CHARACTER INITIAL "Net30" LABEL "Terms"
INDEX seq IS PRIMARY UNIQUE seq
INDEX CustNum IS UNIQUE CustNum
.

DEFINE DATASET dsCustomer FOR ttCustomer.
Note: For access by a Rollbase external object, you must implement the Business Entity to provide its data as a ProDataSet with only a single temp-table, as shown in this example. For access by the Telerik Platform, you can implement the Business Entity to provide its data either as a single temp-table or as a ProDataSet with one or more temp-tables.
The id field is added to each temp-table record to support Rollbase external objects.
The seq field is used to guarantee the order of records in the serialized temp-table that is returned as JSON to the JSDO. To work properly, this field must be initialized with the Unknown value (?). You must also add an index on seq that is both PRIMARY and UNIQUE. You can also have additional indexes, which can be the same or different than those in the database, as shown for CustNum, but the index on seq must be the PRIMARY one.
Following is the class file for the Business Entity, Customer.cls. Manually added annotations and code are in bold, except in the case of added methods, where only the first and last lines of the method are in bold:
@program FILE(name="Customer.cls", module="AppServer").
@openapi.openedge.export FILE(type="REST", executionMode="singleton", useReturnValue="false", writeDataSetBeforeImage="false").
@progress.service.resource FILE(name="Customer", URI="/Customer", schemaName="dsCustomer", schemaFile="Customer/AppServer/customer.i").

USING Progress.Lang.*.

USING OpenEdge.BusinessLogic.BusinessEntity.
USING Progress.Json.ObjectModel.*.

BLOCK-LEVEL ON ERROR UNDO, THROW.

CLASS Customer INHERITS BusinessEntity:

{"customer.i"}

DEFINE DATA-SOURCE srcCustomer FOR Customer.

DEFINE VARIABLE iSeq AS INTEGER NO-UNDO.

CONSTRUCTOR PUBLIC Customer():

DEFINE VAR hDataSourceArray AS HANDLE NO-UNDO EXTENT 1.
DEFINE VAR cSkipListArray AS CHAR NO-UNDO EXTENT 1.

SUPER (DATASET dsCustomer:HANDLE).

/* Data Source for each table in dataset.
Should be in table order as defined in DataSet */
hDataSourceArray[1] = DATA-SOURCE srcCustomer:HANDLE.

/* Skip-list entry array for each table in DataSet.
Should be in temp-table order as defined in DataSet */
/* Each skip-list entry is a comma-separated list of field names
to be ignored in the CREATE statement */

cSkipListArray[1] = "CustNum".

THIS-OBJECT:ProDataSource = hDataSourceArray.
THIS-OBJECT:SkipList = cSkipListArray.

END CONSTRUCTOR.

@openapi.openedge.export(type="REST", useReturnValue="false", writeDataSetBeforeImage="true").
@progress.service.resourceMapping(type="REST", operation="read", URI="?filter=~{filter~}", alias="", mediaType="application/json").
@openapi.openedge.method.property (name="mappingType", value="JFP").
@openapi.openedge.method.property (name="capabilities", value="ablFilter,top,skip,id,orderBy").

METHOD PUBLIC VOID ReadCustomer(
INPUT filter AS CHARACTER,
OUTPUT DATASET dsCustomer):

IF filter BEGINS "~{" THEN
THIS-OBJECT:JFPFillMethod (INPUT filter).
ELSE DO:
BUFFER ttCustomer:HANDLE:BATCH-SIZE = 0.
BUFFER ttCustomer:SET-CALLBACK ("AFTER-ROW-FILL", "AddIdField").

SUPER:ReadData(filter).
END.

END METHOD.

/* Other CUD and Submit operation methods */
. . .

METHOD PRIVATE VOID JFPFillMethod(INPUT filter AS CHARACTER):

DEFINE VARIABLE jsonParser AS ObjectModelParser NO-UNDO.
DEFINE VARIABLE jsonObject AS JsonObject NO-UNDO.
DEFINE VARIABLE cWhere AS CHARACTER NO-UNDO.
DEFINE VARIABLE hQuery AS HANDLE NO-UNDO.
DEFINE VARIABLE lUseReposition AS LOGICAL NO-UNDO.
DEFINE VARIABLE iCount AS INTEGER NO-UNDO.
DEFINE VARIABLE ablFilter AS CHARACTER NO-UNDO.
DEFINE VARIABLE id AS CHARACTER INITIAL ? NO-UNDO.
DEFINE VARIABLE iMaxRows AS INTEGER INITIAL ? NO-UNDO.
DEFINE VARIABLE iSkipRows AS INTEGER INITIAL ? NO-UNDO.
DEFINE VARIABLE cOrderBy AS CHARACTER INITIAL "" NO-UNDO.

/* purge any existing data */
EMPTY TEMP-TABLE ttCustomer.

jsonParser = NEW ObjectModelParser().
jsonObject = CAST(jsonParser:Parse(filter), jsonObject).
iMaxRows = jsonObject:GetInteger("top") NO-ERROR.
iSkipRows = jsonObject:GetInteger("skip") NO-ERROR.
ablFilter = jsonObject:GetCharacter("ablFilter") NO-ERROR.
id = jsonObject:GetCharacter("id") NO-ERROR.
cOrderBy = jsonObject:GetCharacter("orderBy") NO-ERROR.
cWhere = "WHERE " + ablFilter NO-ERROR.

IF cOrderBy > "" THEN DO:
cOrderBy = REPLACE(cOrderBy, ",", " by ").
cOrderBy = "by " + cOrderBy + " ".
/* NOTE: id and seq fields should be removed from
cWhere and cOrderBy */
cOrderBy = REPLACE(cOrderBy, "by id desc", "").
cOrderBy = REPLACE(cOrderBy, "by id ", "").
cOrderBy = REPLACE(cOrderBy, "by seq desc", "").
cOrderBy = REPLACE(cOrderBy, "by seq ", "").
END.

lUseReposition = iSkipRows <> ?.

IF iMaxRows <> ? AND iMaxRows > 0 THEN DO:
BUFFER ttCustomer:HANDLE:BATCH-SIZE = iMaxRows.
END.
ELSE DO:
IF id > "" THEN
BUFFER ttCustomer:HANDLE:BATCH-SIZE = 1.
ELSE
BUFFER ttCustomer:HANDLE:BATCH-SIZE = 0.
END.

BUFFER ttCustomer:ATTACH-DATA-SOURCE(DATA-SOURCE srcCustomer:HANDLE).

IF cOrderBy = ? THEN cOrderBy = "".
cWhere = IF cWhere > "" THEN (cWhere + " " + cOrderBy)
ELSE ("WHERE " + cOrderBy).
DATA-SOURCE srcCustomer:FILL-WHERE-STRING = cWhere.

IF lUseReposition THEN DO:
hQuery = DATA-SOURCE srcCustomer:QUERY.
hQuery:QUERY-OPEN.

IF id > "" AND id <> "?" THEN DO:
hQuery:REPOSITION-TO-ROWID(TO-ROWID(id)).
END.
ELSE IF iSkipRows <> ? AND iSkipRows > 0 THEN DO:
hQuery:REPOSITION-TO-ROW(iSkipRows).
IF NOT AVAILABLE Customer THEN
hQuery:GET-NEXT() NO-ERROR.
END.

iCount = 0.
REPEAT WHILE NOT hQuery:QUERY-OFF-END AND iCount < iMaxRows:
hQuery:GET-NEXT () NO-ERROR.
IF AVAILABLE Customer THEN DO:
CREATE ttCustomer.
BUFFER-COPY Customer TO ttCustomer.
ASSIGN ttCustomer.id = STRING(ROWID(Customer))
iSeq = iSeq + 1
ttCustomer.seq = iSeq.
END.
iCount = iCount + 1.
END.
END.
ELSE DO:
IF id > "" THEN DATA-SOURCE srcCustomer:RESTART-ROWID(1)
= TO-ROWID ((id)).
BUFFER ttCustomer:SET-CALLBACK ("AFTER-ROW-FILL", "AddIdField").
DATASET dsCustomer:FILL().
END.

FINALLY:
BUFFER ttCustomer:DETACH-DATA-SOURCE().
END FINALLY.

END METHOD.

METHOD PUBLIC VOID AddIdField (INPUT DATASET dsCustomer):
ASSIGN ttCustomer.id = STRING(ROWID(Customer))
iSeq = iSeq + 1
ttCustomer.seq = iSeq.
END.

@openapi.openedge.export(type="REST", useReturnValue="false", writeDataSetBeforeImage="false").
@progress.service.resourceMapping(type="REST", operation="invoke", URI="/count?filter=~{filter~}", alias="", mediaType="application/json").
METHOD PUBLIC VOID count( INPUT filter AS CHARACTER, OUTPUT numRecs AS INTEGER):
DEFINE VARIABLE jsonParser AS ObjectModelParser NO-UNDO.
DEFINE VARIABLE jsonObject AS JsonObject NO-UNDO.
DEFINE VARIABLE ablFilter AS CHARACTER NO-UNDO.
DEFINE VARIABLE cWhere AS CHARACTER NO-UNDO.
DEFINE VARIABLE qh AS HANDLE NO-UNDO.

IF filter BEGINS "WHERE " THEN
cWhere = filter.
ELSE IF filter BEGINS "~{" THEN
DO:
jsonParser = NEW ObjectModelParser().
jsonObject = CAST(jsonParser:Parse(filter), jsonObject).
ablFilter = jsonObject:GetCharacter("ablFilter") NO-ERROR.
cWhere = "WHERE " + ablFilter.
END.
ELSE IF filter NE "" THEN
DO:
/* Use filter as WHERE clause */
cWhere = "WHERE " + filter.
END.

CREATE QUERY qh.
qh:SET-BUFFERS(BUFFER Customer:HANDLE).
qh:QUERY-PREPARE("PRESELECT EACH Customer " + cWhere).
qh:QUERY-OPEN ().
numRecs = qh:NUM-RESULTS.

END METHOD.

END CLASS.

Key changes to note in Customer.cls include the following:
*Added statement: USING Progress.Json.ObjectModel.*. — Supports access to the ABL core classes for parsing the JSON Filter Pattern object returned in the filter parameter of the ReadCustomer( ) method.
*Added @openapi.openedge.method.property annotations: (name="mappingType", value="JFP") and (name="capabilities", value="ablFilter,top,skip,id,orderBy") — Causes the JSDO created from this Business Entity to translate the Kendo UI DataSource inputs to the filter parameter of the ReadCustomer( ) method to a JSON Filter Pattern object. Without this annotation, this filter parameter passes a JSON duplicate of the Kendo UI settings most recently provided by the filter configuration property or filter( ) method on the DataSource.
*Updated statement in the ReadCustomer( ) method: IF filter BEGINS "~{" THEN ... ELSE ... — If the filter parameter value starts with a left brace, invokes an added method (JFPFillMethod( )) to handle an anticipated JSON Filter Pattern; otherwise, the BATCH-SIZE attribute on the buffer handle for ttCustomer is set to return all records in the result set, the AddIdField( ) method is registered as a callback for the AFTER-ROW-FILL event on dsCustomer to initialize the id and seq fields of each record in the result set, and the filter parameter is passed to the ReadData( ) method of the inherited OpenEdge.BusinessLogic.BusinessEntity abstract class to handle another filter string format specified when not using the Telerik Platform. (Note that JFPFillMethod( ) also sets different values for BATCH-SIZE based on the filter settings before registering AddIdField( ).)
*Added method: JFPFillMethod( ) — Parses the property values from the JSON Filter Pattern passed to the filter parameter, assigning any that are found to corresponding ABL variables. Any of these variables that contain appropriate values are then used to implement the filtering, sorting, and paging options that are specified. These values determine the BATCH-SIZE to return in the ttCustomer temp-table for a successful result. Thus, a successful result returns either a single record identified by id, a specified page of records (iMaxRows > 0), or the entire result set of records in the ttCustomer temp-table of the DATASET dsCustomer parameter passed as output from the ReadCustomer( ) method. The record, or set of records, returned represent the result from the specified filtering, sorting, and paging options, if any. Note that an ABL query is used for some options, while the FILL( ) method on dsCustomer is used for others to copy Customer data to ttCustomer and update the corresponding id and seq fields.
*Added callback method: AddIdField( ) — With this callback registered by either ReadCustomer( ) or JFPFillMethod( ) in response to the AFTER-ROW-FILL event on dsCustomer, this method assigns the current sequence number (seq) and ROWID value (id) of the corresponding database record whose Customer fields have just been copied (using FILL( )) into the corresponding fields of the current ttCustomer record.
*Method: count( ), added as a Data Object Invoke operation to return the total number of records in the server result set — Executed as part of returning a server page to the Kendo UI DataSource, this method identifies any WHERE string in the filter parameter and adds it to a PRESELECT query on the target database table that it constructs and opens. It then passes NUM-RESULTS on the opened query as the value of its output parameter to provide the total number of records to Kendo UI. Note the URI that you must specify as part of the Invoke operation annotations in Developer Studio for OpenEdge: URI="/count?filter=~{filter~}".
Note: If you do not add a method like this to the Business Entity and annotate it (in Developer Studio) as an Invoke operation, the JSDO throws an exception when the Kendo UI DataSource tries to reference the method as part of reading a server page.