Questa non vuole essere una guida completa per la gestione dei JSON con IBM i – AS400 ma semplicemente una raccolta di FAQ, trucchi e tecniche utilizzate nell’esperienza quotidiana oppure “rubate” da qualche sito internet o forum.
Vuole essere una guida in continua evoluzione e per questo invito anche voi a farla crescere utilizzando i commenti all’articolo o segnalandoci le vostre esperienze.
Index
Sempre lui, sempre il grande Scott Klement dopo averci regalato YAJL e poi YAJL-INTO per la gestione dei JSON nei nostri programmi RPG arriva con un fantastcio comando YAJLGEN, compreso nel prodotto Open Source YAJL: https://www.scottklement.com/yajl/
Con YAJLGEN è possibile costruire la DS per la funzione DATA-INTO di RPG partendo da un qualsia JSON salvato nell’IFS. Non è fantastico? Prendo un JSON, lancio YAJLGEN che mi crea la DS per RPG e un template di chiamata alla funzione DATA-INTO e il lavoro è fatto …. da lì in avanti ho la mia DS compilata e faccio ciò che voglio dei dati letti dal documento JSON.
Vediamo un esempio: prendo un JSON anche abbastanza complesso, con diverse nidificazioni (preso dal sito opensource di Adobe) :
{"root":[ { "id": "0001", "type": "donut", "name": "Cake", "ppu": 0.55, "batters": { "batter": [ { "id": "1001", "type": "Regular" }, { "id": "1002", "type": "Chocolate" }, { "id": "1003", "type": "Blueberry" }, { "id": "1004", "type": "Devil's Food" } ] }, "topping": [ { "id": "5001", "type": "None" }, { "id": "5002", "type": "Glazed" }, { "id": "5005", "type": "Sugar" }, { "id": "5007", "type": "Powdered Sugar" }, { "id": "5006", "type": "Chocolate with Sprinkles" }, { "id": "5003", "type": "Chocolate" }, { "id": "5004", "type": "Maple" } ] }, { "id": "0002", "type": "donut", "name": "Raised", "ppu": 0.55, "batters": { "batter": [ { "id": "1001", "type": "Regular" } ] }, "topping": [ { "id": "5001", "type": "None" }, { "id": "5002", "type": "Glazed" }, { "id": "5005", "type": "Sugar" }, { "id": "5003", "type": "Chocolate" }, { "id": "5004", "type": "Maple" } ] }, { "id": "0003", "type": "donut", "name": "Old Fashioned", "ppu": 0.55, "batters": { "batter": [ { "id": "1001", "type": "Regular" }, { "id": "1002", "type": "Chocolate" } ] }, "topping": [ { "id": "5001", "type": "None" }, { "id": "5002", "type": "Glazed" }, { "id": "5003", "type": "Chocolate" }, { "id": "5004", "type": "Maple" } ] } ] }
Ho salvago il documento JSON nell’IFS del mio IBM i … quindi ho chiamato la funzione YAJLGEN:
YAJLGEN STMF('/myjson/jsonexample3.json') SRCFILE(FAQ400/SRC) SRC MBR(JSONEXAM)
Ecco il mio sorgente JSONEXAM creato dalla funzione YAJLGEN: compilo, metto in debug … fantastico! … la mia DS bella e compilata!
ctl-opt dftactgrp(*no); readTheJson(); *inlr = *on; // FIXME: // - The field lengths (varchar/packed) are guesses // and should be adjusted based on your business rules. // - The array lengths (dim keywords) are also guesses // and should be adjusted based on your business rules dcl-proc readTheJson; dcl-ds jsonDoc qualified; num_ROOT int(10) inz(0); dcl-ds ROOT dim(100); ID varchar(4) inz(''); TYPE varchar(5) inz(''); NAME varchar(4) inz(''); PPU packed(3: 2) inz(0); dcl-ds BATTERS; num_BATTER int(10) inz(0); dcl-ds BATTER dim(4); ID varchar(4) inz(''); TYPE varchar(30) inz(''); end-ds; end-ds; num_TOPPING int(10) inz(0); dcl-ds TOPPING dim(7); ID varchar(4) inz(''); TYPE varchar(4) inz(''); end-ds; end-ds; end-ds; dcl-s ifsPathName varchar(5000); ifsPathName = '/myjson/jsonexample3.json'; data-into jsonDoc %DATA( ifsPathname : 'doc=file case=convert countprefix=num_') %PARSER( 'YAJLINTO' : '{ "document_name": "jsonDoc", + "number_prefix": "YAJL_" }'); end-proc;
Alcune attenzioni:
Purtroppo fino ad oggi, Agosto 2019, Scott non ha previsto un vero e proprio versioning dell’ottima libreria YAJL ( https://www.scottklement.com/yajl/ ) per la gestione dei JSON in RPG … che invece tiene regolarmente aggiornato con nuove funzionalità. Come suggerito anche dallo stesso Scott Klement, per vedere la versione fare un:
DSPSRVPGM SRVPGM(YAJL)
La versione ad oggi, Agosto 2019, disponibile è stata compilata il 19/07/2019
Se avete una versione più vecchia l’aggiornamento lo si può fare “passando tranquillamente sopra” la vecchia libreria … quindi, dopo aver scaricato il SAVF e portanto dentro l’IFS seguendo le istruzione del Readme.txt di Scott … eseguite il comando RSTLIB con una opzione in più:
RSTOBJ OBJ(*ALL) SAVLIB(QTEMP) DEV(*SAVF) RSTLIB(YAJL) SAVF(QGPL/YAJLLIB72) ALWOBJDIF(*ALL)
Quando abbiamo a che fare con un JSON con diversi livelli di Arrays non è sempre semplicissimo farne un parsing corretto.
Prendiamo questo esempio di JSON trovato sul forum Midrange.com: un JSON con i dati nidificati per le distanze di 4 differenti indirizzi da un punto, sembra un ritorno da Google Maps o qualche altro servizio simile di geo-localizzazione:
{ "destination_addresses" : [ "1234 Main St, La Crescent, MN 55947, USA", "1742 Park Ave, La Crosse, WI 54601, USA", "1210 S 10th St, La Crosse, WI 54601, USA", "9876 W 52nd Ave, Onalaska, WI 854650 USA" ], "origin_addresses" : [ "1500 Old Hickory Dr, La Crescent, MN 55947, USA" ], "rows" : [ { "elements" : [ { "distance" : { "text" : "18.3 mi", "value" : 29452 }, "duration" : { "text" : "29 mins", "value" : 1746 }, "status" : "OK" }, { "distance" : { "text" : "9.5 mi", "value" : 15308 }, "duration" : { "text" : "18 mins", "value" : 1056 }, "status" : "OK" }, { "distance" : { "text" : "3.8 mi", "value" : 6142 }, "duration" : { "text" : "9 mins", "value" : 534 }, "status" : "OK" }, { "distance" : { "text" : "19.3 mi", "value" : 31032 }, "duration" : { "text" : "22 mins", "value" : 1331 }, "status" : "OK" } ] } ], "status" : "OK" }
Se il file è caricato sul nostro IFS (esempio ‘/home/FAQ00/gmaps.json’ in una specifica directory possiamo farne il parsing con JSON_TABLE e qualche JOIN/CROSS JOIN:
select * from ( SELECT * from JSON_TABLE(get_clob_from_file('/home/FAQ00/gmaps.json'), '$' COLUMNS( origin_addresses VARCHAR(30) PATH '$.origin_addresses' ) ) as x ) cc cross join table ( SELECT ROW_NUMBER() OVER () rownumber, a.* from JSON_TABLE(get_clob_from_file('/home/FAQ00/gmaps.json'), '$.rows.elements' COLUMNS( distance_text VARCHAR(30) PATH '$.distance.text', distance_value VARCHAR(30) PATH '$.distance.value', duration_text VARCHAR(30) PATH '$.duration.text', duration_value VARCHAR(30) PATH '$.duration.value', statust VARCHAR(30) PATH '$.status') ) as a
) aa join table ( SELECT ROW_NUMBER() OVER () rownumber, b.* from JSON_TABLE(get_clob_from_file('/home/FAQ00/gmaps.json'), '$.destination_addresses' COLUMNS( Destination_addressess VARCHAR(30) PATH '$' ) ) as b ) bb on aa.rownumber=bb.rownumber;
Oppure, sfruttando la YAJLGEN di Scott Klement, creare un piccolo source RPG che con una DS e un DATA-INTO trasforma questo JSON in una normale DS nestata utilizzabile direttamente in RPG:
ctl-opt dftactgrp(*no); readTheJson(); *inlr = *on; // FIXME: // - The field lengths (varchar/packed) are guesses // and should be adjusted based on your business rules. // - The array lengths (dim keywords) are also guesses // and should be adjusted based on your business rules dcl-proc readTheJson; dcl-ds jsonDoc qualified; num_DESTINATION_ADDRESSES int(10) inz(0); DESTINATION_ADDRESSES varchar(40) inz('') dim(4); num_ORIGIN_ADDRESSES int(10) inz(0); ORIGIN_ADDRESSES varchar(47) inz('') dim(1); num_ROWS int(10) inz(0); dcl-ds ROWS dim(1); num_ELEMENTS int(10) inz(0); dcl-ds ELEMENTS dim(4); dcl-ds DISTANCE; TEXT varchar(7) inz(''); VALUE packed(5) inz(0); end-ds; dcl-ds DURATION; TEXT varchar(7) inz(''); VALUE packed(4) inz(0); end-ds; STATUS varchar(2) inz(''); end-ds; end-ds; STATUS varchar(2) inz(''); end-ds; dcl-s ifsPathName varchar(5000); ifsPathName = '/smedoc/UTE/DEPE/gmaps.json'; data-into jsonDoc %DATA( ifsPathname : 'doc=file case=convert countprefix=num_') %PARSER( 'YAJLINTO' : '{ "document_name": "jsonDoc", + "number_prefix": "YAJL_" }'); end-proc;
In alcuni casi potremmo avere a che fare con dei web services che tornano dei JSON non proprio nella forma normale:
{ key : value }
ma in una forma “array”
[{ key : value }, { key : value }, ...}]
In questo caso dobbiamo “ricostruire” il documento JSON prima di farlo elaborare da JSON_TABLE o altri servizi per JSONx
C’è un ottimo post di Scott Forstie dal titolo “JSON_TABLE and survival tips for shredding JSON with SQL“ che spiega bene come fare.
In questo repository di Git troviamo ottimi esempi di JSON e ILE RPG / SQL gentilmente messi a disposizione da Tim Fathers
Se costruiamo un JSON con le procedure della YAJL di Scott Klement possiamo ricavare il documento JSON costruito anche in una variabile tramite la chiamata delle procedura yajl_copyBufStr()
dcl-s jsonStr char(1024);
yajl_beginObj();
.
.
yajl_endObj();
jsonStr = yajl_copyBufStr();
yajl_genClose();
Se in realtà ci serve il JSON per chiamare un Web Service utilizzando le funzioni di HTTPAPI sempre di Scott Klement, possiamo anche semplicemente ricavare il puntatore e la lunghezza della stringa JSON
yajl_beginObj();
.
.
yajl_endObj();
rc = yajl_getBuf(jsonPtr:jsonLen);
rc = http_post( URL
: jsonPtr
: jsonLen
: response
: HTTP_TIMEOUT
: HTTP_USERAGENT
: 'application/json');
yajl_genClose();
Nel caso sopra otteniamo con yajl_getBuf(JsonPtr:JsonLen) il puntatore alla variabile e la sua lunghezza … che in fase di debug, se vogliamo vederne il contenuto potrebbe essere scomodo … anche se quando abbiamo un puntatore ad una variabile possiamo sempre vederne il contenuto con:
eval myPointer:c 10 // To display first 10 bytes addressed by myPointerc as a character string
or
eval myPointer:x 50 // to display the first fifty bytes addressed by myPointer in hex
Prendo spunto da una discussione sulla mailing-list di Midrange.com (Path and DATA-INTO and YAJLINTO) per vedere un esempio concreto di parsing di un JSON con diversi oggetti e array… con qualche consiglio di Scott Klement per ottimizzare le performance di DATA-INTO e YAJLINTO.
I sorgenti di questa FAQ, il documento JSON e altre info sono disponibili in questo repository GIST : Faq400Git/Data-Into-Multi-Objcets-Arrays
Guardiamo il JSON e cerchiamo di capirne la struttura:
{
..
"data": {
...
"Invoice": [
{
"CreatedTimestamp": "2019-12-04T22:30:32.766",
...
},
{
"CreatedTimestamp": "2019-12-04T22:30:32.766",
...
}
],
Vediamo che c’è un oggetto esterno senza nome (il JSON parte subito con una parentesi graffa) … poi vediamo un oggetto “data” con dentro, a sua volta, un oggetto “Invoice” che contiene un array di fatture con i dati.
In pratica le opzioni sopra indicate andiamo ad impostarle come nel pezzo di sorgente che trovate qui sotto
// Options
RPGOPTS=' path=json/data/Invoice'
+' doc=file'
+' case=any'
+' allowextra=yes'
+' allowmissing=yes';
YAJLOPTS='{"value_null":"0" '
+', "document_name": "json" '
+'}';
// Parsing JSON
data-into Invoice %DATA( ifsPathname : RPGOPTS)
%PARSER( 'YAJL/YAJLINTO' : YAJLOPTS);
Quando abbiamo un array dentro il JSON “[ {…},{…}]”, se stiamo analizzando il livello stesso dell’array, YAJLINTO ritorna anche il numero di elementi trovato nella PSDS del programma e di conseguenza recuperarlo dopo il DATA-INTO, come nell’esempio che segue la variabile numElements:
dcl-ds pgmStat psds;
numElements int(20) pos(372);
end-ds;
...
data-into Invoice %DATA( ifsPathname : RPGOPTS)
%PARSER( 'YAJL/YAJLINTO' : YAJLOPTS);
for i=1 to numElements;
string='InvoiceId(' +%editc(i:'Z')+ '): '
+invoice(i).InvoiceId;
...
endfor;
Nel post sul forum Scott Klement consiglia di utilizzare le opzioni “allowextra=yes e allowmissing=yes” solo se la nostra DS non rispecchia il nostro JSON: se abbiamo invece la certezza del JSON meglio creare la DS che rispecchia ed evitare quelle due opzioni che rallentano l’esecuzione della DATA-INTO.
Un altra cosa che troviamo spesso nelle opzioni di DATA-INTO è “countprefix=xxx” e poi nelle opzioni del parser “number_prefix”:”xxx” … queste due impostazioni servono quando si hanno degli elementi del JSON che iniziano come un numero, ad esempio { “30-day-outlook”: [ xxxx ] } … in questo caso il campo dovrà chiamarsi xxx_30-day-outlook nella nostra DS
Nei sorgenti del GIST trovate anche un esempio (XDATAINTO3_RPGLE) che esegue il parsing con DATA-INTO utilizzando una DS con tutti i livelli di oggetti presenti nel JSON (prestare attenzione che, in questo caso, il parser YAJLINTO noo tornerà più il numero di elementi trovati negli arrary perché l’elemento più esterno non è a sua volta un array!
Se ho un documento JSON già preparato … e lo voglio incapsulare dentro un altro JSON posso utilizzare la funzione YAJL_addPreformattedPtr.
Esempio:
YAJL_beginObj();
YAJL_addBool('success' : stdOutput.success);
YAJL_addChar('errCode' : stdOutput.errCode);
YAJL_addChar('message' : stdOutput.message);
YAJL_addPreformattedPtr('data'
: %addr(stdOutput.data:*DATA)
: %len(%trimr(stdOutput.data));
YAJL_endObj();
YAJL_copyBuf(CCSID
: %addr(jsonData)
: %size(jsonData)
: wRetLen);
YAJL_genClose();
---
Roberto De Pedrini
Faq400.com I primi di Aprile è uscita la "Spring Version" di ACS Access Client Solution, versione 1.1.9.5 Interessanti novità soprattutto in…
Se non vi bastava la ricca agenda delle sessioni del Common Europe Congress 2024, 3-6 Giugno Milano, ecco un altro…
Le funzioni di debug con Visual Studio Code sono disponibili da qualche tempo ma questa nuova versione 2.10.0 semplifica la…
A distanza di due anni e mezzo dal mio post Trasferire oggetti con ObjectConnect ed Enterprise Extender, sono finalmente riuscito…
Con un piccolo trucco anche una semplice istruzione SELECT può eseguire qualsiasi comando di sistema ! Vediamo come...
Una mini-guida a puntate per la configurazione, gestione, uso e risoluzione dei problemi di IBM i NetServer