A Simple RESTful Service
To create a service we will require a WebApp, and within that an object of the cWebHttpHandler class.
The code below shows how you might write a simple RESTful API for the sample Customer table (from the DataFlex Examples: WebOrder or WebOrderMobile, which will, by default, have been installed under "C:\DataFlex nn.n Examples" if you chose to install the Examples during the DataFlex installation process - it also uses the WebAppUser table from those examples).
Warning: This sample code exposes your customer table to the world (or, if using one of the security options below, to anybody who has valid credentials) - they can abuse that as they choose. It is probably not an approach you would want to take in a real-world environment without considerable enhancement! (Details on the discussion page.)
CustomerService.wo:
Use cWebHttpHandler.pkg
Use cJsonObject.pkg
Use cCustomerDataDictionary.dd
// Used to validate users and their passwords when using BASIC authentication
Open WebAppUser
// Definitions of the various authentication types we will support
Enum_List
Define C_authTypeNone
Define C_authTypeBasic
Define C_authTypeBearer
End_Enum_List
Object oCustomerService is a cWebHttpHandler
Property Integer peAuthType C_authTypeNone
Property String psApiKey
Property String psCachedBaseURL
Property String[] pasPathParts
Property Boolean pbAllowAccess False
Property String psRealm "REST Demo" // The "realm" for use in BASIC Auth
Set psPath to "customerAPI" // The URL *within* the WebApp's virtual directory
Set psVerbs to "GET,POST,PATCH,DELETE"
Set peErrorType to httpErrorJson
Object oCustomer_DD is a cCustomerDataDictionary
End_Object
//
// Utility methods
//
// This function will return the Base URL, so that URLs to other resources
// within the API can be constructed. When first constructed it is then
// cached in the psCachedBaseURL property so that we don't have to do it
// each time, which is expensive on server messages and hurts performance.
Function BaseURL Returns String
String sHost sPort sProt sBase sURL sPath
Boolean bSec
Integer iPos
Get psCachedBaseURL to sBase
If (sBase = "") Begin
Get ServerVariable "SERVER_NAME" to sHost
Get ServerVariable "SERVER_PORT" to sPort
Get ServerVariable "SERVER_PORT_SECURE" to bSec
Get ServerVariable "URL" to sURL
Get psRequestPath to sPath
Move (RightPos(sPath, sURL)) to iPos
Move (Left(sURL, (iPos - 1))) to sURL
// Note: Case should not matter, but it seems that in Postman it
// does, so stick to lowercase for the protocol:
Move (If(bSec, "https", "http")) to sProt
Move (sProt + "://" + sHost + ":" + ;
sPort + sURL) to sBase
Set psCachedBaseURL to sBase
End
Function_Return sBase
End_Function
// A simple function to find a customer row from the appropriate part
// of the invoking URL using its DD
Function FindCustomerFromRequest Returns Boolean
String[] asParts
Boolean bFound
Get pasPathParts to asParts
If (SizeOfArray(asParts) = 3) Begin
Send Clear of oCustomer_DD
Move asParts[2] to Customer.Customer_Number
Send Find of oCustomer_DD EQ Index.1
Move (Found) to bFound
If bFound ; // Check that you actually got the right one (should always be the case)
Move (Customer.Customer_Number = asParts[2]) to bFound
If not bFound ;
Send SetResponseStatus 404 "Not Found"
End
Else ; // This shouldn't ever happen, but...
Send SetResponseStatus 400 "Bad Request"
Function_Return bFound
End_Function
// A simple function to update a customer row from the passed JSON through
// its DD. However see Wil's note regarding this on the discussion page.
Function UpdateCustomerFromJson Returns Boolean
Handle hoReq
UChar[] ucaReq
Boolean bOK
Integer i iMax iField iType
String sField sVal
Get RequestDataUChar 0 to ucaReq // Read the entire body into the UChar[]
If (SizeOfArray(ucaReq) < 10) Begin // '{"x":"y"}' is the minimum that
// makes sense: 9 chars
Send SetResponseStatus 400 "Bad Request"
Function_Return False
End
Get Create (RefClass(cJsonObject)) to hoReq
Get ParseUtf8 of hoReq ucaReq to bOK
If not bOK Begin
Send SetResponseStatus 400 "Bad Request"
Function_Return False
End
Get MemberCount of hoReq to iMax
Decrement iMax
Move False to Err
For i from 0 to iMax
Get MemberNameByIndex of hoReq i to sField
Field_Map Customer.File_Number sField to iField
If iField Begin
Get_Attribute DF_FIELD_TYPE of Customer.File_Number iField to iType
Get MemberValue of hoReq sField to sVal
If (iType = DF_DATE) ;
Move (ConvertFromClient(typeDate, sVal)) to sVal
If (iType = DF_DATETIME) ;
Move (ConvertFromClient(typeDateTime, sVal)) to sVal
Set Field_Changed_Value of oCustomer_DD iField to sVal
End
Loop
If (Err) ;
Send SetResponseStatus 400 "Bad Request"
Function_Return (not(Err))
End_Function
// This will provide the entry-point to the API, listing all the collections
// within it
Procedure ApiRoot
Handle hoResp hoColls hoColl
UChar[] ucaResp
Get Create (RefClass(cJsonObject)) to hoColls
Send InitializeJsonType of hoColls jsonTypeArray
// Do these six lines for each collection in the API:
Get Create (RefClass(cJsonObject)) to hoColl
Send InitializeJsonType of hoColl jsonTypeObject
Send SetMemberValue of hoColl "name" jsonTypeString "customers"
Send SetMemberValue of hoColl "href" jsonTypeString (BaseURL(Self) + "/customers")
Send AddMember of hoColls hoColl
Send Destroy of hoColl
// Create the response object and add the collections array to it:
Get Create (RefClass(cJsonObject)) to hoResp
Send InitializeJsonType of hoResp jsonTypeObject
Send SetMember of hoResp "collections" hoColls
Send Destroy of hoColls
// Return the serialized response object:
Get StringifyUtf8 of hoResp to ucaResp
Send Destroy of hoResp
Send OutputUChar ucaResp
End_Procedure
//
// The worker procedures to perform the GET (to the customer collection),
// GET (to a specific customer instance), POST (to the customer collection)
// to create a new row, PATCH (to update an existing instance) and DELETE
// (to delete an instance)
//
Procedure CustomerList
Handle hoResp hoCusts hoCust
UChar[] ucaResp
String sVal
Get Create (RefClass(cJsonObject)) to hoCusts
Send InitializeJsonType of hoCusts jsonTypeArray
Send Find of oCustomer_DD FIRST_RECORD Index.1
While (Found)
Get Create (RefClass(cJsonObject)) to hoCust
Send InitializeJsonType of hoCust jsonTypeObject
Get Field_Current_Value of oCustomer_DD Field Customer.Customer_Number to sVal
Send SetMemberValue of hoCust "Customer_Number" jsonTypeInteger sVal
Get Field_Current_Value of oCustomer_DD Field Customer.Name to sVal
Send SetMemberValue of hoCust "Name" jsonTypeString sVal
// More detail in the list can be provided by adding lines like the
// two above
// To be RESTful, we should provide links to our resources. The following
// pair of lines provides a link to each specific customer detail from the
// list - you might move this up to make it the first item for each entry
// in the list, rather than the last.
Get Field_Current_Value of oCustomer_DD Field Customer.Customer_Number to sVal
Send SetMemberValue of hoCust "href" jsonTypeString (BaseURL(Self) + "/customers/" + sVal)
Send AddMember of hoCusts hoCust
Send Destroy of hoCust
Send Find of oCustomer_DD NEXT_RECORD Index.1
Loop
Get Create (RefClass(cJsonObject)) to hoResp
Send InitializeJsonType of hoResp jsonTypeObject
Send SetMember of hoResp "customers" hoCusts
Send Destroy of hoCusts
Get StringifyUtf8 of hoResp to ucaResp
Send Destroy of hoResp
Send OutputUChar ucaResp
End_Procedure
Procedure CustomerDetail
Handle hoResp hoCust
UChar[] ucaResp
String sVal sField
Boolean bFound
Integer i iMax iType iJType iPrec
If not (FindCustomerFromRequest(Self)) ;
Procedure_Return
Get Create (RefClass(cJsonObject)) to hoCust
Send InitializeJsonType of hoCust jsonTypeObject
Get_Attribute DF_FILE_NUMBER_FIELDS of Customer.File_Number to iMax
For i from 1 to iMax
Get_Attribute DF_FIELD_NAME of Customer.File_Number i to sField
Get_Attribute DF_FIELD_TYPE of Customer.File_Number i to iType
Get Field_Current_Value of oCustomer_DD i to sVal
If (iType = DF_DATE) ;
Move (ConvertToClient(typeDate, sVal)) to sVal
If (iType = DF_DATETIME) ;
Move (ConvertToClient(typeDateTime, sVal)) to sVal
If ((iType = DF_ASCII) or ;
(iType = DF_DATE) or ;
(iType = DF_DATETIME) or ;
(iType = DF_TEXT)) ;
Move jsonTypeString to iJType
If (iType = DF_BCD) Begin
Get_Attribute DF_FIELD_PRECISION of Customer.File_Number i to iPrec
If (iPrec = 0) ;
Move jsonTypeInteger to iJType
Else ;
Move jsonTypeDouble to iJType
End
Send SetMemberValue of hoCust sField iJType sVal
Loop
Get Create (RefClass(cJsonObject)) to hoResp
Send InitializeJsonType of hoResp jsonTypeObject
Send SetMember of hoResp "customer" hoCust
Get StringifyUtf8 of hoResp to ucaResp
Send Destroy of hoResp
Send OutputUChar ucaResp
End_Procedure
Procedure CreateCustomer
Handle hoResp
UChar[] ucaResp
Boolean bErr
Send Clear of oCustomer_DD
If not (UpdateCustomerFromJson(Self)) ;
Procedure_Return
Get Validate_Save of oCustomer_DD to bErr
If not bErr Begin
Move False to Err
Send Request_Save of oCustomer_DD
Move (Err) to bErr
End
Get Create (RefClass(cJsonObject)) to hoResp
Send InitializeJsonType of hoResp jsonTypeObject
Send SetMemberValue of hoResp "Operation" jsonTypeString "Create Customer"
Send SetMemberValue of hoResp "Result" jsonTypeString (If(bErr, "failed", "succeesed"))
If not bErr ;
Send SetMemberValue of hoResp "New Customer_Number" jsonTypeInteger Customer.Customer_Number
Get StringifyUtf8 of hoResp to ucaResp
Send Destroy of hoResp
Send OutputUChar ucaResp
End_Procedure
Procedure UpdateCustomer
Handle hoResp
UChar[] ucaResp
Boolean bErr
If not (FindCustomerFromRequest(Self)) ;
Procedure_Return
If not (UpdateCustomerFromJson(Self)) ;
Procedure_Return
Get Validate_Save of oCustomer_DD to bErr
If not bErr Begin
Move False to Err
Send Request_Save of oCustomer_DD
Move (Err) to bErr
End
Get Create (RefClass(cJsonObject)) to hoResp
Send InitializeJsonType of hoResp jsonTypeObject
Send SetMemberValue of hoResp "Operation" jsonTypeString "Update Customer"
Send SetMemberValue of hoResp "Result" jsonTypeString (If(bErr, "failed", "succeeded"))
Get StringifyUtf8 of hoResp to ucaResp
Send Destroy of hoResp
Send OutputUChar ucaResp
End_Procedure
Procedure DeleteCustomer
Handle hoResp
UChar[] ucaResp
Boolean bErr
If not (FindCustomerFromRequest(Self)) ;
Procedure_Return
Get Validate_Delete of oCustomer_DD to bErr
If not bErr Begin
Move False to Err
Send Request_Delete of oCustomer_DD
Move (Err) to bErr
End
Get Create (RefClass(cJsonObject)) to hoResp
Send InitializeJsonType of hoResp jsonTypeObject
Send SetMemberValue of hoResp "Operation" jsonTypeString "Delete Customer"
Send SetMemberValue of hoResp "Result" jsonTypeString (If(bErr, "failed", "scuceeded"))
Get StringifyUtf8 of hoResp to ucaResp
Send Destroy of hoResp
Send OutputUChar ucaResp
End_Procedure
// If we are using "Basic Authentication", this function will validate that
// the username and password passed with the request are valid in the
// WebAppUser table
Function ValidateBasic Returns Boolean
String sCreds
String[] asCreds
Integer iLen
Address pAddr
Boolean bOK bValid
Move False to bValid
Get HttpRequestHeader "Authorization" to sCreds
// Check that we have the right kind of authorization
If (Left(Uppercase(sCreds), 6) = "BASIC ") Begin
Move (Right(sCreds, (Length(sCreds) - 6))) to sCreds
// base64 decode the credentials string:
Move (Length(sCreds)) to iLen
Move (Base64Decode(AddressOf(sCreds), &iLen)) to pAddr
Move (Repeat(Character(0), iLen)) to sCreds
Move (MemCopy(AddressOf(sCreds), pAddr, iLen)) to bOK
Move (Free(pAddr)) to bOK
// Split username and password:
Move (StrSplitToArray(sCreds, ":")) to asCreds
// Only if there are exactly two parts:
If (SizeOfArray(asCreds) = 2) Begin
// Is it a vaild user?
Clear WebAppUser
Move asCreds[0] to WebAppUser.LoginName
Find eq WebAppUser by Index.1
If (Found) Begin
Move (asCreds[1] = WebAppUser.Password) to bValid
End
End
End
If not bValid Begin
// Deny access, but tell invoker that we are using basic auth:
Send AddHttpResponseHeader "WWW-Authenticate" ;
('Basic realm="' + psRealm(Self) + '"')
Send SetResponseStatus 401 "Unauthorized"
End
Function_Return bValid
End_Function
// If we are using "Bearer Authentication" (in this simle case with an
// API key, issued to users), this will validate it
Function ValidateBearer Returns Boolean
String sKey sPassed
Boolean bOK
Move False to bOK
Get psApiKey to sKey
Get HttpRequestHeader "Authorization" to sPassed
// Check that we have the right kind of authorization
If (Uppercase(Left(sPassed, 6)) = "BEARER") Begin
Move (Right(sPassed, (Length(sPassed) - 7))) to sPassed
Move (sPassed = sKey) to bOK
End
If not bOK ;
Send SetResponseStatus 401 "Unauthorized"
Function_Return bOK
End_Function
// OnPreRequest is called on every request, prior to other processing.
// We will use it to work out whether to allow access for a request.
Procedure OnPreRequest String sVerb String sPath
Integer iAuthType
Set pbAllowAccess to False
Get peAuthType to iAuthType
If (iAuthType = C_authTypeBasic) ;
Set pbAllowAccess to (ValidateBasic(Self))
Else If (iAuthType = C_authTypeBearer) ;
Set pbAllowAccess to (ValidateBearer(Self))
Else ;
Set pbAllowAccess to True
End_Procedure
// This is the main procedure that works out how to handle (or reject) requests
Procedure OnHttpRequest String sVerb String sPath String sContentType String sAcceptType Integer iSize
String[] asPathParts
UChar[] ucaResp
Integer iParts
If not (pbAllowAccess(Self)) ;
Procedure_Return // The return ststus will already have been set
// elsewhere - just exit
// All of our responses will be in JSON (if they work), so we can set
// the response content type here
Send AddHttpResponseHeader "Content-type" "application/json"
// Need to do this to mark response as not cacheable
Send AddHttpResponseHeader "Cache-Control" "no-cache"
// We parse the URL into parts and store those for later use as well as
// using them in the Case statement below
Move (StrSplitToArray(sPath, "/")) to asPathParts
Move (SizeOfArray(asPathParts)) to iParts
Set pasPathParts to asPathParts
Case Begin
Case ((sVerb = "GET") and ;
(sPath = ""))
Send ApiRoot
Case Break
Case ((sVerb = "GET") and ;
(iParts = 2) and ;
(asPathParts[1] = "customers"))
Send CustomerList
Case Break
Case ((sVerb = "GET") and ;
(iParts = 3) and ;
(asPathParts[1] = "customers") and ;
(asPathParts[2] <> ""))
Send CustomerDetail
Case Break
Case ((sVerb = "POST") and ;
(iParts = 2) and ;
(asPathParts[1] = "customers"))
Send CreateCustomer
Case Break
Case ((sVerb = "PATCH") and ;
(iParts = 3) and ;
(asPathParts[1] = "customers") ;
and (asPathParts[2] <> ""))
Send UpdateCustomer
Case Break
Case ((sVerb = "DELETE") and ;
(iParts = 3) and ;
(asPathParts[1] = "customers") and ;
(asPathParts[2] <> ""))
Send DeleteCustomer
Case Break
Case Else
Send SetResponseStatus 400 "Bad Request"
Case End
End_Procedure
End_Object
// This procedure (which is called immediately) allows us to set up the
// authentication type (NONE, BASIC or BEARER) to use, and in the case
// of BEARER, also set the API key, dynamically from parameters passed
// to the application on the command line, rather than hard-coding the
// properties involved.
Procedure SetAuthDetails
Handle hoCmdLine
Integer iNumArgs iArg
String[] asArgs
Get phoCommandLine of ghoApplication to hoCmdLine
Get CountOfArgs of hoCmdLine To iNumArgs
For iArg from 1 to iNumArgs
Move (StrSplitToArray(Argument(hoCmdLine, iArg), "=")) to asArgs
If (SizeOfArray(asArgs) = 2) Begin
Move (Uppercase(asArgs[0])) to asArgs[0]
If (asArgs[0] = "AUTH") Begin
Move (Uppercase(asArgs[1])) to asArgs[1]
If (asArgs[1] = "BASIC") ;
Set peAuthType of oCustomerService to C_authTypeBasic
Else If (asArgs[1] = "BEARER") ;
Set peAuthType of oCustomerService to C_authTypeBearer
Else ;
Set peAuthType of oCustomerService to C_authTypeNone
End
If (asArgs[0] = "APIKEY") Begin
Set psApiKey of oCustomerService to asArgs[1]
End
End
Loop
End_Procedure
Send SetAuthDetails
You should be able to copy and paste this code to see it in operation, as follows:
Your workspace should contain the tables Customer (perhaps copied from the WebOrder sample) and WebAppUser. (You may find that if you have only copied the Data and DDSrc directories from the samples that you need to comment out the line "Use CustomerWebLookup.wo" in the cCustomerDataDictionary.dd file in order to get things to compile.)
If your workspace does not already have a WebApp, in the Studio use File --> New --> Project --> Basic Web Project (or Desktop Web Project, if you prefer) to create one first.
Then with the WebApp as the current project (selector top-left on the Studio tool-bar by default), use File --> New --> Web Object --> WebHttpHandler, calling the object oCustomerService; then select all (CTRL-A) and paste the code above over the existing contents.
If you then compile and run the WebApp (F5), and change the URL in the web browser (Firefox is best for this, as it displays the returned JSON in the nicest format), replacing "Index.html" with "customerAPI" (the psPath setting in the code) you should see the output produced by the "ApiRoot" procedure, which will (at this point) have a link to a single collection: "customers". Clicking (in Firefox, at least) on the "href" value for that should then display a list of customers, each of which should in turn have an "href" element, clicking on which will take you to the details for that customer (the instance).
To do more than simply display (i.e. GET) data, you will have to use a tool such as Postman (click the "Get Started" button, then the "Download" button, selecting the version - 32 or 64 bit - as appropriate for your machine, saving and then running the downloaded file) or the RESTTester tool detailed in the Consuming RESTful Services in DataFlex article.
To create new customer rows you will need to use POST to the customers collection (e.g.: .../customerAPI/customers), passing the required JSON in the request body (in Postman go to the request "Body" tab and select "raw" just below that). At a minimum that should be something like:
{
"Name": "My test customer",
"EMail_Address": "customer@sample.org"
}
To modify (update) an existing customer you will need to use PATCH to that customer's instance (e.g.: .../customerAPI/customers/101), passing the JSON to update it with, e.g.:
{
"Address": "My house in my street",
"Comments": "Where I live"
}
To delete a customer (which in most cases will be prevented by the Data Dictionary if that customer has orders attached to it) you will need to use DELETE to that customer's instance (e.g.: .../customerAPI/customers/101).
Security
The above sample has two different security mechanisms built in, although by default neither of them are active.
To activate them you should pass parameters on the command line (as a nicer alternative to hard-coding them) - these will be picked up by that "SetAuthDetails" procedure at the end of the example. This can be done in the WebApp Administrator. In that, select the application in question and right-click it, choosing the "Web Application Properties" option. In the "Parameters" box on the "General" tab enter the required parameters. (You will need to restart the web application for your changes to take effect: File --> Restart Web Application.)
To set up the same thing for debug-running in the Studio, do Project --> Project Properties <WebApp.src> and in the "General" tab enter the parameters you require in the "Parameters" box.
(Note: parameters are separated from each other by spaces.)
To use an authorization type of "Basic Auth" (username and password) pass the parameter "Auth=Basic", while to use a Bearer token use "Auth=Bearer ApiKey=WhateverKeyYouWantToUse". If you use bearer tokens you will not be able to access the API in a browser any more, however all browsers understand Basic Auth and will prompt you for credentials, which they will then "remember" for you (to make them "forget" those credentials again, do Ctrl-Shift-Del in the browser and select "Active Logins" in Firefox, or "Other Site Data" in Chrome - I can't work out how to do it in IE).
Of course, either of those authorization mechanisms rely on requests being made over a secure connection (HTTPS), so for deployment they absolutely must only be used in an environment where that is enforced, otherwise the passwords or API keys being employed will be exposed to any bad guys listening in on the connection.