Try OpenEdge Now
skip to main content
Web Services
Creating OpenEdge REST Web Services : Data Object Services : Coding Business Entities to implement Data Objects : Updates to allow access by Kendo UI DataSources and Rollbase external objects : Sample Read operation updated to handle JFP input and server paging
 
Sample Read operation updated to handle JFP input and server paging
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 Kendo UI, 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="count",
URI="/MyCount?filter=~{filter~}",
alias="", mediaType="application/json").
METHOD PUBLIC VOID MyCount( 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.

IF cWhere = ? OR cWhere = "?" THEN cWhere = "".
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 intercept the value the Kendo UI DataSource passes to the filter parameter of ReadCustomer( ) and convert it to a JSON Filter Pattern object. Without this annotation, the DataSource passes a value to the filter parameter that is a JSON duplicate of the Kendo UI-proprietary settings most recently provided by the filter configuration property or the 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 Kendo UI to access the Read operation. (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.
*Added method: MyCount( ), added as a Data Object Count 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. (Note that it sets the string returned by filter to "" if its value is otherwise unspecified and set to the Unknown value (?).) It then passes the value of the NUM-RESULTS attribute on the opened query to its output parameter to provide the total number of records to Kendo UI.
Note: If you do not add a method like this to the Business Entity and annotate it (in Developer Studio) as a Count operation, the JSDO throws an exception when the Kendo UI DataSource tries to reference the method as part of reading a server page.