Salesforce

How to store JSON from an HTTP service in a temp-table

« Go Back

Information

 
TitleHow to store JSON from an HTTP service in a temp-table
URL NameHow-to-store-JSON-from-an-HTTP-service-in-a-temp-table
Article Number000183784
EnvironmentProduct: OpenEdge
Version: 11.5.1, 11.6
OS: All supported platforms
Other: JSON
Question/Problem Description
How to retrieve JSON data from an external service using the ABL HTTP client, and store it in a temp-table using the Progress.Json.ObjectModel classes.

Example code to retrieve and store JSON that was not produced by the ABL WRITE-JSON() method in a round-trip application.
Steps to Reproduce
Clarifying Information
Error Message
Defect Number
Enhancement Number
Cause
Resolution
The following ABL code sample retrieves a 5-day weather forecast from the AccuWeather APIs service.

The ABL HTTP client classes are used to retrieve forecast data formatted in JSON from the service.

The JSON returned by the AccuWeather service is not formatted in such a way that the WRITE-JSON() method of the temp-table handle can be used to infer the structure needed to store the data. Specifically, the JSON consists of a simple string containing two top-level objects that are not elements of an array. In this case, the Progress.Json.ObjectModel classes may be used to unpack the JsonObject returned by the HTTP client, and store the data in the individual temp-table fields.

In order to use this technique, the structure of the JSON returned from a particular service must be known in advance, and the temp-table schema must be defined or created in the ABL code rather than inferred as with WRITE-JSON().
 
&GLOBAL-DEFINE FORECAST_DAYS    5
&GLOBAL-DEFINE FORECAST_URL     http://dataservice.accuweather.com/forecasts/v1/daily/5day/01730?apikey=<your API key here>

USING Progress.Lang.Object.
USING OpenEdge.Net.HTTP.RequestBuilder.
USING OpenEdge.Net.HTTP.IHttpRequest.
USING OpenEdge.Net.HTTP.ClientBuilder.
USING OpenEdge.Net.HTTP.IHttpResponse.
USING Progress.Json.ObjectModel.*.

/* HTTP client objects */
DEFINE VARIABLE oRequest    AS IHttpRequest     NO-UNDO.
DEFINE VARIABLE oResponse   AS IHttpResponse    NO-UNDO.
DEFINE VARIABLE oEntity     AS Object           NO-UNDO.

/* JSON objects for manipulating values. Objects can be reused for values
    that are not nested. For nested objects, each level must be stored in
    its own object. Objects and arrays are different types (JsonObject and
    JsonArray respectively. Note that the structure of the JSON must be
    known in advance to work with JSON when it is not in a format that the
    ABL can infer (such as the output of a previous WRITE-JSON() method).
    */
/* Top level JSON object */
DEFINE VARIABLE oForecast       AS JsonObject   NO-UNDO.
/* Lower level JSON objects can be reused at the same level. How many are
    needed (that is, how deep is the nesting) must be known in advance.
     */
DEFINE VARIABLE oNestObject1    AS JsonObject   NO-UNDO.
DEFINE VARIABLE oNestObject2    AS JsonObject   NO-UNDO.
DEFINE VARIABLE oNestObject3    AS JsonObject   NO-UNDO.
DEFINE VARIABLE oNestArray1     AS JsonArray    NO-UNDO.
DEFINE VARIABLE oNestArray2     AS JsonArray    NO-UNDO.

/* Variables for iterating through JsonArrays */
DEFINE VARIABLE i       AS INTEGER      NO-UNDO.
DEFINE VARIABLE j       AS INTEGER      NO-UNDO.
DEFINE VARIABLE iLength AS INTEGER      NO-UNDO.
DEFINE VARIABLE cList   AS CHARACTER    NO-UNDO.

/* If the JSON is not in a format similar to what is produced by the ABL
    WRITE-JSON() method, the temp-table schema cannot be inferred by
    READ-JSON(). Instead, the structure of the JSON must be
    known in advance and the temp-table must be defined or created dynamically
    with a schema that can store it.
    */
DEFINE TEMP-TABLE ttForecast NO-UNDO
    FIELD EffectiveDate                 AS DATETIME
    FIELD EffectiveEpochDate            AS INTEGER      FORMAT '9999999999'
    FIELD Severity                      AS INTEGER
    FIELD ForecastText                  AS CHARACTER    FORMAT 'X(30)'
    FIELD Category                      AS CHARACTER    FORMAT 'X(30)'
    FIELD EndDate                       AS DATETIME
    FIELD EndEpochDate                  AS INTEGER      FORMAT '9999999999'
    FIELD MobileLink                    AS CHARACTER    FORMAT 'X(78)'
    FIELD Link                          AS CHARACTER    FORMAT 'X(78)'
    FIELD DailyForecastDate             AS DATETIME     EXTENT {&FORECAST_DAYS}
    FIELD DailyForecastEpochDate        AS INTEGER      EXTENT {&FORECAST_DAYS} FORMAT '9999999999'
    FIELD DailyForecastTempMinValue     AS DECIMAL      EXTENT {&FORECAST_DAYS}
    FIELD DailyForecastTempMinUnit      AS CHARACTER    EXTENT {&FORECAST_DAYS}
    FIELD DailyForecastTempMinUnitType  AS INTEGER      EXTENT {&FORECAST_DAYS}
    FIELD DailyForecastTempMaxValue     AS DECIMAL      EXTENT {&FORECAST_DAYS}
    FIELD DailyForecastTempMaxUnit      AS CHARACTER    EXTENT {&FORECAST_DAYS}
    FIELD DailyForecastTempMaxUnitType  AS INTEGER      EXTENT {&FORECAST_DAYS}
    FIELD DailyForecastDayIcon          AS INTEGER      EXTENT {&FORECAST_DAYS}
    FIELD DailyForecastDayIconPhrase    AS CHARACTER    EXTENT {&FORECAST_DAYS} FORMAT 'X(30)'
    FIELD DailyForecastNightIcon        AS INTEGER      EXTENT {&FORECAST_DAYS}
    FIELD DailyForecastNightIconPhrase  AS CHARACTER    EXTENT {&FORECAST_DAYS} FORMAT 'X(30)'
    FIELD DailyForecastSources          AS CHARACTER    EXTENT {&FORECAST_DAYS} FORMAT 'X(78)'
    FIELD DailyForecastMobileLink       AS CHARACTER    EXTENT {&FORECAST_DAYS} FORMAT 'X(78)'
    FIELD DailyForecastLink             AS CHARACTER    EXTENT {&FORECAST_DAYS} FORMAT 'X(78)'
    .

/* Make request to service */
oRequest = RequestBuilder:Get("{&FORECAST_URL}")
    :Request.
oResponse = ClientBuilder:Build():Client:EXECUTE(oRequest).

IF oResponse:StatusCode <> 200
    THEN DO:
        MESSAGE
            'ERROR: Cannot retrieve forecast from service'
            SKIP 'HTTP Status Code:' oResponse:StatusCode
            SKIP oResponse:StatusReason
            VIEW-AS ALERT-BOX.
        RETURN ERROR.
    END.

/* Get payload and verify that it is JSON */
oEntity = oResponse:Entity.
IF TYPE-OF(oEntity, JsonObject)
    THEN oForecast = CAST(oEntity, JsonObject).
    ELSE DO:
        MESSAGE
            'ERROR: Cannot understand response from service'
            VIEW-AS ALERT-BOX.
        RETURN ERROR.
    END.

/* Create the temp-table record to store the Json content. */
EMPTY TEMP-TABLE ttForecast.
CREATE ttForecast.

/* Store the Headline object in a separate JsonObject, so the simple values it
    contains can be retrieved individually.
    */
oNestObject1 = oForecast:GetJsonObject('Headline').
ASSIGN
    ttForecast.EffectiveDate        = oNestObject1:GetDatetime('EffectiveDate')
    ttForecast.EffectiveEpochDate   = oNestObject1:GetInteger('EffectiveEpochDate')
    ttForecast.Severity             = oNestObject1:GetInteger('Severity')
    ttForecast.ForecastText         = oNestObject1:GetJsonText('Text')
    ttForecast.Category             = oNestObject1:GetJsonText('Category')
    ttForecast.EndDate              = oNestObject1:GetDatetime('EndDate')
    ttForecast.EndEpochDate         = oNestObject1:GetInteger('EndEpochDate')
    ttForecast.MobileLink           = oNestObject1:GetJsonText('MobileLink')
    ttForecast.Link                 = oNestObject1:GetJsonText('Link')
    .

/* Store the DailyForecasts array in a JsonArray. Iterate through the array,
    reusing the oNestObject1 variable to store each top-level object. Note
    that the array length could be obtained dynamically with the
    JsonArray:Length property, but there is no need here; the temp-table where
    we are storing the values cannot contain indeterminate arrays, so it is
    necessary to know in advance how many forecast days there will be.
    */
oNestArray1 = oForecast:GetJsonArray('DailyForecasts').

DO i = 1 TO {&FORECAST_DAYS}:
    oNestObject1 = oNestArray1:getJsonObject(i).

    ASSIGN
        ttForecast.DailyForecastDate[i]         = oNestObject1:GetDatetime('Date')
        ttForecast.DailyForecastEpochDate[i]    = oNestObject1:GetInteger('EpochDate')
        .
        
    /* Store the temperatures in a second JsonObject, because the first one
        is still being used for the entire daily forecast.
        */
    oNestObject2 = oNestObject1:GetJsonObject('Temperature').

    /* Store the Minimum temperature in a third JsonObject, because the
        second one is still being used for the entire temperature set.
        */
    oNestObject3 = oNestObject2:GetJsonObject('Minimum').
    ASSIGN
        ttForecast.DailyForecastTempMinValue[i]     = oNestObject3:GetDecimal('Value')
        ttForecast.DailyForecastTempMinUnit[i]      = oNestObject3:GetJsonText('Unit')
        ttForecast.DailyForecastTempMinUnitType[i]  = oNestObject3:GetInteger('UnitType')
        .

    /* Min temp is now stored in the temp-table, so the oNestObject3
        variable can be reused for the max temp.
        */
    oNestObject3 = oNestObject2:GetJsonObject('Maximum').
    ASSIGN
        ttForecast.DailyForecastTempMaxValue[i]     = oNestObject3:GetDecimal('Value')
        ttForecast.DailyForecastTempMaxUnit[i]      = oNestObject3:GetJsonText('Unit')
        ttForecast.DailyForecastTempMaxUnitType[i]  = oNestObject3:GetInteger('UnitType')
        .

    /* Done storing temps, so reuse oNestObject2 for Day and Night objects.
        */
    oNestObject2 = oNestObject1:GetJsonObject('Day').
    ASSIGN
        ttForecast.DailyForecastDayIcon[i]          = oNestObject2:GetInteger('Icon')
        ttForecast.DailyForecastDayIconPhrase[i]    = oNestObject2:GetJsonText('IconPhrase')
        .

    oNestObject2 = oNestObject1:GetJsonObject('Night').
    ASSIGN
        ttForecast.DailyForecastNightIcon[i]        = oNestObject2:GetInteger('Icon')
        ttForecast.DailyForecastNightIconPhrase[i]  = oNestObject2:GetJsonText('IconPhrase')
        .

    /* Sources is an array so presumably there can be more than one source,
        although the AccuWeather API does not say specifically. Since the
        values are short, the number is unknown, and ABL temp-table fields
        cannot be nested, a CHARACTER field containing a comma-separated list is
        a good implementation for Sources in the temp-table. */
    oNestArray2 = oNestObject1:GetJsonArray('Sources').
    ASSIGN
        iLength     = oNestArray2:LENGTH
        cList       = ''
        .

    DO j = 1 TO iLength:
        IF j > 1
            THEN cList = cList + ','.
        cList = cList + oNestArray2:GetJsonText(j).
    END.

    ttForecast.DailyForecastSources[i] = cList.

    /* Store the remaining elements of the daily forecast */
    ASSIGN
        ttForecast.DailyForecastMobileLink[i]   = oNestObject1:GetJsonText('MobileLink')
        ttForecast.DailyForecastLink[i]         = oNestObject1:GetJsonTExt('Link')
        .

END.

/* Sample code only: Display the results. */
DISPLAY ttForecast.

RETURN.
Workaround
Notes

Reference to Other Documentation

AccuWeather APIs
http://developer.accuweather.com/

AccuWeather API Reference
http://apidev.accuweather.com/developers/forecasts

OpenEdge Development: Programming Interfaces, Chapter 6, "Making HTTP(S) requests from ABL applications"
OpenEdge Development: Working with JSON
OpenEdge Development: ABL Reference, Chapter 6, "Class, Interface, and Enumeration Reference"

Progress Articles:
 How to read a JSON object into a temp-table
 Does JSON serialization of a ProDataSet support representing a single nested child record as a simple string instead of an array ?
 GetJsonObject() is not referenced as a method of Progress.Json.ObjectModel.JsonObject class definition
How to set SSL Protocols and Ciphers to use in the http client?
Getting started with HTTP client for OpenEdge
 

Keyword Phrase
Last Modified Date9/11/2023 11:37 AM

Powered by