APIs oder vollständig ausgesprochen Application Programming Interfaces sind schon eine wirklich feine Sache. Sie können relativ einfach in bestehende Programme eingebunden werden und stellen über eine vordefinierte und stabile Schnittstelle die Datenversorgung zu einem bestimmten fachlichen Sachverhalt sicher. Ebenso vorteilhaft ist, dass APIs ihre Antworten einem nicht in Form von undefinierbarem Datenbrei vor die Füße werfen, der erstmal aufwändig geparst werden muss, sondern fein säuberlich als einfach verdauliche XML / JSON Datenstrukturen.
Doch was tun wir, wenn es in der fachlichen Domäne, in der wir uns bewegen, keine bestehende API gibt? Müssen wir in solchen Fällen auf automatisierte Datenversorgung verzichten? Nicht immer. Je nach Sachverhalt gibt es Möglichkeiten, sich eine eigene Pseudo-API zu bauen.
Lasst uns das Thema an einem konkreten Beispiel aus meiner Praxis anschauen. In einem Kundenprojekt gibt es nämlich die Notwendigkeit, Fusionen von Krankenkassen zeitnah mitzubekommen. Liegt eine Krankenkassenfusion vor, muss in diesem Fall eine bestimmte fachliche Anpassung am zentralen Schlüsselverzeichnis der Anwendung vorgenommen werden. Die Anpassung an sich ist nicht das Problem. Doch wie stellt man sicher, dass man jede Krankenkassenfusion überhaupt mitbekommt? Natürlich kann man in regelmäßigen Abständen eine kurze Internetrecherche betreiben – Krankenkassenfusionen erfolgen schließlich nicht von heute auf morgen, sondern haben einigen zeitlichen Vorlauf. Doch jeder, der mit Softwareentwicklern zu tun hat, weiß eigentlich schon vorab, dass so eine Lösung nicht in Frage kommt. Softwareentwickler haben nun mal eine gesunde Abneigung gegen monotone und wiederkehrende Tätigkeiten – auch dann, wenn sie nicht einen selbst betreffen.
Datengrundlage
Eine Zeitlang fand ich für diese Problematik jedoch keine wirklich zufriedenstellende Lösung. So hatte ich mit meinem Kunden die Vereinbarung getroffen, dass er das Thema Fusionen selbst im Blick behält und mich im Fusionsfall informiert. Doch eines Tages fiel mir eine einfach zu realisierende Lösung plötzlich ein. Eine der Aufgaben in meiner hauptberuflichen Tätigkeit darin besteht, regelmäßig Beitragsdaten von Krankenkassen in SAP HCM zu importieren, um deren Beitragsdaten für Gehaltsabrechnungsläufe aktuell zu halten. Diese Daten werden regelmäßig vom Unternehmen namens ITSG GmbH veröffentlicht, das IT-Dienstleistungen für Krankenkassen erbringt. Die Analyse dieser XML-Dateien hat ergeben, dass neben den Beitrags- und Konto- und Stammdaten aller Krankenkassen, dort mit etwas XPath-Akrobatik herausgefunden werden kann, welche Krankenkasse wann mit welcher Kasse fusioniert hat.
Das würde bedeuten, dass wenn ich eine Kasse aufbaue, die selbstständig die oben genannte Datei herunterlädt, sie entzippt, einliest, ein XPath-Ausdruck darauf anwendet und das Ergebnis zurückgibt, ich mehr oder weniger eine Pseudo-API habe.
Warum Pseudo-API?
- weil die nicht webbasiert, sondern ausschließlich lokal aufruf- und ausführbar ist.
- weil sie keine eigene Datenhaltung hat, sondern die Daten vorab aus dem öffentlich verfügbaren Download bezieht.
Warum Pseudo-API?
- weil sie die Komplexität der fachlichen Logik wegabstrahiert und eine einfach zu nutzende Schnittstelle bereitstellt.
Implementierung
Schauen wir uns mal die Implementierung der Klasse an und gehen Methode für Methode durch.
Konstruktor-Methode (Class_Initialize())
Die Konstruktormethode wird automatisch aufgerufen, wenn ein Objekt der API-Klasse erzeugt wird. Sie prüft als erstes, ob das Arbeitsverzeichnis der Klasse, das sich unterhalb der AddIn-Verzeichnisses befindet, wirklich existiert. Denn ohne dieses Arbeitsverzeichnis kann die Klasse ihren Dienst nicht verrichten.
Private c_strArbeitsverzeichnisPfad As String
Private c_strPfadZipArchiv As String
Private c_objXmlBeitragsdatei As MSXML2.DOMDocument
Private c_objBrowser As MSXML2.ServerXMLHTTP
Private c_objShell As Object
Private Const c_strURL As String = "https://stammdatendatei.gkv-ag.de/Stammdatendatei.zip"
Private Const c_strVerzeichnisName As String = "api"
Private Const c_strZipArchivDateiname As String = "Archiv.zip"
Private Sub Class_Initialize()
c_strArbeitsverzeichnisPfad = ThisWorkbook.Path & "\" & c_strVerzeichnisName & "\"
If Dir(c_strArbeitsverzeichnisPfad, vbDirectory) = "" Then
Err.Raise 512, TypeName(Me), "Das Arbeitsverzeichnis " & c_strArbeitsverzeichnisPfad & " existiert nicht!"
End If
c_strPfadZipArchiv = c_strArbeitsverzeichnisPfad & c_strZipArchivDateiname
Call arbeitsverzeichnisBereinigen()
Call beitragsdatendateiHerunterladen()
End Sub
Aufräum-Methode (arbeitsverzeichnisBereinigen())
Der Name dieser Methode verrät schon, was diese tut. Bevor die neue Beitragsdatendatei heruntergeladen und entzippt wird, wird das Verzeichnis erstmal bereinigt, indem alte zip-Archive und alte Beitragsdatendatei gelöscht werden.
Private Sub arbeitsverzeichnisBereinigen()
Dim strXmlDateiName As String
On Error GoTo fehlerBeimLoeschen
If Dir(c_strPfadZipArchiv) <> "" Then
Kill c_strPfadZipArchiv
End If
Do While getXmlDateiName() <> ""
Kill c_strArbeitsverzeichnisPfad & getXmlDateiName()
Loop
Exit Sub
fehlerBeimLoeschen:
Err.Raise 512, TypeName(Me), "Beim Bereinigen des Arbeitsverzeichnisses ist ein Fehler aufgetreten!" & vbCrLf & Err.Description
End Sub
Herunterladen-Methode (beitragsdatendateiHerunterladen())
Diese Methode ist das Herzstück der Klasse und übernimmt die tatsächliche Datenbeschaffung. Dies erfolgt, indem ein Browserobjekt den Request an die Download-URL von ITSG absetzt. Die vom Server empfangenen Daten (das Zip-Archiv), die in Form eines Bytestroms vorliegen, werden im Arbeitsverzeichnis abgelegt. Anschließend wird ein Shellobjekt aufgebaut, welches mit Hilfe eines Powershell-Befehls das heruntergeladene Archiv entzippt und somit die XML-Datei im Arbeitsverzeichnis ablegt.
Am Ende wartet die Methode eine Sekunde, bevor die XML-Datei ins DomObjekt eingelesen wird. Ab diesem Zeitpunkt befindet sich die Beitragsdatendatei im Hauptspeicher, sodass darauf tatsächliche Selektionen in Form von XPath-Ausdrücken abgesetzt werden können.
Private Sub beitragsdatendateiHerunterladen()
Dim bytDateiinhalt() As Byte
Dim strAufrufparameter As String
Set c_objBrowser = New MSXML2.ServerXMLHTTP
c_objBrowser.Open "GET", c_strURL, False
c_objBrowser.send
If c_objBrowser.Status <> 200 Then
Err.Raise 512, TypeName(Me), "Beim Herunterladen des Archivs ist ein Problem aufgetreten"
End If
bytDateiinhalt = c_objBrowser.responseBody
Open c_strPfadZipArchiv For Binary As #1
Put #1, , bytDateiinhalt()
Close #1
Set c_objShell = CreateObject("WScript.Shell")
strAufrufparameter = "powershell.exe Expand-Archive -Path '" & c_strPfadZipArchiv & "' -DestinationPath '" & c_strArbeitsverzeichnisPfad & "'"
c_objShell.Run strAufrufparameter, 0
Application.Wait Now + TimeSerial(0, 0, 1)
Set c_objXmlBeitragsdatei = New MSXML2.DOMDocument
c_objXmlBeitragsdatei.Load (c_strArbeitsverzeichnisPfad & getXmlDateiName())
If c_objXmlBeitragsdatei.parseError <> 0 Then
Err.Raise 512, TypeName(Me), "Beim Laden der Beitragsdatei ist ein Fehler aufgetreten!" & vbCrLf & Err.Description
End If
End Sub
Private Function getXmlDateiName() As String
getXmlDateiName = Dir(c_strArbeitsverzeichnisPfad & "*.xml")
End Function
Fusionserkennungsmethode (LiegtFusionVor())
Aktuell ist es die einzige fachliche Methode dieser API, die nur die Aufgabe hat, die Info zurückzugeben, ob zur übergebenen Betriebsnummer eine Fusion bekannt. ist. Die liegt dann vor, wenn es zur gegebenen Betriebsnummer mindestens einen Knoten Einzugsstelle gibt, der ein Attribut nachfolge_bn aufweist.
Da sich die komplette Beitragsdatendatei im Speicher befindet, lassen sich natürlich auch komplexere Selektionen durchführen. So wäre es beispielsweise möglich, zur gegebenen Betriebsnummer die Nachfolgekasse zu ermitteln. Dabei wäre jedoch zu beachten, dass es zur übergebenen Betriebsnummer zwar eine Nachfolgekrankenkasse geben kann, die jedoch wiederum selbst bereits eine Fusion hinter sich haben kann. Am einfachsten kann diese Problematik mit Hilfe von rekursiven Funktionsaufrufen gelöst werden, indem die Beitragsdatendatei anhand der Betriebsnummer solange gelesen wird, bis der Knoten /Stammdatendatei/Einzugsstelle gefunden wird, der kein Attribut nachfolge_bn aufweist.
Public Function LiegtFusionVor(betriebsnummer As String) As Boolean
Dim nodKasse As MSXML2.IXMLDOMNode
'nach dieser Betriebsnummer wird dann in der ITSGXML gesucht
Set nodKasse = c_objXmlBeitragsdatei.SelectSingleNode("/Stammdatendatei/Einzugsstelle[@bbnr=" & Chr(39) & betriebsnummer & Chr(39) & "]")
If nodKasse Is Nothing Then
LiegtFusionVor = False
Exit Function
End If
If nodKasse.SelectSingleNode("@nachfolge_bn") Is Nothing Then
LiegtFusionVor = False
Exit Function
End If
LiegtFusionVor = True
End Function
Nutzung der Pseudo-API
Die Einfachheit der API-Nutzung erkennt man an diesem Codeschnipsel. Damit erstellt man eine Instanz der API und ruft übergibt der Methode LiegtFusionVor() die entsprechende Betriebsnummer. Daraufhin erhält man die Antwort, ob eine Fusion vorliegt. Spoileralarm – der Methodenaufruf liefert TRUE zurück – die Kasse hat fusioniert und zwar Ende 2022. Da ist nämlich die BKK Stadt Augsburg in Audi BKK aufgegangen.
Dim clsAPI As klasseKrankenkassenfusionInfoAPI.KrankenkassenfusionInfoAPI
Set clsAPI = klasseKrankenkassenfusionInfoAPI.getKlasseninstanz()
MsgBox clsAPI.LiegtFusionVor("81211334")
In meinem konkreten Anwendungsfall geschieht die Nutzung der Pseudo-API natürlich nicht auf der Einzelsatzebene. Stattdessen wird beim Start der Hauptanwendung das zentrale Schlüsselverzeichnis gelesen und Kasse für Kasse durchiteriert. Für jede Kasse wird die Methode LiegtFusionVor() mit der Betriebsnummer der Kasse aufgerufen. Alle Kassen, für die eine Fusion vorliegt, werden protokolliert und als ToDo angezeigt.
Fazit
Wie man sieht, ist der Aufbau einer Pseudo-API recht einfach und ermöglicht eine automatisierte Datenversorgung. Die Voraussetzung hierfür natürlich ist, dass es entsprechende öffentliche Downloadmöglichkeiten gibt, idealerweise natürlich in strukturierter Form.
Die vorgestellte API ist noch nicht ganz optimal realisiert – so wird der Donwload / das Enzippen immer durchgeführt, wenn eine neue Instanz der Klasse erstellt wird. Dies kostet natürlich Laufzeit und Datenverkehr – zumal die ITSG die Datei, soweit ich es mitbekommen habe, lediglich 2 bis 3 Mal pro Monat aktualisiert. So werde ich bei der nächsten Anpassung die Logik etwas aufbohren, indem ich zuerst prüfe, ob die aktuell heruntergeladene Datei mit der zuletzt herunterladenden übereinstimmt. Ist es der Fall, dann kann die bisherige Datei nach wie vor verwendet werden. Dies werde ich entweder realisieren, indem ich den Vergleich anhand der Hashwerte durchführe oder anhand des im Archiv hinterlegten Namen der XML-Datei, an dem man seinen Erstellungszeitpunkt ohne Weiteres erkennen kann.