08 February 2017

Der Beitrag dürfte auch den Namen «INTERLIS leicht gemacht #14» erhalten, da er sehr schön die Vorzüge einer model-driven GDI mit INTERLIS zeigt.

Die Idee mit Gradle Datenflüsse zu orchestrieren resp. aus einzelnen immer wiederkehrenden Schritten verschiedene Jobs zusammenstöpseln, wurde in diesem Beitrag erläutert. Eine Herausforderung, die auftritt, ist der Umgang mit Wiederholungen. Wenn es nun häufig darum gehen soll, Daten von A nach B zu kopieren und diese in eine Datenbank zu importieren, merkt man sofort, dass auf dem fremden Server ja nicht nur eine Datei liegt, die es zu importieren gilt, sondern viele. Jetzt könnte man natürlich das build-File einfach mehrfach kopieren. Bei zwei Dateien geht das ja vielleicht noch, aber bei 109 Gemeinden mit je einem AV-Datensatz will man mit diesem Ansatz nie mehr was verändern. Oder die einzelnen Schritte sind so ausgelegt, dass sie nicht nur mit einer Datei umgehen können, sondern mit einer beliebigen Anzahl. Das ist wahrscheinlich komplizierter zu programmieren und ich finde es «schöner», wenn die einzelnen Steps möglichst atomar sind. Vor allem möchte ich aber die Ressource kennen, mit der ich mich während des Jobs beschäftigen muss. Was wenn z.B. auf dem FTP-Server eine unbekannte 10GB-Datei liegt und einfach alles vom FTP-Server herunterladen wird und versucht wird zu importieren? Uncool. Also muss was her, dass zwar tendenziell auf die einzelne Datei zielt aber es soll so etwas geben wie eine Forschleife für die Wiederverwendbarkeit von Jobs (mit anderen Parameter).

Gradle kennt Multiprojekte. Es gibt ein Root-Projekt und Subprojekte. Das Root-Projekt soll in unserem Fall dazu dienen die 1 bis n (identischen) Subprojekte zu konfigurieren und auszuführen.

Für die Probe aufs Exempel will ich Daten, die in unserer model-driven GDI, erfasst werden täglich prüfen. Dazu muss ich die Daten aus der PostgreSQL-Datenbank nach INTERLIS mit ili2pg exportieren und anschliessend mit ilivalidator auf ihre Modellkonformität prüfen. Genau genommen reicht der Export mit ili2pg, da die ilivalidator-Prüfbibliotheken bequemerweise bereits eingebaut sind. Trotz den praktischen QGIS-Editier-Widgets und -Formularen sind Fehler bei der Erfassung schnell passiert. Vor allem wenn der Benutzer Beziehungen manuell korrigiert und erfasst. Darum lohnt sich dieses tägliche Validieren. Insbesondere können so auch Geometriefehler gefunden werden.

Zum Testen stehen zwei kleinere Modelle zur Verfügung: Hoheitsgrenzen und die Nachführungskreise der amtlichen Vermessung.

Die einzelnen Subprojekte müssen in Gradle in der Datei settings.gradle definiert werden:

include ':av_hoheitsgrenzen'
include ':av_nachfuehrungskreise'

Die Namen der Subprojekte sind frei wählbar. Weil aber diese Namen über ein Property im build-File ansprechbar sind, wählt man sinnigerweise etwas aus, das man im weiteren Verlauf noch verwenden kann. Diese Subprojekte befinden sich «eigentlich» in Unterordnern. Weil wir aber sowieso keinen Code in diesen Unterordnern resp. Subprojekten haben, müssen diese Ordner auch nicht existieren.

Nun kommt das erste Mal die Magie zum Vorschein. Im Haupt-Build-File build.gradle können den Subprojekten Properties mitgegeben werden. Wir müssen ja z.B. wissen in welchem Datenbankschema die Daten liegen und welches INTERLIS-Datenmodell verwendet werden muss.

project(':av_hoheitsgrenzen') {
    ext.dbschema = "av_hoheitsgrenzen"
    ext.ilimodels = "SO_Hoheitsgrenzen_20170203"
}

project(':av_nachfuehrungskreise') {
    ext.dbschema = "av_nachfuehrungskreise"
    ext.ilimodels = "SO_AV_Nachfuehrungskreise_20161126"
}

Wo werden die Daten jetzt aber exportiert und geprüft? Gleich nachfolgend, hier:

subprojects {
    apply from: "${rootDir}/validation.gradle"
}

Das bedeutet, dass für jedes Subprojekt das Build-Skript validation.gradle ausgeführt wird. Und in genau diesem validation.gradle findet der eigentliche Validierungs-Job statt:

import ch.so.agi.gdi.tasks.io.InterlisExportTask

ext {
    exportDir = "/opt/tmp/validation/"
}

task validateData(type: InterlisExportTask) {
    host = db_host
    port = db_port
    database = db_database
    usr = db_usr
    pwd = db_pwd

    schema = dbschema
    models = ilimodels

    xtfFileName = exportDir + project.name + ".xtf"
    logfile = exportDir + project.name + ".log"
}

task cleanUp(type: Delete) {
    outputs.upToDateWhen {false}

    onlyIf {
        validateData.state.failure == null
    }

    delete fileTree(dir: exportDir, include: "**/${project.name}.*")
}

validateData.finalizedBy('cleanUp')

Zuerst wird der selber geschriebene - auf ili2pg basierende - InterlisExportTask importiert. Anschliessend wird das Exportverzeichnis definiert. Der Task validateData ist vom Typ InterlisExportTask und prüft gleich während des Exportierens die Daten. Falls sie fehlerhaft sind, ist der Build nicht erfolgreich. Nach dem Validieren werden die exportierten Daten wieder gelöscht. Dies macht der cleanUp-Task vom Typ Delete. Dieser Typ ist bereits Bestandteil von Gradle und musste nicht selber geschrieben werden. Gelöscht werden die Daten aber nur, falls die Validierung erfolgreich war: onlyIf in Verbindung mit validateData.state.failure == null.

Zu guter Letzt möchten wir mit einer E-Mail benachrichtigt werden, falls etwas schief lief. Dies können wir im Root-Build-File mit dem buildFinished-Hook erledigen. Das ist dann kein Task, denn wir aufrufen, sondern ein paar Zeilen selbst erklärendes Groovy pur.

import org.apache.commons.mail.DefaultAuthenticator
import org.apache.commons.mail.Email
import org.apache.commons.mail.SimpleEmail

gradle.buildFinished { buildResult ->
    println "\nBUILD FINISHED"

    if (buildResult.failure) {
        logger.error("build failure - " + buildResult.failure)
    }

    if (buildResult.failure) {
        def message = buildResult.failure.getMessage()

        Email email = new SimpleEmail()
        email.setHostName(email_host)
        email.setSmtpPort(Integer.parseInt(email_port))
        email.setAuthenticator(new DefaultAuthenticator(email_login, email_password))
        email.setSSLOnConnect(true)
        email.setFrom(email_from)
        email.setSubject(project.name)
        email.setMsg(message)
        email.addTo(email_receiver)
        email.send()
    }
}

Will man einzelne Subprojekte ausführen, muss man den vollständigen Pfad (mit Doppelpunkt getrennt) zum Task angeben. In unserem Fall für die Hoheitsgrenzen: gradle :av_hoheitsgrenzen:validateData. Oder aber wenn wir alle Subprojekte am Stück ausführen lassen wollen: gradle validateData. Falls jetzt das erste Subprojekt einen Fehler wirft, wird das zweite nicht mehr ausgeführt. Dieses Verhalten können wir mit der Option --continue ändern.

Ein weiteres kleines Goodie ist die Option --profile. Das liefert eine nette HTML-Seite mit Informationen zum Build.

Interessanter ist so eine Auswertung aber für ein anderes Projekt: Von map.geo.admin.ch werden sämtliche 230 frei verfügbaren AV-Datensätze herunterladen und mit ilivalidator geprüft. Da gibt der Report schon ein bisschen mehr her. Mit der Performanz von ilivalidator darf man schon mal ganz zufrieden sein. 230 AV-Datensätze in 1:11h. Die grafische Aufbereitung der Resultate gibt es hier, die natürlich täglich mittels cronjob und Gradle-Build-File nachgeführt werden.

Posted by Stefan Ziegler. | KGDI , GDI , Gradle , Groovy , Java , INTERLIS , Datenintegration , know-your-gdi