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