Tre modi per consumare SOAP Web Services su IBM i

L’argomento “consumare Web Service da RPG e SQL” non è una cosa nuova … anzi, credo che ormai quasi tutte le aziende con IBM i si siano dovute confrontare con i Web Service, in pubblicazione o consumo. Il presente post vuole solo mettere un po’ d’ordine e presentare tre differenti modi per consumare un SOAP web service da IBM i: nella quarta parte del post vediamo anche un caso concreto di parsing di un XML abbastanza complesso, con differenti namespaces, livelli, tag ripetuti e altre amenità che possiamo incontrare nel mondo reale dei servizi.

Sintesi

In pratica il post si divide in 4 distinte parti:

  • Nella prima parte vediamo come consumare un Web Service SOAP con la tecnica del Axis Client IBM, sfruttando una interessante utility WSDL2RPG che, partendo appunto dal WSDL di un Web Service SOAP, genera un service program di interfaccia al WS setesso. Questo metodo è abbastanza semplice, ma più di una volta mi è capitato che wsdl2rpg fallisse nell’analizzare alcuni WSDL complessi.
  • Nella seconda parte andiamo ad utilizzare la classica HTTPAPI di Scott Klement … Santo Scott Klement … quante volte ci ha salvato con le sue utility o con i suoi esempi scopiazzati un po’ da tutti?
  • Arriviamo quindi ad una terza parte dove vediamo il consumo di un SOAP con HTTPPOSTCLOB SQL … che non solo permette di consumare dei REST di tipo post ma anche dei SOAP avendo cura di passare header e body di chiamata in modo corretto, rispettando i concetti di Envelope dei soap stessi.
  • C’è anche una quarta parte del post … dedicata ad un esempio concreto di parsing di un SOAP Response abbastanza complesso, estraendo dati da diversi livelli e namespaces… utilizzando sempre SQL con la funziona XMLTABLE.

Introduzione

Per fare in modo che anche voi possiate provare il servizio abbiamo preferito creare i nostri esempi sul più comune SOAP web service per IBM i: ConvertTemp, il servizio SOAP che viene creato per default per ogni HTTP Server che andiamo a creare dall’interfaccia IWS IBM i.

Andiamo, per prima cosa, a recuperare il WSDL del servizio ConvertTemp passando dalla gestione web di IWS (IBM Web Administration for i), raggiungibile solitamente dall’URL: http://<IP IBMi>:2001/HTTPAdmin

Dalle proprietà del servizio possiamo vedere indirizzo e porta di pubblicazione (URL del servizio) .

Con l’apposito link vediamo anche il WSDL, il documento XML che spiega le funzioni esposte dal servizio stesso, i parametri e il dettaglio della comunicazione HTTP.

Metodo 1 : wsdl2rpg, Axis Client

WSDL2RPG crea un service program che possiamo “bindare” nei nostri programmi per interfacciarsi al Web Service SOAP senza preoccuparci di XML ecc, eseguendo delle semplici chiamate a procedure con passaggio parametri abbastanza normale per un programmatore RPG.

Apriamo una console QSH ( ed eseguiamo il seguente comando, in modo da creare nella nostra libreria (nel mio caso FAQ400) un service program di interfacciamento, con l’opzione -o andiamo anche a definire una directory dell’IFS dove salvare i sorgenti del service program, dei moduli e degli “includes” RPG

/QIBM/ProdData/OS/WebServices/V1/client/BIN/wsdl2rpg.sh 
 -t90 -o/IFS/faq400/converttemp 
 -s/QSYS.LIB/FAQ400.LIB/convt.SRVPGM 
http://xxx.xxx.xxx.xxx:10010/web/services/ConvertTempService/ConvertTemp?wsdl

A questo punto non ci resta che scrivere il nostro programma … indicare le /COPY opportune per le procedures del service program e “consumare” il Web Service SOAP … ecco qui sotto l’intero sorgente di un programma RPG che riceve in input una temperatura in gradi Fahrenheit e la trasforma in gradi centigradi. Il service program creato con wsdl2rpg va messo in bind con il programma, oppure, come nel sorgente qui sotto, aggiunto ad una Bind-Directory da specificare nelle specifiche ctl-opt (H-spec)

        //-------------------------------------
        // This program will invoke a SOAP webservice
        // through a service program generated
        // with WSDL2RPG.SH utility:
        // STRQSH
        // /QIBM/ProdData/OS/WebServices/V1/client/BIN/wsdl2rpg.sh
        // -t90 -o/home/faq400/converttemp
        // -s/QSYS.LIB/FAQ400.LIB/convt.SRVPGM
        // http://xxx.xxx.xxx.xxx:port/web/services/ConvertTempService/ConvertTemp?wsdl
        //
        // Then I added this service program to my Bind Directory
        // ADDBNDDIRE BNDDIR(FAQ400/FAQ400SRV) OBJ((CONVT))
        //
        // Now I set my BNDDIR in ctl-opt
        // --------------------------------------------------------------------

        ctl-opt DFTNAME(F4WSDL01);
        ctl-opt BNDDIR('FAQ400SRV');

        /copy /home/faq400/ConvertTemp/ConvertTempServices.rpgleinc

        dcl-s  OutputText char(50);
        dcl-ds WsStub likeds(This_t);
        dcl-ds Input     likeds(CONVERTTEMPInput_t);
        dcl-ds Result    likeds(CONVERTTEMPResult_t) ;

        // *entry plist Temperature IN
        dcl-pi main extpgm('F4WSDL01');
          tempin char(32);
        END-PI;

       //--------------------------------------------------------------------
       // Web service logic. The code will attempt to invoke a Web
       // service in order to convert temperature in Fahrenheit to Celsius
       //and then display the results.
       //--------------------------------------------------------------------

         // Get a Web service stub. The host and port for the endpoint may need
         // to be changed to match host and port of Web service. Or you can pass
         // blanks and endpoint in the WSDL file will be used.
         clear WsStub;
         WsStub.endpoint =
         'http://172.17.238.46:10010'+
         '/web/services/ConvertTempService/ConvertTemp';
         clear input;
         Input.TEMPIN.value = %trim(TEMPIN);
         if (stub_create_ConvertTempServices(WsStub) = *ON);
           // Invoke the ConvertTemp Web service operation.
           if (stub_op_ConvertTemp0(WsStub:Input:Result) = *ON);
              OutputText = Input.TEMPIN.value + ' Fahrenheit is '
                         + Result.TEMPOUT.value + ' Celsius.';
           else;
              OutputText = WsStub.excString;
           endif;

           // Display results.
           dsply OutputText;

           // Destroy Web service stubs.
           stub_destroy_ConvertTempServices(WsStub);
         endif;

         *INLR=*ON;
         return;

Questo sorgente è disponibile su Github: https://github.com/Faq400Git/F4P0001/blob/master/F4P0001/F4P0001/F4WSDL01.RPGLE

Metodo 2, HTTPAPI di Scott Klement

Scott Klement ha fatto tantissimo per la Community IBM i nel tempo creando utility, documentazione e partecipando attivamente a forum, eventi e corsi di formazione. Fra le grandi cose che ha fatto c’è anche HTTPAPI, una libreria che permette di consumare Web Service REST e SOAP da RPG in modo molto semplice.

Se vogliamo, ad esempio, creare un programma RPG che richiami il nostro Web Service SOAP ConvertTemp, è sufficiente prendere spunto dal seguente sorgente (preso proprio da un esempio dello stesso Scott!):

       //-------------------------------------------------------
       // F4HTTPAPI: Consuming a SOAP web service
       //      through HTTPAPI utility by Scott Klement
       //
       //  Version 1.0
       // -------------------------------------------------

        ctl-opt option(*srcstmt:*nounref) dftactgrp(*no);
        ctl-opt BNDDIR('HTTPAPI') ;

        // Entry plist
        dcl-pi F4HTTPAPI1;
           TempinAlfa char(15);
        END-PI;


        /copy httpapi_h

        dcl-s UrlHost       varchar(200);
        dcl-s UrlEndPoint   varchar(200);
        dcl-s PostUrl       varchar(254);
        dcl-s PostData      varchar(32000) ;
        dcl-s PostResult    varchar(32000) ;
        dcl-s string char(30);
        dcl-s reply  char(10);
        dcl-s errorMsg char(256);

        // Input data
        dcl-s tempin packed(7:2);

        // Output data
        dcl-ds xmlout qualified;
             Tempout packed(7:2);
        END-DS;


        // Set CCSID 280 (ITA)
         exec sql
          CALL QSYS2.QCMDEXC('CHGJOB CCSID(280)');

        // Legge input
        monitor;
           tempin=%dec(tempinAlfa:7:2);
        on-error;
           tempin=40;
        ENDMON;

        UrlHost       ='http://172.17.238.46:10010';
        UrlEndPoint='/web/services/ConvertTempService/ConvertTemp';

        PostUrl=%trim(UrlHost)+%trim(UrlEndPoint);
        PostData=set_PostdataConvertTemp(Tempin);
        //PostHeader=get_PostHeader(%len(%trim(PostData)):'text/xml');

        // Call the SOAP web service
        // http_setOption('SoapAction': '"GetConversionRate"');
        clear errorMsg;
        clear xmlout;

        monitor;
          PostResult = http_string( 'POST': PostURL: PostData: 'text/xml');
        on-error;
          ErrorMsg=http_error();
          PostResult='<Result>Error</Result>';
        endmon;

        // Parse output
        monitor;
          xml-into xmlout %xml(postResult: 'case=any ns=remove +
             path=Envelope/Body/converttempResponse/return');
        on-error;
        endmon;

        // Reimposta CCSID predefinito dell'utente
         exec sql
          CALL QSYS2.QCMDEXC('CHGJOB CCSID(*USRPRF)');


        string='Tempin:'+%editc(tempin:'K');
        dsply string;
        string='Tempout:'+%editc(xmlout.tempout:'K');
        dsply string ;
        Dsply ( 'Press <Enter> to end program' ) ' ' reply;

        *inlr = *on ;


        //-------------------------------------------------------
        // Set Postdata ... SOAP Envelope
        //-------------------------------------------------------
        dcl-proc set_PostDataConvertTemp;
        dcl-pi   set_PostDataConvertTemp varchar(32000);
          TempF   packed(7:2) const;
        end-pi;
        dcl-s PostData varchar(32000);

        PostData=' '
        +'<soapenv:Envelope'
        +' xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"'
        +' xmlns:con="http://converttemp.wsbeans.iseries/">'
        +' <soapenv:Header/>'
        +'    <soapenv:Body>'
        +'      <con:converttemp>'
        +'         <arg0>'
        +'            <TEMPIN>$$TEMPF</TEMPIN>'
        +'         </arg0>'
        +'      </con:converttemp>'
        +'   </soapenv:Body>'
        +'</soapenv:Envelope>';

        // Set input temperature;
        PostData=%scanrpl('$$TEMPF':%editc(Tempf:'K'):PostData);

        return PostData;
       end-proc;

Questo sorgente è disponibile su Github: https://github.com/Faq400Git/F4P0001/blob/master/F4P0001/F4P0001/F4HTTPAPI1.SQLRPGLE

Metodo 3 .. SQL o SQL Embedded in RPG con HTTPPOSTCLOB

Le funzionalità HTTP del DB2 for i e le funzioni di gestione del XML rendono molto semplice il consumo di servizi SOAP (e REST naturalmente !) direttamente in SQL …

La funzione HTTPPOSTCLOB che andiamo ad utilizzare si aspetta un HEADER, un BODY e una URL del servizio … per capire come compilare gli XML di Header e Body vi consiglio di installare una delle tante utility che testano i Web Service da Windows o Mac … io personalmente utilizzo SOAPUi (http://www.soapui.org) … in questo modo diamo in pasto a SOAPui il WSDL e vediamo le varie Request e relativi parametri da utilizzare via HTTPPOSTCLOB.

Per poter riutilizzare e semplificare il lavoro lato RPG io tendenzialmente creo delle Stored procedure o delle UDF o UDTF per tornare i risultati dei Web Service come se fosse una normale SELECT su una tabella.

Ecco una UDF SQL che torna la temperatura convertita:

CREATE or replace function faq400.converttemp (
 i_fahrenheit CHAR(10))
 RETURNS varchar(50)
 LANGUAGE SQL
 BEGIN

 DECLARE v_header varchar (1000);
 DECLARE v_body varchar (1000);
 DECLARE v_url varchar (1000);


 declare v_data varchar (1000);
 DECLARE v_result varchar (50);


 -- Set default Envelope-Body
 SET v_body =
 '<soapenv:Envelope
 xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
 xmlns:con="http://converttemp.wsbeans.iseries/">
   <soapenv:Header/>
   <soapenv:Body>
      <con:converttemp>
         <arg0>
            <TEMPIN>$$$TEMPIN</TEMPIN>
         </arg0>
      </con:converttemp>
   </soapenv:Body>
</soapenv:Envelope>';

-- Replace the input variable in the body
set v_body=replace(v_body, '$$$TEMPIN', i_fahrenheit);


SET v_header =
 '<httpHeader>
 <header name ="content-type" value ="application/xml"/>
 </httpHeader>';


SET v_url =
 'http://172.17.238.46:10010/web/services/ConvertTempService/ConvertTemp';


select a. * into v_result
FROM
XMLTABLE (
xmlnamespaces ('http://schemas.xmlsoap.org/soap/envelope/' AS "soap",
                'http://converttemp.wsbeans.iseries/' as "ns2"),
'$ doc/soap:Envelope/soap:Body/*:converttempResponse/return'
PASSING
xmlparse (document SYSTOOLS.HTTPPOSTCLOB(v_url, v_header, v_body))
as "doc"
columns
TEMPOUT varchar (50) path 'TEMPOUT'
) as a;
 return v_result;

END;


--- Testing our UDF
select faq400.converttemp('42')
 from sysibm.sysdummy1;


--- Direct test
select a.*
FROM
XMLTABLE (
xmlnamespaces ('http://schemas.xmlsoap.org/soap/envelope/'
  AS "soap", 'http://converttemp.wsbeans.iseries/' as "ns2"),
'$ doc/soap:Envelope/soap:Body/*:converttempResponse/return'
PASSING
xmlparse (document SYSTOOLS.HTTPPOSTCLOB(
'http://172.17.238.46:10010/web/services/ConvertTempService/ConvertTemp',
'<httpHeader>
<header name="content-type" value="application/xml"/>
</httpHeader>',
'<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:con="http://converttemp.wsbeans.iseries/">
   <soapenv:Header/>
   <soapenv:Body>
      <con:converttemp>
         <arg0>
            <TEMPIN>80</TEMPIN>
         </arg0>
      </con:converttemp>
   </soapenv:Body>
</soapenv:Envelope>
'))
as "doc"
columns
TEMPOUT varchar (50) path 'TEMPOUT'
) as a;
 return v_result;

--
select * from table(
SYSTOOLS.HTTPPOSTCLOBVERBOSE(
'http://172.17.238.46:10010/web/services/ConvertTempService/ConvertTemp',
'<httpHeader>
<header name="content-type" value="application/xml"/>
</httpHeader>',
'<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:con="http://converttemp.wsbeans.iseries/">
   <soapenv:Header/>
   <soapenv:Body>
      <con:converttemp>
         <arg0>
            <TEMPIN>80</TEMPIN>
         </arg0>
      </con:converttemp>
   </soapenv:Body>
</soapenv:Envelope>
') );


Questo sorgente SQL è disponibile su Github: https://github.com/Faq400Git/F4P0001/blob/master/F4P0001/F4P0001/F4SOAPSQL1.TXT

Ora possiamo testare la nostra UDF con un semplice SELECT sql

select faq400.converttemp(’42’)
from sysibm.sysdummy1;

(4) Parsing di un XML Soap Response “complesso”

L’esempio che abbiamo fatto con il web service ConvertTemp è un esempio semplice, abbastanza didattico. Nel mondo reale poi ci troviamo spesso coinvolti in web service anche abbastanza complessi, servizi creati da qualche “robot” o generatore di codice che semplificano la pubblicazione di un web service ma che creano una struttura dati tutt’altro che semplice da gestire … almeno per noi essere umani del mondo RPG, che non abbiamo grandissimi strumenti che ci semplificano la vita nella gestione di XML e JSON (anche se questa affermazione non è del tutto vera … le ultime versione di sistema operativo 7.3 / 7.4 mettono a disposizione ottimi strumenti per JSON e XML … e Scott Klement e il resto della Community IBM i mondiale)

Ma veniamo al Soap Response con XML complesso, con Namespaces, livelli, e tag ripetuti. Quello riportato qui sotto è un XML SOAP Response di un Web Service su piattaforma NetSuite di Oracle (con qualche modifica per anonimizzarne il contenuto!):

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
   <soapenv:Header>
      <platformMsgs:documentInfo xmlns:platformMsgs="urn:messages_2018_1.platform.webservices.netsuite.com">
         <platformMsgs:nsId>WEBSERVICES_xxx</platformMsgs:nsId>
      </platformMsgs:documentInfo>
   </soapenv:Header>
   <soapenv:Body>
      <searchResponse xmlns="urn:core_2018_1.platform.webservices.netsuite.com">
         <searchResult>
            <status isSuccess="true"/>
            <totalRecords>1</totalRecords>
            <pageSize>1000</pageSize>
            <totalPages>1</totalPages>
            <pageIndex>1</pageIndex>
            <searchId>WEBSERVICES_xxx</searchId>
            <searchRowList>
               <searchRow xsi:type="listAcct:ItemSearchRow" xmlns:listAcct="urn:accounting_2018_1.lists.webservices.netsuite.com">
                  <listAcct:basic xmlns:platformCommon="urn:common_2018_1.platform.webservices.netsuite.com">
                     <platformCommon:displayName>
                        <searchValue>Item AAA</searchValue>
                     </platformCommon:displayName>
                     <platformCommon:internalId>
                        <searchValue internalId="161"/>
                     </platformCommon:internalId>
                     <platformCommon:itemId>
                        <searchValue>XXX51113</searchValue>
                     </platformCommon:itemId>
                     <platformCommon:saleUnit>
                        <searchValue internalId="4"/>
                     </platformCommon:saleUnit>
                     <platformCommon:type>
                        <searchValue>_nonInventoryItem</searchValue>
                     </platformCommon:type>
                     <platformCommon:weight>
                        <searchValue>0.0</searchValue>
                     </platformCommon:weight>
                     <platformCommon:customFieldList>
                        <customField internalId="2595" scriptId="custitem_dlt_qct_pack_quantity" xsi:type="ns1:SearchColumnLongCustomField" xmlns:ns1="urn:core_2018_1.platform.webservices.netsuite.com">
                           <ns1:searchValue>12</ns1:searchValue>
                        </customField>
                     </platformCommon:customFieldList>
                  </listAcct:basic>
                  <listAcct:customSearchJoin xmlns:platformCommon="urn:common_2018_1.platform.webservices.netsuite.com">
                     <platformCommon:customizationRef scriptId="custitem_cseg_dlt_qct_neg_cl" internalId="2575"/>
                     <platformCommon:searchRowBasic xsi:type="platformCommon:CustomRecordSearchRowBasic">
                        <platformCommon:recType internalId="296"/>
                        <platformCommon:name>
                           <searchValue>Item AAA long description</searchValue>
                           <customLabel>Item Classification</customLabel>
                        </platformCommon:name>
                     </platformCommon:searchRowBasic>
                  </listAcct:customSearchJoin>
                  <listAcct:customSearchJoin xmlns:platformCommon="urn:common_2018_1.platform.webservices.netsuite.com">
                     <platformCommon:customizationRef scriptId="custitem_dlt_qct_neg_package_item" internalId="2593"/>
                     <platformCommon:searchRowBasic xsi:type="platformCommon:CustomRecordSearchRowBasic">
                        <platformCommon:recType internalId="305"/>
                        <platformCommon:name>
                           <searchValue>XXX5011300012</searchValue>
                           <customLabel>Item Packaging</customLabel>
                        </platformCommon:name>
                     </platformCommon:searchRowBasic>
                  </listAcct:customSearchJoin>
                  <listAcct:customSearchJoin xmlns:platformCommon="urn:common_2018_1.platform.webservices.netsuite.com">
                     <platformCommon:customizationRef scriptId="custitem_dlt_qct_neg_bar_code" internalId="2598"/>
                     <platformCommon:searchRowBasic xsi:type="platformCommon:CustomRecordSearchRowBasic">
                        <platformCommon:recType internalId="306"/>
                        <platformCommon:name>
                           <searchValue>1234567890123</searchValue>
                           <customLabel>Barcode</customLabel>
                        </platformCommon:name>
                     </platformCommon:searchRowBasic>
                  </listAcct:customSearchJoin>
               </searchRow>
               
   <searchRow xsi:type="listAcct:ItemSearchRow" xmlns:listAcct="urn:accounting_2018_1.lists.webservices.netsuite.com">
                  <listAcct:basic xmlns:platformCommon="urn:common_2018_1.platform.webservices.netsuite.com">
                     <platformCommon:displayName>
                        <searchValue>Item BBB</searchValue>
                     </platformCommon:displayName>
                     <platformCommon:internalId>
                        <searchValue internalId="161"/>
                     </platformCommon:internalId>
                     <platformCommon:itemId>
                        <searchValue>XXX51114</searchValue>
                     </platformCommon:itemId>
                     <platformCommon:saleUnit>
                        <searchValue internalId="4"/>
                     </platformCommon:saleUnit>
                     <platformCommon:type>
                        <searchValue>_nonInventoryItem</searchValue>
                     </platformCommon:type>
                     <platformCommon:weight>
                        <searchValue>0.0</searchValue>
                     </platformCommon:weight>
                     <platformCommon:customFieldList>
                        <customField internalId="2595" scriptId="custitem_dlt_qct_pack_quantity" xsi:type="ns1:SearchColumnLongCustomField" xmlns:ns1="urn:core_2018_1.platform.webservices.netsuite.com">
                           <ns1:searchValue>12</ns1:searchValue>
                        </customField>
                     </platformCommon:customFieldList>
                  </listAcct:basic>
                  <listAcct:customSearchJoin xmlns:platformCommon="urn:common_2018_1.platform.webservices.netsuite.com">
                     <platformCommon:customizationRef scriptId="custitem_cseg_dlt_qct_neg_cl" internalId="2575"/>
                     <platformCommon:searchRowBasic xsi:type="platformCommon:CustomRecordSearchRowBasic">
                        <platformCommon:recType internalId="296"/>
                        <platformCommon:name>
                           <searchValue>Item BBB long description</searchValue>
                           <customLabel>Item Classification</customLabel>
                        </platformCommon:name>
                     </platformCommon:searchRowBasic>
                  </listAcct:customSearchJoin>
                  <listAcct:customSearchJoin xmlns:platformCommon="urn:common_2018_1.platform.webservices.netsuite.com">
                     <platformCommon:customizationRef scriptId="custitem_dlt_qct_neg_package_item" internalId="2593"/>
                     <platformCommon:searchRowBasic xsi:type="platformCommon:CustomRecordSearchRowBasic">
                        <platformCommon:recType internalId="306"/>
                        <platformCommon:name>
                           <searchValue>XXX5011300013</searchValue>
                           <customLabel>Item Packaging</customLabel>
                        </platformCommon:name>
                     </platformCommon:searchRowBasic>
                  </listAcct:customSearchJoin>
                  <listAcct:customSearchJoin xmlns:platformCommon="urn:common_2018_1.platform.webservices.netsuite.com">
                     <platformCommon:customizationRef scriptId="custitem_dlt_qct_neg_bar_code" internalId="2598"/>
                     <platformCommon:searchRowBasic xsi:type="platformCommon:CustomRecordSearchRowBasic">
                        <platformCommon:recType internalId="306"/>
                        <platformCommon:name>
                           <searchValue>1234567890124</searchValue>
                           <customLabel>Barcode</customLabel>
                        </platformCommon:name>
                     </platformCommon:searchRowBasic>
                  </listAcct:customSearchJoin>
               </searchRow>     
<searchRow xsi:type="listAcct:ItemSearchRow" xmlns:listAcct="urn:accounting_2018_1.lists.webservices.netsuite.com">
                  <listAcct:basic xmlns:platformCommon="urn:common_2018_1.platform.webservices.netsuite.com">
                     <platformCommon:displayName>
                        <searchValue>Item CCC</searchValue>
                     </platformCommon:displayName>
                     <platformCommon:internalId>
                        <searchValue internalId="161"/>
                     </platformCommon:internalId>
                     <platformCommon:itemId>
                        <searchValue>XXX51114</searchValue>
                     </platformCommon:itemId>
                     <platformCommon:saleUnit>
                        <searchValue internalId="4"/>
                     </platformCommon:saleUnit>
                     <platformCommon:type>
                        <searchValue>_nonInventoryItem</searchValue>
                     </platformCommon:type>
                     <platformCommon:weight>
                        <searchValue>0.0</searchValue>
                     </platformCommon:weight>
                     <platformCommon:customFieldList>
                        <customField internalId="2595" scriptId="custitem_dlt_qct_pack_quantity" xsi:type="ns1:SearchColumnLongCustomField" xmlns:ns1="urn:core_2018_1.platform.webservices.netsuite.com">
                           <ns1:searchValue>12</ns1:searchValue>
                        </customField>
                     </platformCommon:customFieldList>
                  </listAcct:basic>
                  <listAcct:customSearchJoin xmlns:platformCommon="urn:common_2018_1.platform.webservices.netsuite.com">
                     <platformCommon:customizationRef scriptId="custitem_cseg_dlt_qct_neg_cl" internalId="2575"/>
                     <platformCommon:searchRowBasic xsi:type="platformCommon:CustomRecordSearchRowBasic">
                        <platformCommon:recType internalId="297"/>
                        <platformCommon:name>
                           <searchValue>Item CCC long description</searchValue>
                           <customLabel>Item Classification</customLabel>
                        </platformCommon:name>
                     </platformCommon:searchRowBasic>
                  </listAcct:customSearchJoin>
                  <listAcct:customSearchJoin xmlns:platformCommon="urn:common_2018_1.platform.webservices.netsuite.com">
                     <platformCommon:customizationRef scriptId="custitem_dlt_qct_neg_package_item" internalId="2593"/>
                     <platformCommon:searchRowBasic xsi:type="platformCommon:CustomRecordSearchRowBasic">
                        <platformCommon:recType internalId="305"/>
                        <platformCommon:name>
                           <searchValue>XXX5011300013</searchValue>
                           <customLabel>Item Packaging</customLabel>
                        </platformCommon:name>
                     </platformCommon:searchRowBasic>
                  </listAcct:customSearchJoin>
                  <listAcct:customSearchJoin xmlns:platformCommon="urn:common_2018_1.platform.webservices.netsuite.com">
                     <platformCommon:customizationRef scriptId="custitem_dlt_qct_neg_bar_code" internalId="2598"/>
                     <platformCommon:searchRowBasic xsi:type="platformCommon:CustomRecordSearchRowBasic">
                        <platformCommon:recType internalId="306"/>
                        <platformCommon:name>
                           <searchValue>1234567890125</searchValue>
                           <customLabel>Barcode</customLabel>
                        </platformCommon:name>
                     </platformCommon:searchRowBasic>
                  </listAcct:customSearchJoin>
               </searchRow>			   
            </searchRowList>
         </searchResult>
      </searchResponse>
   </soapenv:Body>
</soapenv:Envelope>

Questo documento XML è disponibile su Github; https://github.com/Faq400Git/F4P0001/blob/master/NetSuite%20XML%20SOAP%20Response.xml

Per il parsing, nel programma originale, lo eseguo direttamente con una sola istruzione SQL dove chiamo con HTTPPOSTCLOB il web service Netsuite … in questo caso vado a leggere l’XML leggendolo da un file IFS, ma il concetto del parsing non cambia.

Nel seguente SQLRPGLE eseguo proprio un parsing leggendo l’XML dall’IFS

       //-----------------------------------------
       // F4XMLPARS1 SQLRPGLE Source
       //   We are trying to parse and XML stored
       //   in our IFS, and extract data from
       //   different levels and  different namespaces
       //   through XMLTABLE and XMLPARSE SQL Function
       //
       //   Pay attention to GET_XML_FILE to read an xml
       //   stored in the IFS
       //
       //   Rev. 5
       // ------------------------------------------------
       ctl-opt DFTACTGRP(*NO);




       dcl-ds dsresult qualified dim(999);
         totalrecords packed(9:0);
         pagesize packed(9:0);
         totalpages packed(9:0);
         pageindex packed(9:0);
         searchid varchar(50);
         displayname varchar(50);
         internalid varchar(50);
         itemid varchar(50);
         saleunit varchar(50);
         type varchar(50);
         weight varchar(50);
       end-ds;




       dcl-s  risposta char(10);

       dcl-s  RowsFetched int(5);
       dcl-s  i           int(5);


       // Set COMMIT to read XML file from IFS
       exec sql SET OPTION COMMIT=*CHG;

       // Set CCSID for some HTTP Functions (65535 is no good!)
       exec sql CALL QSYS2.QCMDEXC('CHGJOB CCSID(280)');


       // Get my XML file from IFS and parse it with  XMLTABLE
       // and XMLPARSE SQL Functions
       exec sql
        declare wscursor cursor for
        select *
        FROM
        XMLTABLE(
        xmlnamespaces
        (default 'urn:core_2018_1.platform.webservices.netsuite.com',
         'http://schemas.xmlsoap.org/soap/envelope/' AS "soap",
         'urn:accounting_2018_1.lists.webservices.netsuite.com' as "listact",
         'urn:common_2018_1.platform.webservices.netsuite.com' as "platf"
            ),
          '$doc/soap:Envelope/soap:Body/+
          *:searchResponse/searchResult/+
          *:searchRowList/searchRow[*]'
       PASSING
       xmlparse(document GET_XML_FILE('/tmp/NetSuite XML SOAP Response.xml'))
       as "doc"
       columns
       totalRecords decimal(9, 0) path '../../totalRecords',
       pageSize decimal(9, 0) path '../../pageSize',
       totalPages decimal(9, 0) path '../../totalPages',
       pageIndex decimal(9, 0) path '../../pageIndex',
       searchId varchar(50) path '../../searchId',
       displayName varchar(50) path '*:basic/*:displayName/searchValue',
       internalId varchar(50)
         path '*:basic/*:internalId/searchValue/@internalId',
       itemId varchar(50) path '*:basic/*:itemId/searchValue',
       saleUnit varchar(50) path '*:basic/*:saleUnit/searchValue/@internalId',
       type varchar(50) path '*:basic/*:type/searchValue',
       weight varchar(50) path '*:basic/*:weight/searchValue'
       ) as a;

       exec sql open wscursor;

       exec sql
          fetch wscursor for 999 rows into :dsresult;

       exec sql GET DIAGNOSTICS :RowsFetched = ROW_COUNT ;

       // Return to my current job CCSID
       exec sql CALL QSYS2.QCMDEXC('CHGJOB CCSID(*USRPRF)');

       dsply 'Risultato:';
       for i=1 to RowsFetched;
          dsply 'Row :';
          dsply i;
          dsply '- displayname:';
          dsply dsresult(i).displayname;
          dsply '- internalid:';
          dsply dsresult(i).internalid;
          dsply '- itemid:';
          dsply dsresult(i).itemid;
          dsply '- weight:';
          dsply dsresult(i).weight;
       endfor;
       dsply 'End' '' risposta;


       *inlr=*on;

Questo sorgente è disponibile su Github: https://github.com/Faq400Git/F4P0001/blob/master/F4P0001/F4P0001/F4XMLPARS1.SQLRPGLE

Qui vediamo il solo SQL da eseguire nell’ambiente “Esegui script SQL di Rdi o ACS”

select a.*
FROM
XMLTABLE(xmlnamespaces (default 'urn:core_2018_1.platform.webservices.netsuite.com',
  'http://schemas.xmlsoap.org/soap/envelope/' AS "soap",
  'urn:accounting_2018_1.lists.webservices.netsuite.com' as "listact",
  'urn:common_2018_1.platform.webservices.netsuite.com' as "platformCommon"
  ),
'$doc/soap:Envelope/soap:Body/:searchResponse/searchResult/:searchRowList/searchRow[]'
PASSING
xmlparse(document GET_XML_FILE('/tmp/Netsuite XML SOAP Response.xml'))
as "doc"
columns
totalRecords decimal(9, 0) path '../../totalRecords',
pageSize decimal(9, 0) path '../../pageSize',
totalPages decimal(9, 0) path '../../totalPages',
pageIndex decimal(9, 0) path '../../pageIndex',
searchId varchar(50) path '../../searchId'
,displayName varchar(50) path ':basic/:displayName/searchValue'
,internalId varchar(50) path ':basic/:internalId/searchValue/@internalId'
,itemId varchar(50) path ':basic/:itemId/searchValue'
,saleUnit varchar(50) path ':basic/:saleUnit/searchValue/@internalId'
,type varchar(50) path ':basic/:type/searchValue'
,weight varchar(50) path ':basic/:weight/searchValue'
,customFieldList_id varchar(50) path ':basic/:customFieldList/customField/@internalId'
,rectype_internalid varchar(50) path ':customSearchJoin[1]/:searchRowBasic/:recType/@internalId'
,name varchar(50) path ':customSearchJoin[1]/:searchRowBasic/*:name/searchValue'
 ) as a;         x

Il risultato di questo SQL è una tabella SQL che possiamo utilizzare direttamente nel nostro RPG con SQL Embedded oppure come una normale tabella per JOIN o altre operazioni.

Conclusione

Abbiamo visto tre differenti modi di consumare un Web Service SOAP, non sono naturalmente gli unici, sono quelli che incontriamo più spesso. Lascia un tuo commento al post se utilizzi un modo differente o se hai dei suggerimenti.

Related Posts
DB2 for i SQL – Stringhe – POSSTR-LOCATE-LOCATE_IN_STRING (IT)

Introduzione Spesso, nelle nostre applicazioni, abbiamo la necessità di lavorare con le stringhe di testo e l'SQL del DB2 può Read more

DB2 for i & SQL – FAQ & Howto (Part. 1) (IT)

Database DB2 e SQL ... forse lo strumento più potente e completo che abbiamo sulla piattaforma IBM i: ecco una Read more

Annuncio IBM i 7.4

Arriva direttamente con l'uovo di Pasqua questo annuncio IBM per le novità della versione IBM i 7.4, versione iNext secondo Read more

Generated Always Columns – Approfondimenti (IT)

Introduzione "Generated Always Column": sono colonne, campi, di una tabella il cui contenuto è controllato direttamente dal sistema ... e Read more

--- Roberto De Pedrini Faq400.com

Recent Posts

Gestione dei file video bloccati su IBM i: una soluzione efficace

Riceviamo e pubblichiamo ben volentieri questo "tip & trick" di Patrick Rizzi che presenta una tecnica che permette di intervenire…

2 mesi ago

Monitoraggio Messaggi QSYSOPR: SQL per Ottenere Messaggi e Reply

Prendo spunto da una risposta di Michael Mayer sulle mailing list di Midrange.com a chi chiedeva come monitorare i messaggi…

2 mesi ago

Perché l’ERP è la Chiave del Successo per le Imprese Moderne

Le imprese sono sempre più alla ricerca di strumenti che possano migliorare l'efficienza, la collaborazione e la gestione delle risorse.…

4 mesi ago

ACS Access Client Solution 1.1.9.5

I primi di Aprile è uscita la "Spring Version" di ACS Access Client Solution, versione 1.1.9.5 Interessanti novità soprattutto in…

8 mesi ago

Tim Rowe and Scott Forstie – Promo video for CEC 2024 – Milan

Se non vi bastava la ricca agenda delle sessioni del Common Europe Congress 2024, 3-6 Giugno Milano, ecco un altro…

8 mesi ago

Code for IBM i 2.10.0 – Debug IBM i con Visual Studio Code

Le funzioni di debug con Visual Studio Code sono disponibili da qualche tempo ma questa nuova versione 2.10.0 semplifica la…

8 mesi ago