Montag, 5. November 2018

VFP, das Web und der ganze ReST - 7 - Fertig machen zur Landung / VFP, the web and all the ReST - 7 - preparing for landing

7 - Fertig machen zur Landung / preparing for landing

Soeben haben wir den .ReadyState 4 erhalten. Dies bedeutet, dass wir eine Serverantwort in Empfang nehmen können. Das hört sich doch eigentlich ganz gut an. Dummerweise bleibt uns an dieser Stelle ein eventueller Blick in die Dokumentation des Webservice nicht erspart.

Das oXmlHttp Objekt stellt uns nämlich zwei Eigenschaften zur Verfügung mit deren Hilfe wir die Serverantwort abfragen können.

  • .ResponseText
  • .ResponseBody

Welche der beiden herangezogen werden muss, darüber informiert uns im Normalfall die Eigenschaft .ResponseType. Aaaaber, es kommt auch schon mal vor, dass dieser auf dem Server (content/type) falsch hinterlegt ist. Das wiederum bedeutet: die hoffentlich vorhandene Dokumentation wälzen und hoffen, dort fündig zu werden.

Grundsätzlich bedeutet ein leerer Eintrag oder "text" innerhalb von .ResponseType dass die Eigenschaft .ResponseText gelesen werden muss. In allen anderen Fällen ist es .ResponseBody. Erschwerend kommt hierbei hinzu, dass nicht jeder Browser jeden .ResponseType unterstützt. Was wohl zumindest in Teilen erklärt, warum manche Webservices bei der Typdeklaration ein wenig schludern.

Wenn es hart auf hart kommt hilft uns jedoch der Debugger. Dort können wir die Inhalte ohne weiteres einsehen (im Code einfach .ResponseText und .ResponseBody jeweils einer Variablen zuweisen und diese im Debugger anzeigen lassen oder direkt in eine Datei ausgeben).

Haben wir uns zuvor eine Tabelle angelegt in welcher wir die diversen Bauteile der Service URI hinterlegt haben, dann können wir an dieser Stelle auch gleich eine weitere Spalte für den .ResponseType anlegen und eintragen ob .ResponseText oder .ResponseBody ausgelesen werden muss.

Der folgende Link gibt eine kurze Übersicht der möglichen .ResponeType Werte:

https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType

Als goldene Regel gilt, dass Binärdaten, Arrays, Dateien (pdf, jpg, xml, json) IMMER im .ResponseBody liegen. Übrigens können sowohl XML als auch JSON neben Datensätzen/Arrays auch Binärdaten transportieren/aufnehmen. Im Normalfall werden Binärdaten für JSON zuvor über BASE64 in Text umgewandelt. Wenn Euch das nicht geläufig sein sollte, dann schaut Euch die Funktion STRCONV(,13|14) in der VFP Hilfe mal etwas genauer an 😉.

Hier ein kurzes Beispiel, wie ein Mailversand über einen Webservice aussehen kann, der auch Dateien innerhalb von JSON übergibt. Die erste FOR...ENDFOR Schleife verarbeitet die ausgewählten Dateianhänge und verpackt diese über STRCONV() in einen JSON Array (siehe hierzu auch Kapitel 2). Im zweiten Schritt erfolgt der Zusammenbau des eigentlichen JSON Objektes bei dem die gewandelten Binärdaten als Bestandteil des Wertepaares für "attachments" platziert werden.

WITH Thisform.cmgAnhang

    FOR liLoop = 1 TO .ButtonCount
        lcStream = FILETOSTR( .Buttons( liLoop ).Tag )
        lcAttach = lcAttach + [{ "id" : ] + TRANSFORM( liLoop ) + [ , "name" : "] + JUSTFNAME( .Buttons( liLoop ).Tag ) +  [" , "file" : "] + STRCONV( lcStream , 13 ) + [" }]
        lcAttach = lcAttach + IIF( liLoop < .ButtonCount , [ ,] , [] )
    ENDFOR 

ENDWITH 

TEXT TO lcDlePar TEXTMERGE NOSHOW PRETEXT 1+2+3+8
    {
    "to" : "<<ALLTRIM( Thisform._To )>>" ,
    "from" : "<<ALLTRIM( Thisform._From )>>" ,
    "cc" : "<<ALLTRIM( Thisform._Cc )>>" ,
    "bcc" : "<<ALLTRIM( Thisform._Bcc )>>" ,
    "subject" : "<<ALLTRIM( Thisform._Subject )>>" ,
    "body" : "<<Thisform.Converter( Thisform._Body )>>" ,
    "attachments" : [ <<lcAttach>> ]
    }
ENDTEXT 

Aber nun zurück zum eigentlichen Thema um dort weiter zu machen wo das vorangegangene Kapitel endete.

Der folgende Code beinhaltet eine explizite Prüfung auf .ResponseType und versucht im ersten Schritt den .ResponseText auszulesen. Eine Abfrage basierend auf einer Tabellenfeldvorgabe ist sicherlich sinnvoll.

*// something went wrong
IF oXmlHttp.status != 200 
    =GiveFeedback( oXmlHttp.status )

*// otherwise start computing data
ELSE 
    LOCAL lcRetVal AS String
    STORE [] TO lcRetVal
    IF EMPTY( oXmlHttp.ResponseType ) OR LOWER( ALLTRIM( oXmlHttp.ResponseType ) == [text]
        TRY 
            *// try to read .ResponseText
            =myFormObj.StatusOutput( [computing ResponseText] )
            IF NOT ISNULL( oXmlHttp.responseText )
                lcRetVal = oXMLHttp.responseText
            ENDIF
        CATCH 
            lcRetVal = []
        ENDTRY
    ENDIF

    IF EMPTY( lcRetVal )
        TRY 
            *// If lcRetVal is empty, read .ResponseBody
            =myFormObj.StatusOutput( [computing ResponseBody] )
            IF NOT ISNULL( oXmlHttp.responseBody )
                lcRetVal = oXMLHttp.responseBody
            ENDIF
        CATCH 
            lcRetVal = []
        ENDTRY
    ENDIF
ENDIF 

Ab jetzt steht in der Variablen lcRetVal das Ergebnis aus unserer Webservice Anfrage. Der Inhalt kann alles Mögliche sein.

  • Eine PDF-Datei die wir direkt mit STRTOFILE() auf die Platte schreiben können:

lcFile = ADDBS( GETENV( [TEMP] ) ) ;
       + FORCEEXT( DTOC(DATE(),1)+[_]+SYS(3),[pdf])
STRTOFILE( lcRetVal , lcFile , 0 )

  • Ein JSON String der in VFP kompatible Daten zerlegt werden muss:

lcSplit     = CHR( 13 )
lcStream    = Thisform.Tag
lcStream    = STRTRAN( lcStream , [\u0026] , [&] )
lcStream    = STRTRAN( lcStream , [\u003C] , [<] )
lcStream    = STRTRAN( lcStream , [\u003c] , [<] )
lcStream    = STRTRAN( lcStream , [\u003E] , [>] )
lcStream    = STRTRAN( lcStream , [\u003e] , [>] )
lcStream    = STRTRAN( lcStream , [\u00E4] , [ä] )
lcStream    = STRTRAN( lcStream , [\u00e4] , [ä] )
lcStream    = STRTRAN( lcStream , [\u00C4] , [Ä] )
lcStream    = STRTRAN( lcStream , [\u00c4] , [Ä] )
lcStream    = STRTRAN( lcStream , [\u00F6] , [ö] )
lcStream    = STRTRAN( lcStream , [\u00f6] , [ö] )
lcStream    = STRTRAN( lcStream , [\u00D6] , [Ö] )
lcStream    = STRTRAN( lcStream , [\u00d6] , [Ö] )
lcStream    = STRTRAN( lcStream , [\u00FC] , [ü] )
lcStream    = STRTRAN( lcStream , [\u00fc] , [ü] )
lcStream    = STRTRAN( lcStream , [\u00DC] , [Ü] )
lcStream    = STRTRAN( lcStream , [\u00dc] , [Ü] )
lcStream    = STRTRAN( lcStream , [\u00DF] , [ß] )
lcStream    = STRTRAN( lcStream , [\u00df] , [ß] )
lcStream    = STRTRAN( lcStream , [\u00AC] , [€] )
lcStream    = STRTRAN( lcStream , [\u00AC] , [€] )
lcStream    = STRTRAN( lcStream , [\u0024] , [$] )
lcStream    = STRTRAN( lcStream , [\u00A3] , [£] )
lcStream    = STRTRAN( lcStream , [\u00a3] , [£] )
    
IF LEFT( lcStream , 1 ) = '['     && multiple records
    lcStream    = STREXTRACT( lcStream, '[' , ']' ) 
    llMultiRec    = .T.
ENDIF 

Thisform.edit2.Value = []
liRecord = 1
DO WHILE .T.

    lcRecord = STRTRAN( STREXTRACT( lcStream , [{] , [}]  , liRecord) , [","] , ["] + lcSplit + ["] )

    IF NOT EMPTY( lcRecord )
        FOR liLoop = 1 TO GETWORDCOUNT( lcRecord , lcSplit )
            lcWord      = GETWORDNUM( lcRecord , liLoop , lcSplit )
            lcColumn    = STREXTRACT( lcWord , ["] , ["] , 1 )
            lcValue     = STREXTRACT( lcWord , ["] , ["] , 3 )

            Thisform.edit2.Value = Thisform.edit2.Value + CHR(13) + lcColumn + [ / ] + lcValue
        ENDFOR 
        liRecord = liRecord + 1 
    ELSE 
        EXIT 
    ENDIF 
ENDDO 

Der o.a. Codeblock stammt 1:1 aus der Demoform die am Ende des Blogeintrags über einen Download-Link abgerufen werden kann.

  • Ein Bilddatei (die wie eine PDF direkt als Datei gespeichert wird).

  • Ein einfaches "OK" das uns mitteilt, dass der Webservice nun weitere Anfragen für uns bearbeiten wird.

  • Eine SessionID mit der wir unsere nächsten Anfragen an den Webservice versehen müssen, damit dieser uns wiedererkennt und gecachte Daten nutzt.

Die Möglichkeiten sind vielfältig. Aber nun sollte es kein unüberwindbares Problem mehr sein, eine passende Reaktion zu programmieren.

Bisher konnte ich mit den in diesem und den vorangegangenen Kapiteln beschriebenen Vorgehensweisen fast alle Webservices ansprechen und deren Daten in VFP weiterverarbeiten. Das bedeutet natürlich nicht, dass ich alle Fallstricke berücksichtigt habe. Manche sind mir noch nicht über den Weg gelaufen oder ich habe sie im Laufe der Zeit schlicht und einfach verdrängt, schließlich liegt mein erster Kontakt mit dem oXmlHttp Objekt schon beinahe 10 Jahre zurück. Sollte also jemandem auffallen das etwas Wichtiges fehlt, dann kommentiert dies bitte. Ich ergänze diese Artikelserie gerne um weitere Informationen.

Zum Abschluß gibt es nun noch einen kleinen Goody :)

Im Rahmen einer Anbindung an ein System das dutzende von Web Services bereitstellt, habe ich vor Jahren einen Prototypen marke 'Quick and Dirty' gebastelt.

Die Form beinhaltet verschiedene ReST basierende Web Service Zugriffe die durchweg als praxistauglich angesehen werden können. Sie erfolgen zwar mit einem Testschlüssel (API Key) und somit werden keine Echtdaten verarbeitet bzw. gesendet, aber es findet eine tatsächliche Kommunikation zu den Diensten des Anbieters statt. Also viel Spaß beim ausprobieren ;)

Hier der Download Link zur Testform:


Die drei oberen (rot umrandeten) Buttons fragen Dienste ab, die JSON Daten liefern. Diese können über den unteren (ebenfalls rot markierten) Button konvertiert werden.

Die drei darunterliegenden Buttons liefern bzw. verarbeiten Daten in anderen Formaten.

Die ZIP enthält zwei Dateien:
  • webservicetestform.sct
  • webservicetestform.scx
Da sich diese Artikelserie in erster Linie an VFP Entwickler richtet sollte meines Erachtens eine reine VFP Form mehr als ausreichend sein. Sollte doch eine EXE gewünscht werden ist das auch kein großes Problem. Einfach einen Kommentar schreiben ;)

Achja, noch ein kleiner Tipp zum Abschluß:

Da wir für den Zugriff auf Web Services gültige URIs zusammenbauen, können wir diese auch DIREKT im Browser unserer Wahl eingeben und testen. Auf diese Weise sparen wir uns speziell zu Beginn einige mühselige Debugging Sitzungen...

Links zu den restlichen Kapiteln:

Einführung / Introduction 

Teil 1: Abkürzungen und was sie bedeuten
           / Abbreviations and their meaning

Teil 2: Wer ReST sagt, sagt auch JSON
          / In for a ReST, in for a JSON

Teil 3: Leerzeichen und andere Entitäten
          / BLANKS and other entities

Teil 4: JSON und der goldene Konverter
          / JSON and the golden converter

Teil 5: Die Startvorbereitungen
          / preparations for launch

Teil 6: Kleine Denkpause gefällig?
          / in need of a reflection period?

Teil 7: Fertig machen zur Landung
          / preparing for landing



Donnerstag, 1. November 2018

VFP, das Web und der ganze ReST - 6 - kleine Denkpause gefällig? / VFP, the web and all the ReST - 6 - in need of a reflection period?

6. Kleine Denkpause gefällig? / in need of a reflection period?


Nachdem nun die Übertragung an den Webservice initiiert wurde bleibt uns als nächstes nur eines: Warten...

Das hört sich schlimmer an als es tatsächlich ist. Rufen wir uns an dieser Stelle mal das uralte Grundprinzip der EDV ins Gedächtnis zurück: EVA

    E ingabe
    V erarbeitung
    A usgabe

Als Entwickler haben wir uns dieses Prinzip vermutlich schon so verinnerlicht, dass wir es gar nicht mehr bewusst zur Kenntnis nehmen 😊.

Unser oXmlHttp.Send() ist die Eingabe die der Server entgegen nimmt.

In dem Augenblick in dem die Daten beim Zielserver ankommen beginnt dort die Verarbeitung. Diese kennt jedoch verschiedene Stufen.

Wie der aktuelle Stand der Dinge beim Server aussieht, darüber gibt uns die oXmlHttp.Status Eigenschaft Auskunft. Sie liefert uns alle möglichen Werte darüber, was bei der Verarbeitung so alles passiert ist. Aber ungeachtet der vielen verschiedenen Werte interessiert uns für die weitere Verarbeitung bzw. für den Empfang der Daten nur ein einziger Status.

Sobald wir den Wert 200 geliefert bekommen bedeutet dies, dass der Webservice Daten zur Ausgabe bereithält. Alle anderen Statuswerte ermöglichen uns eine gezielte Reaktion auf Probleme, die bei der Kommunikation zum und vom Server auftreten könnten.

Auf dieser Seite findet Ihr eine umfangreiche Liste der möglichen Werte:

https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms767625(v%3Dvs.85)

Wie bereits geschrieben bedeutet der Status 200 dass die Verarbeitung erfolgreich abgeschlossen wurde und nun das Ergebnis abgerufen werden kann.
Uns interessiert jedoch nicht, was der Service gerade so anstellt. Uns interessiert nur, ob unsere Anfrage angekommen ist, ob sie zur Verarbeitung angenommen wurde, ob die Verarbeitung noch läuft und ob die Verarbeitung abgeschlossen ist.  Beim letzten Punkt steht dann auch hoffentlich ein Status 200 bereit.

Hierbei müssen wir uns darüber im klaren sein, dass eine abgeschlossene Verarbeitung nicht automatisch auch eine erfolgreiche Verarbeitung bedeutet.

Zunächst benötigen wir also eine kleine Schleife die immer wieder beim Service anfragt ob er mit der Verarbeitung fertig ist. Für diese Schleife greifen wir auf eine andere Eigenschaft des oXmlHttp-Objektes zu, den oXmlHttp.ReadyState. Diese Eigenschaft reduziert sich auf den eigentlichen Stand der Verarbeitung nach dem EVA Prinzip.

  • Wurde eine Verbindung zum Server hergestellt
  • wurden die Daten erfolgreich an den Webservice übermittelt
  • werden die übermittelten Daten verarbeitet
  • ist die Verarbeitung abgeschlossen und kann die Antwort abgerufen werden.
Siehe hierzu auch:

https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms761388(v%3dvs.85)

Wird der letzte dieser vier Punkte gemeldet, dann ist es Zeit in Erfahrung zu bringen, ob bei der Verarbeitung alles glatt gelaufen ist und wir tatsächlich Daten abrufen können.

In der Praxis sieht es also so aus, dass wir in einer Schleife den .ReadyState = 4 prüfen und anschließend den .Status = 200 und erst wenn beide Eigenschaften uns sozusagen ihr GO gegeben haben, dann können wir auch wirklich Daten vom Webservice abrufen.

DECLARE Sleep IN Win32API INTEGER nMilliseconds
DO WHILE .T.
    DO CASE 
    CASE oXmlHttp.ReadyState = 0
        =myFormObj.StatusOutput( [(0) request not initialized] )
    CASE oXmlHttp.ReadyState = 1
        =myFormObj.StatusOutput( [(1) server connection established] )
    CASE oXmlHttp.ReadyState = 2
        =myFormObj.StatusOutput( [(2) request received] )
    CASE oXmlHttp.ReadyState = 3
        =myFormObj.StatusOutput( [(3) processing request] )
    CASE oXmlHttp.ReadyState = 4
        =myFormObj.StatusOutput( [(4) request finished and response is ready] )
        EXIT
    ENDCASE 
    =sleep(200)
ENDDO 
CLEAR DLLS [Sleep]
IF oXmlHttp.status != 200
    *// Something went wrong, so this is the time to call a routine
    *// that visualizes the state from oXmlHttp.status
    =GiveFeedback( oXmlHttp.status )
ELSE
    *// everything went fine, so we can read the data
ENDIF

Bekommen wir den Status 200 gemeldet, dann beginnt der nächste Schritt, andernfalls sollten ein paar Informationen bzgl. der aufgetretenen Probleme ausgegeben werden.

Hierbei sind nicht alle Statuswerte zwingend auf Fehler zurückzuführen und müssen somit auch nicht explizit innerhalb der Feedbackroutine verarbeitet werden.

FUNCTION GiveFeedback as Boolean
LPARAMETERS vStatus as Integer

    DO CASE 
    CASE m.vStatus = 100
         lcInfo = [Continue]
    CASE m.vStatus = 101
         lcInfo = [Switching protocols]
         
    * // #######################################################
    * // this Block (201/202/203/204/205/206) might not be part
    * // of the error handling and could be used for further
    * // Web Service handling because of non-default data
    CASE m.vStatus = 201
         lcInfo = [Created]
    CASE m.vStatus = 202
         lcInfo = [Accepted]
    CASE m.vStatus = 203
         lcInfo = [Non-Authoritative Information]
    CASE m.vStatus = 204
         lcInfo = [No Content]
    CASE m.vStatus = 205
         lcInfo = [Reset Content]
    CASE m.vStatus = 206
         lcInfo = [Partial Content]
    * // #######################################################
         
    CASE m.vStatus = 300
         lcInfo = [Multiple Choices]
    CASE m.vStatus = 301
         lcInfo = [Moved Permanently]
    CASE m.vStatus = 302
         lcInfo = [Found]
    CASE m.vStatus = 303
         lcInfo = [See Other]
    CASE m.vStatus = 304
         lcInfo = [Not Modified]
    CASE m.vStatus = 305
         lcInfo = [Use Proxy]
    CASE m.vStatus = 307
         lcInfo = [Temporary Redirect]
    CASE m.vStatus = 400
         lcInfo = [Bad Request]
    CASE m.vStatus = 401
         lcInfo = [Unauthorized]
    CASE m.vStatus = 402
         lcInfo = [Payment Required]
    CASE m.vStatus = 403
         lcInfo = [Forbidden]
    CASE m.vStatus = 404
         lcInfo = [Not Found]
    CASE m.vStatus = 405
         lcInfo = [Method Not Allowed]
    CASE m.vStatus = 406
         lcInfo = [Not Acceptable]
    CASE m.vStatus = 407
         lcInfo = [Proxy Authentication Required]
    CASE m.vStatus = 408
         lcInfo = [Request Timeout]
    CASE m.vStatus = 409
         lcInfo = [Conflict]
    CASE m.vStatus = 410
         lcInfo = [Gone]
    CASE m.vStatus = 411
         lcInfo = [Length Required]
    CASE m.vStatus = 412
         lcInfo = [Precondition Failed]
    CASE m.vStatus = 413
         lcInfo = [Request Entity Too Large]
    CASE m.vStatus = 414
         lcInfo = [Request-URI Too Long]
    CASE m.vStatus = 415
         lcInfo = [Unsupported Media Type]
    CASE m.vStatus = 416
         lcInfo = [Requested Range Not Suitable]
    CASE m.vStatus = 417
         lcInfo = [Expectation Failed]
    CASE m.vStatus = 500
         lcInfo = [Internal Server Error]
    CASE m.vStatus = 501
         lcInfo = [Not Implemented]
    CASE m.vStatus = 502
         lcInfo = [Bad Gateway]
    CASE m.vStatus = 503
         lcInfo = [Service Unavailable]
    CASE m.vStatus = 504
         lcInfo = [Gateway Timeout]
    CASE m.vStatus = 505
         lcInfo = [HTTP Version Not Supported]
    ENDCASE 
    MESSAGEBOX( ;
        [A problem has occured with the webservice] + CHR( 13 ) + lcInfo , ;
        0+16+0, ;
        [Web Processing Problem] ;
        )
ENDFUNC 

Im nächsten Kapitel geht es dann mit dem Empfang der Daten weiter...


Links zu den restlichen Kapiteln:

Einführung / Introduction 

Teil 1: Abkürzungen und was sie bedeuten
           / Abbreviations and their meaning

Teil 2: Wer ReST sagt, sagt auch JSON
          / In for a ReST, in for a JSON

Teil 3: Leerzeichen und andere Entitäten
          / BLANKS and other entities

Teil 4: JSON und der goldene Konverter
          / JSON and the golden converter

Teil 5: Die Startvorbereitungen
          / preparations for launch

Teil 6: Kleine Denkpause gefällig?
          / in need of a reflection period?

Teil 7: Fertig machen zur Landung
          / preparing for landing


Sonntag, 28. Oktober 2018

VFP, das Web und der ganze ReST - 5 - Die Startvorbereitungen / VFP, the web and all the ReST - 5 - preparations for launch

5 - Die Startvorbereitungen / preparations for launch

Einige Abkürzungen haben wir schon kennengelernt (SOAP, WDSL, ReST, JSON). Nun geht es damit munter weiter 😊. Web-basierender Datenaustausch baut in unserem Fall auf dem Hypertext Transfer Protocol kurz HTTP auf. Dieses Jahrzehnte alte Protokoll wurde in den letzten Jahren von einer sichereren Version seiner selbst ersetzt, dem Hypertext Transfer Protocol secure (=HTTPs). Mittlerweile lassen einige Browser den Zugriff auf 'unsichere' Webseiten (=HTTP) gar nicht mehr zu.

Natürlich nutzen auch Webseiten die ReST basierende Kommunikation. Dort kommt sie immer dann zum Einsatz, wenn gezielt nur einzelne Teile einer Webseite mit neuem Inhalt versehen werden sollen. Im Web ist dann von AJaX (Asynchronous JavaScript and XML) die Rede und ermöglicht den gezielten Austausch bzw. die Aktualisierung von Teilen einer Webseite ohne diese in Gänze vom Server neu abzurufen. In diesem Fall müsste ich eigentlich von AJaJ (Asynchronous JavaScript and JSON) schreiben 😊.

ReST nutzt zur Kommunikation beide Protokolle, also sowohl HTTP als auch HTTPS. Welche Variante zum Einsatz kommt hängt davon ab, welchen Modus wir innerhalb der URI (Uniform Resource Identifier) vorgeben müssen.

Um eine HTTP basierende Kommunikation aufzubauen setzen wir das Microsoft.XMLHTTP Objekt ein.

loXmlHttp = CREATEOBJECT( [Microsoft.XMLHTTP] )

Dieses Objekt verfügt über mehrere Methoden, auf die wir im weiteren Verlauf zugreifen werden.
Stellen wir also im ersten Schritt die URI zusammen.

Je nachdem wie viele unterschiedliche Webservices wir ansprechen ist es sinnvoll die Zugriffsinformationen in einer Tabelle zu speichern. Die Erfahrung lehrt uns, dass allein schon der Glaube in Zukunft keine weiteren Webservices mehr ansprechen zu müssen die Wahrscheinlichkeit darauf um mehrere 100 Prozent erhöht. In Fachkreisen wird dann üblicherweise ganz lapidar von 'Murphy’s Gesetz' gesprochen.

"Wenn es mehrere Möglichkeiten gibt, eine Aufgabe zu erledigen, und eine davon in einer Katastrophe endet oder sonst wie unerwünschte Konsequenzen nach sich zieht, dann wird es jemand genauso machen."
Edward A. Murphy, 1949
Also packen wir die notwendigen Zugriffsinformationen besser von Anfang an in eine Tabelle 😊.

Der folgende Codeblock zeigt das Zusammenbauen der URI:

*// the following values usually are part of
*// an INI file or a DB table
lcServer    = <<InsertIPadress>>
lcSubPath   = <<InsertWebPath>>
lcDienst    = <<InsertNameOfService>>

*// V E R S I O N #1
*// Based on regular URI parameters
TEXT TO lcHttpPost TEXTMERGE NOSHOW PRETEXT 1+2+4+8
    https://<<lcServer>>/<<lcSubPath>>/<<lcDienst>>
    ?param1=<<WertParam1>>
    &param2=<<URIConverter( WertParam2 , [%] )>>
ENDTEXT

*// V E R S I O N #2
*// URI parameter containing a JSON object
TEXT TO lcHttpPost TEXTMERGE NOSHOW PRETEXT 1+2+4+8
    https://<<lcServer>>/<<lcSubPath>>/<<lcDienst>>
    ?parameter=<<URIConverter( vJSON , [&] )>>
ENDTEXT 

*// take care to remove blanks from your URI
*// as well as all other irregular characters
*// before sending your URI to the xmlhttp object
*// example: BLANK -> %20
*// more about this: see previous chapter: BLANKS and other entities
*// at this point we only have to take care for still existing blanks
*// that shouldn’t be part of our URI string
lcHttpPost = STRTRAN( lcHttpPost , [ ] , [] )

Wie an den beiden Beispielen (V E R S I O N #1/#2) unschwer zu erkennen ist gibt es mehrere Ansätze per HTTP Daten an einen Webservice zu übermitteln.  Die auch heute noch recht häufig zum Einsatz kommende Variante des XML Datenstroms lasse ich aufgrund ihrer Komplexität und hochgradigen Individualität und dem aus meiner Sicht unnötigen Daten-Overhead gezielt außen vor.
Im zweiten Schritt teilen wir dem XmlHttp Objekt nun mit, wie der Service angesprochen werden soll. Zu diesem Zweck geben wir der Methode OPEN() unter anderem die gemäß Spezifikation des Dienstes zusammengestellte URI (Parameter 2) vor. Zuvor definieren wir die Kommunikationsmethode (Parameter 1) und ob die Kommunikation synchron (.F.) oder asynchron (.T.) stattfinden soll (Parameter 3).

*// "OPEN" defines the method, the data that shall be transmitted and the comm mode
*//
*// Param#1 = The method we want to use
*//           the POST method is more robust than GET and has no data limitations
*//
*// Param#2 = the webservice URI that shall be connected
*//
*// Param#3 = defines an asynchronous(.T.) or synchronous(.F.) communication
*//           As we want to wait for the webservices response, we need a 
*//           synchronous communication

oXmlHttp.open( [POST] , lcHttpString , .F. )

Anstelle von POST kann auch mit GET gearbeitet werden. Nach diversen Recherchen habe ich mich auf POST eingeschossen, da es angeblich robuster ist und keine Datenbeschränkung kennt.

Mit der Methode SEND() starten wir dann die Kommunikation

*// Now that our object has all information it needs to contact the service
*// we can grant the authorisation to start the process
oXmlHttp.send()


und warten auf das Ergebnis...


Links zu den restlichen Kapiteln:

Einführung / Introduction 

Teil 1: Abkürzungen und was sie bedeuten
           / Abbreviations and their meaning

Teil 2: Wer ReST sagt, sagt auch JSON
          / In for a ReST, in for a JSON

Teil 3: Leerzeichen und andere Entitäten
          / BLANKS and other entities

Teil 4: JSON und der goldene Konverter
          / JSON and the golden converter

Teil 5: Die Startvorbereitungen
          / preparations for launch

Teil 6: Kleine Denkpause gefällig?
          / in need of a reflection period?

Teil 7: Fertig machen zur Landung
          / preparing for landing

Mittwoch, 24. Oktober 2018

VFP, das Web und der ganze ReST - 4 - JSON und der goldene Konverter / VFP, the web and all the ReST - 4 - JSON and the golden converter

4 - JSON und der goldene Converter / JSON and the golden converter


Bereits im vorherigen Kapitel haben wir eine Möglichkeit gesehen wie wir Daten in ein JSON Gerüst einbetten können:

TEXT TO lcJSONData TEXTMERGE NOSHOW PRETEXT 2+4+8
    "int1" : <<myCursor.Column1>>,
    "int2" : <<myCursor.Column2>>,
    "text1" : "<<ALLTRIM( myCursor.Column3 )>>",
    "int3" : <<myCursor.Column4>>,
    "text2" : "<<ALLTRIM( myCursor.Column5 )>>"
ENDTEXT
lcJSONData = [{] + URIConverter( lcJSONData ) + [}]


Nun ist diese kleine Routine zum Erstellen eines JSON Streams noch recht übersichtlich. Bei umfangreicheren Übergabedaten macht es durchaus Sinn einen spezialisierten Cursor zu erstellen und diesen an eine Methode zu übergeben, die aus dem Cursor ein JSON Konstrukt generiert. Voraussetzung ist, dass die Spaltennamen mit den vom Webservice erwarteten Feldnamen des JSON Streams korrespondieren.

Nehmen wir einmal die folgenden Daten als Grundlage:




Gehen wir nun davon aus, dass der Webservice als Feldnamen int1, int2, text1, int3 und text2 erwartet (dies entspricht letztlich dem ersten oben zu sehenden JSON Konstrukt), so kann mit einem einfachen SELECT die benötigte Struktur bzw. Benamung erzeugt werden.

SELECT Column1 AS int1, Column2 AS int2, Column3 AS text1, Column4 AS int3, Column5 AS text2 ;
  FROM myRealData ;
  INTO CURSOR myJSONcursor ;
  READWRITE


Dieser neu erstellte Cursor wird nun an eine Funktion übergeben deren Aufgabe darin besteht, aus diesen Daten einen JSON String zu erstellen. Hierbei nimmt die Funktion zwei Parameter entgegen.

Parameter 1 ist der Name des zu verarbeitetenden Cursors und der optionale Parameter zwei erzwingt eckige Klammern die das Konstrukt umgeben. Bei mehr als einem Datensatz erfolgt dies allerdings automatisch.

Über den zweiten Parameter kann auch ein einfacher Array (also keine Name:Wert Paare) erzeugt werden. Dies ist in der Funktion jedoch nur zulässig, wenn der übergebene Cursor maximal einen Datensatz enthält. Andernfalls wird der Parameterwert 2 auf den Wert 1 zurückgesetzt.

Parameter #2

=0 (default)
Der generierte JSON String erhält automatisch eckigen Klammern wenn der übergebene Cursor mehr als einen Datensatz enthält.

=1
Erzwingt eckige Klammen, unabhängig von der Anzahl der Datensätze

=2
Erzeugt einen einfachen JSON Array. D.h. es werden keine Name:Wert Paare gebildet sondern eine einfache komma-separierte Wertliste die in eckige Klammern gepackt wird.

* // Param #1: Name of Cursor
* // Param #2: Mode
* //            0 = default
* //                this means, the JSON String gets square        
* //                brackets if more than one record is found.    
* //            1 = force square brackets                        
* //            2 = create JSON array only                        
* //                this means, only comma separated values        
* //                packed in square brackets
* //                IMPORTANT: only allowed when cursor has ONE 
* //                single record. Otherwise an object array has
* //                to be created!!!
FUNCTION Cursor2JSON as String
LPARAMETERS vCrsName as String, vForceSquareBrackets as Integer
    m.vForceSquareBrackets = EVL( m.vForceSquareBrackets , 0 )
    * // pure JSON Array only allowed bei single records
    IF m.vForceSquareBrackets = 2 AND RECCOUNT( m.vCrsName ) > 1
        m.vForceSquareBrackets = 1
    ENDIF 
    LOCAL lcJSON as String, liLoop as Integer
    SELECT ( m.vCrsName )
    AFIELDS( arrName , m.vCrsName )
    lcJSON = []
    * // loop through records
    SELECT( vCrsName )
    GO TOP 
    DO WHILE NOT EOF( vCrsName )
        IF m.vForceSquareBrackets <> 2
            lcJSON = lcJSON + IIF( RECNO( vCrsName ) > 1 , [,{] , [{] )
        ENDIF 
        * // loop through columns
        FOR liLoop = 1 TO ALEN( arrName , 1 )
            * // from 2nd run on we have to add a comma as seperator
            lcJSON = lcJSON + IIF( liLoop > 1 , [,] , [] )
            * // now add the columnname
            IF m.vForceSquareBrackets <> 2
                lcJSON = lcJSON + ["] + ALLTRIM( LOWER( arrName( liLoop , 1 ) ) ) + ["] + [:]
            ENDIF 
            luValue = EVALUATE( m.vCrsName + [.] + ALLTRIM( arrName( liLoop , 1 ) ) )
            * // now add the columvalue depending on its vartype
            DO CASE 
            CASE arrName( liLoop , 2 ) = [C]    && Character
                lcJSON = lcJSON + ["] + ALLTRIM( luValue ) + ["]
            CASE arrName( liLoop , 2 ) = [Y]    && Money
                lcJSON = lcJSON + CHRTRAN( TRANSFORM( luValue ) . [,] , [.] )
            CASE arrName( liLoop , 2 ) = [D]    && Date
                lcJSON = lcJSON + ["] + DTOC( luValue ) + ["]
            CASE arrName( liLoop , 2 ) = [T]    && Datetime
                lcJSON = lcJSON + ["] + TTOC( luValue , 3 ) + [.000Z] + ["]
            CASE arrName( liLoop , 2 ) = [F]    && Float
                lcJSON = lcJSON + CHRTRAN( TRANSFORM( luValue ) . [,] , [.] )
            CASE arrName( liLoop , 2 ) = [I]    && Integer
                lcJSON = lcJSON + TRANSFORM( luValue )
            CASE arrName( liLoop , 2 ) = [L]    && Logical
                lcJSON = lcJSON + TRANSFORM( IIF( luValue = .T. , [true] , [false] ) )
            CASE arrName( liLoop , 2 ) = [N]    && Numeric
                lcJSON = lcJSON + CHRTRAN( TRANSFORM( luValue ) . [,] , [.] )
            ENDCASE 
        ENDFOR 
        IF m.vForceSquareBrackets <> 2 
            lcJSON = lcJSON + [}]
        ENDIF 
        SKIP IN ( m.vCrsName )
    ENDDO
    * // in case of multiple records, we have to use squarebrackets
    * // as each record is a JSON object
    IF RECCOUNT( vCrsName ) > 1 OR m.vForceSquareBrackets  > 0
        lcJSON = "[" + lcJSON + "]"
    ENDIF 
    RETURN lcJSON
ENDFUNC


Auf diese Weise können auch mehrere in Relation stehende Cursor verschachtelt in einen JSON String gepackt werden. 

Im folgenden mal ein paar Codebeispiele, wie diese Funktion genutzt werden kann:

CLEAR 

* // cleanup                    
USE IN SELECT( [crsMydata] )
USE IN SELECT( [myJSONcursor] )
* // creating basic data        
CREATE CURSOR crsMydata ( column1 i, column2 i, column3 c(10), column4 i, column5 c(10))
INSERT INTO crsMydata ( column1 , column2 , column3 , column4 , column5 ) VALUES ( 1 , 11 , [aaa] , 1111 , [a] )
* // convert to JSON capability    
SELECT column1 AS int1, column2 AS int2, column3 AS text1, column4 AS int3, column5 AS text2 ;
  FROM crsMydata ;
  INTO CURSOR myJSONcursor ;
  READWRITE
* // now convert to JSON        
lcJSONdata = Cursor2JSON( [myJSONcursor] )
?[---------------------------------------------- ONE record as JSON object/collection]
?lcJSONdata

* // cleanup                    
USE IN SELECT( [crsMydata] )
USE IN SELECT( [myJSONcursor] )
* // creating basic data        
CREATE CURSOR crsMydata ( column1 i, column2 i, column3 c(10), column4 i, column5 c(10))
INSERT INTO crsMydata ( column1 , column2 , column3 , column4 , column5 ) VALUES ( 1 , 11 , [aaa] , 1111 , [a] )
INSERT INTO crsMydata ( column1 , column2 , column3 , column4 , column5 ) VALUES ( 2 , 22 , [bbb] , 2222 , [ab] )
INSERT INTO crsMydata ( column1 , column2 , column3 , column4 , column5 ) VALUES ( 3 , 33 , [ccc] , 3333 , [abc] )
INSERT INTO crsMydata ( column1 , column2 , column3 , column4 , column5 ) VALUES ( 4 , 44 , [ddd] , 4444 , [abcd] )
INSERT INTO crsMydata ( column1 , column2 , column3 , column4 , column5 ) VALUES ( 5 , 55 , [eee] , 5555 , [abcde] )
* // convert to JSON capability    
SELECT column1 AS int1, column2 AS int2, column3 AS text1, column4 AS int3, column5 AS text2 ;
  FROM crsMydata ;
  INTO CURSOR myJSONcursor ;
  READWRITE
* // now convert to JSON        
lcJSONdata = Cursor2JSON( [myJSONcursor] , 1 )
?[---------------------------------------------- MULTIPLE records as JSON object array]
?lcJSONdata


* // cleanup                    
USE IN SELECT( [crsMydata] )
USE IN SELECT( [myJSONcursor] )
* // creating basic data        
CREATE CURSOR crsMydata ( column1 i, column2 i, column3 i, column4 i, column5 i)
INSERT INTO crsMydata ( column1 , column2 , column3 , column4 , column5 ) VALUES ( 1 , 11 , 111 , 1111 , 11111 )
* // convert to JSON capability    
SELECT column1 AS int1, column2 AS int2, column3 AS int3, column4 AS int4, column5 AS int5 ;
  FROM crsMydata ;
  INTO CURSOR myJSONcursor ;
  READWRITE
* // now convert to JSON        
lcJSONdata = Cursor2JSON( [myJSONcursor] , 2 )
?[---------------------------------------------- ONE record as JSON array]
?lcJSONdata


* // cleanup                    
USE IN SELECT( [crsMydata] )
USE IN SELECT( [myJSONcursor] )
* // creating basic data        
CREATE CURSOR crsMydata ( column1 C(1), column2 C(2), column3 c(3), column4 c(4), column5 c(5))
INSERT INTO crsMydata ( column1 , column2 , column3 , column4 , column5 ) VALUES ( [a] , [aa] , [aaa] , [aaaa] , [aaaaa] )
* // convert to JSON capability    
SELECT column1 AS text1, column2 AS text2, column3 AS text3, column4 AS text4, column5 AS text5 ;
  FROM crsMydata ;
  INTO CURSOR myJSONcursor ;
  READWRITE
* // now convert to JSON        
lcJSONdata = Cursor2JSON( [myJSONcursor] , 2 )
?[---------------------------------------------- ONE record as JSON array]
?lcJSONdata


* // Param #1: Name of Cursor
* // Param #2: Mode
* //            0 = default
* //                this means, the JSON String gets square        
* //                brackets if more than one record is found.    
* //            1 = force square brackets                        
* //            2 = create JSON array only                        
* //                this means, only comma separated values        
* //                packed in square brackets
* //                IMPORTANT: only allowed when cursor has ONE 
* //                single record. Otherwise an object array has
* //                to be created!!!
FUNCTION Cursor2JSON as String
LPARAMETERS vCrsName as String, vForceSquareBrackets as Integer
    m.vForceSquareBrackets = EVL( m.vForceSquareBrackets , 0 )
    * // pure JSON Array only allowed bei single records
    IF m.vForceSquareBrackets = 2 AND RECCOUNT( m.vCrsName ) > 1
        m.vForceSquareBrackets = 1
    ENDIF 
    LOCAL lcJSON as String, liLoop as Integer
    SELECT ( m.vCrsName )
    AFIELDS( arrName , m.vCrsName )
    lcJSON = []
    * // loop through records
    SELECT( vCrsName )
    GO TOP 
    DO WHILE NOT EOF( vCrsName )
        IF m.vForceSquareBrackets <> 2
            lcJSON = lcJSON + IIF( RECNO( vCrsName ) > 1 , [,{] , [{] )
        ENDIF 
        * // loop through columns
        FOR liLoop = 1 TO ALEN( arrName , 1 )
            * // from 2nd run on we have to add a comma as seperator
            lcJSON = lcJSON + IIF( liLoop > 1 , [,] , [] )
            * // now add the columnname
            IF m.vForceSquareBrackets <> 2
                lcJSON = lcJSON + ["] + ALLTRIM( LOWER( arrName( liLoop , 1 ) ) ) + ["] + [:]
            ENDIF 
            luValue = EVALUATE( m.vCrsName + [.] + ALLTRIM( arrName( liLoop , 1 ) ) )
            * // now add the columvalue depending on its vartype
            DO CASE 
            CASE arrName( liLoop , 2 ) = [C]    && Character
                lcJSON = lcJSON + ["] + ALLTRIM( luValue ) + ["]
            CASE arrName( liLoop , 2 ) = [Y]    && Money
                lcJSON = lcJSON + CHRTRAN( TRANSFORM( luValue ) . [,] , [.] )
            CASE arrName( liLoop , 2 ) = [D]    && Date
                lcJSON = lcJSON + ["] + DTOC( luValue ) + ["]
            CASE arrName( liLoop , 2 ) = [T]    && Datetime
                lcJSON = lcJSON + ["] + TTOC( luValue , 3 ) + [.000Z] + ["]
            CASE arrName( liLoop , 2 ) = [F]    && Float
                lcJSON = lcJSON + CHRTRAN( TRANSFORM( luValue ) . [,] , [.] )
            CASE arrName( liLoop , 2 ) = [I]    && Integer
                lcJSON = lcJSON + TRANSFORM( luValue )
            CASE arrName( liLoop , 2 ) = [L]    && Logical
                lcJSON = lcJSON + TRANSFORM( IIF( luValue = .T. , [true] , [false] ) )
            CASE arrName( liLoop , 2 ) = [N]    && Numeric
                lcJSON = lcJSON + CHRTRAN( TRANSFORM( luValue ) . [,] , [.] )
            ENDCASE 
        ENDFOR 
        IF m.vForceSquareBrackets <> 2 
            lcJSON = lcJSON + [}]
        ENDIF 
        SKIP IN ( m.vCrsName )
    ENDDO
    * // in case of multiple records, we have to use squarebrackets
    * // as each record is a JSON object
    IF RECCOUNT( vCrsName ) > 1 OR m.vForceSquareBrackets  > 0
        lcJSON = "[" + lcJSON + "]"
    ENDIF 
    RETURN lcJSON
ENDFUNC


Wer lieber auf eine fertige Klasse zurückgreifen möchte wird auf gitHub (vormals CodePlex) fündig:

 
Die nfJson Programmbibliothek wird kontinuierlich gepflegt und erzeugt nicht nur JSON sondern kann dieses auch in VFP Cursor wandeln.

Hiermit sind die wichtigsten Vorarbeiten abgeschlossen. Im nächsten Schritt steht nun die Kontaktaufnahme mit einem REST basierenden Webservice an.


Links zu den restlichen Kapiteln:

Einführung / Introduction 

Teil 1: Abkürzungen und was sie bedeuten
           / Abbreviations and their meaning

Teil 2: Wer ReST sagt, sagt auch JSON
          / In for a ReST, in for a JSON

Teil 3: Leerzeichen und andere Entitäten
          / BLANKS and other entities

Teil 4: JSON und der goldene Konverter
          / JSON and the golden converter

Teil 5: Die Startvorbereitungen
          / preparations for launch

Teil 6: Kleine Denkpause gefällig?
          / in need of a reflection period?

Teil 7: Fertig machen zur Landung
          / preparing for landing