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