Für ein Projekt musste ich ein PDF erzeugen und habe das dann mit iText umgesetzt.
Um mich in die Technik einzuarbeiten habe ich mir ein paar Bilder von Pixabay heruntergeladen, ein Projekt auf GitHub angelegt und dann schrittweise ein PDF mit Bildern erzeugt:
Vor ca. zwei Jahren durfte ich einen Impusvortrag zum Thema Redis halten. Den Inhalt der Folien kopiere ich hierher.
Durch welches Problem bin ich auf Redis gestoßen?
• Migration einer Anwendung von SAP Application Server mit drei Application Server Instanzen auf zwei Apache Tomcat Server • Session Data ging durch Wechsel der Tomcat Server verloren • Lösung: Sticky Session [im LoadBalancer] • Alternative Lösung: Persistieren den Session in der Application Datenbank • • zB in PROJECT_XYZ umgesetzt • • Problem bei der aktuellen Application: DB hat bereits Performanceprobleme • Erweiterte alternative Lösung: • • Speichern der Session Daten in einer eigenen Datenbank • • -> Fragestellung: Gibt es für so ein Scenario spezialisierte Datenbanken?
[Anmerkung aus 2023: Es gibt beim Tomcat ein Clustering/Session Replication Feature, das hatten wir aber aus Gründen nicht in Erwägung gezogen]
Was ist Redis
Der Name Redis steht für Remote Dictionary Server.
In-Memory-Datenbank • Alle Daten werden direkt im Arbeitsspeicher gespeichert • Dadurch sehr kurze Zugriffszeiten • Auch bei großen, unstrukturierten Datenmengen
Key-Value-Store • Hohe Performanz • Dank einfacher Struktur leicht skalierbar • Zu jedem Eintrag wird ein Schlüssel erstellt, über den die Informationen dann wieder aufgerufen werden können.
Redis ist über Module erweiterbar. Mitbewerber der Key-Value-Datenbanken: Amazon DynamoDB
Weitere Optionen • Inkrementelle Vergrößerung oder Verkleinerung • Lebenszeit von Werten setzen • Mit append dem hinterlegten Wert einen weiteren hinzufügen • Einträge mit rename umbenennen
Heute durfte ich einen Impusvortrag zum Thema Quarkus halten. Den Inhalt der Folien incl. Kommentare und die Notizen der Live Demo kopiere ich hierher.
Quarkus
https://quarkus.io/
Was ist Quarkus?
• Open-Source-Framework • Um Anwendungen für eine moderne, Cloud-native Welt zu erstellen • Kubernetes-natives Java-Framework • auf GraalVM und HotSpot zugeschnitten • aus den besten Java-Bibliotheken und -Standards entwickelt
https://quarkus.io/about/
Was ist Quarkus?
Traditionelle Java-Stacks wurden für monolithische Anwendungen mit langen Startzeiten und großem Speicherbedarf in einer Welt entwickelt, in der es noch keine Cloud, Container und Kubernetes gab. Java-Frameworks mussten sich weiterentwickeln, um den Anforderungen dieser neuen Welt gerecht zu werden.
Quarkus wurde entwickelt, um Java-Entwicklern die Möglichkeit zu geben, Anwendungen für eine moderne, Cloud-native Welt zu erstellen. Quarkus ist ein Kubernetes-natives Java-Framework, das auf GraalVM und HotSpot zugeschnitten ist und aus den besten Java-Bibliotheken und -Standards entwickelt wurde. Ziel ist es, Java zur führenden Plattform in Kubernetes- und Serverless-Umgebungen zu machen und Entwicklern ein Framework zu bieten, das eine größere Bandbreite an verteilten Anwendungsarchitekturen abdeckt.
Vollständig und absolut Open Source
https://quarkus.io/about/
Ausgewählte Features
• Live-Coding mit Dev-Modus • Microprofile-Integration für Cloud-native Anwendungen • Nutzung von Quarkus für RESTful-Anwendungen • Serverless Funktionen mit Quarkus Funqy • Integration mit Datenbanken • Performance • Erweiterungen
Live-Coding mit Dev-Modus
Eine der herausragenden Eigenschaften von Quarkus ist die Möglichkeit des Live-Codings. Mit dem Dev-Modus können Entwickler Änderungen am Code vornehmen und diese Änderungen werden sofort in der laufenden Anwendung wirksam, ohne dass ein Neustart erforderlich ist. Dies beschleunigt den Entwicklungsprozess erheblich und ermöglicht eine iterative Entwicklung in Echtzeit.
Serverless Funktionen mit Quarkus Funqy
Eine API für verschiedenen FaaS-Umgebungen wie AWS Lambda, Azure Functions, Google Cloud Functions, Knative und Knative Events (Cloud Events), daher eine sehr einfache API.
https://quarkus.io/guides/funqy
Quarkus Funqy ist Teil der serverlosen Strategie von Quarkus und zielt darauf ab, eine portable Java-API zum Schreiben von Funktionen bereitzustellen, die in verschiedenen FaaS-Umgebungen wie AWS Lambda, Azure Functions, Google Cloud Functions, Knative und Knative Events (Cloud Events) eingesetzt werden können. Es ist auch als eigenständiger Dienst nutzbar.
Da es sich bei Funqy um eine Abstraktion handelt, die mehrere verschiedene Cloud-/Funktionsanbieter und Protokolle umfasst, muss es eine sehr einfache API sein und verfügt daher möglicherweise nicht über alle Funktionen, die Sie von anderen Remoting-Abstraktionen gewohnt sind. Ein schöner Nebeneffekt ist jedoch, dass Funqy so optimiert und so klein wie möglich ist. Das bedeutet, dass Funqy zwar ein wenig an Flexibilität einbüßt, dafür aber einen Rahmen bietet, der wenig bis gar keinen Overhead hat.
Integration mit Datenbanken
Quarkus bietet Erweiterungen für verschiedene Datenbanken, wie z.B. PostgreSQL, MySQL, MongoDB und viele mehr.
Performance
Erweiterungen
Live Demo
Download
https://code.quarkus.io/
Import into Eclipse IDE
Prerequisites
Eclipse öffnen, Terminal mit Ubuntu (WSL) öffnen
Java und Maven Versionen überprüfen:
java --version
echo $JAVA_HOME
mvn --version
# bei mir wird die Maven Version nicht angezeigt, wohl aber der Pfad der Installation, über den prüfe ich die Version
ls - lisah /opt/maven
cd /mnt/c/dev/workspace/code-with-quarkus/
clear
Start Quarkus
Starte Projekt:
./mvnw compile quarkus:dev
Internal Web Browser öffnen: http://localhost:8080/ „Visit the DEV UI“ funktioniert nicht im Eclipse Browser
Return String ändern und hello-Seite neu laden -> kein Neustart notwendig!
curl http://localhost:8080/hello
# In Klasse GreetingResource ändern von:
@Produces(MediaType.TEXT_PLAIN)
# nach:
@Produces(MediaType.APPLICATION_JSON)
Änderung wird auch hier ohne Neustart übernommen, um sie zu zeigen verwende ich HTTPie statt cURL:
http http://localhost:8080/hello
Um das Hot Deployment für JUnit Tests zu zeigen gehe ich in das Terminal, mit dem ich Quarkus gestartet habe und drücke "r" für "re-run test". Der Test schlägt fehlt, da ich in GreetingResource den Rückgabestring geändert hatte. Ich passe den zu erwartenden Wert in GreetingResourceTest an und drücke dann wieder "r" im Terminal. Alle Tests sind jetzt grün. Es war kein Neustart notwendig.
Contexts and Dependency Injection (CDI)
Klasse GreetingService hinzufügen:
package org.acme;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class GreetingService {
private String greeting = "Hallo";
public String greeting(String name) {
return greeting + " " + name;
}
}
Die Klasse GreetingResource erweitern:
package org.acme;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/hello")
public class GreetingResource {
@Inject
GreetingService service;
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "Hello from Welt";
}
@GET
@Produces(MediaType.TEXT_PLAIN)
@Path("/greeting/{name}")
public String greeting(String name) {
return service.greeting(name);
}
}
Und wieder ohne Neustart testen:
http http://localhost:8080/hello/greeting/Welt
Configuration
Klasse GreetingService ändern:
package org.acme;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class GreetingService {
@ConfigProperty(name = "greeting")
private String greeting;
public String greeting(String name) {
return greeting + " " + name;
}
}
Testen:
http http://localhost:8080/hello/greeting/Welt
Der Test schlägt fehl, da die Property greeting noch nicht gesetzt wurde, was wir jetzt nachholen:
greeting=Huhu
Testen:
http http://localhost:8080/hello/greeting/Welt
Jetzt funktioniert es und es kommt "Huhu Welt" zurück.
Staged Properties: In DEV Modus möchten wir eine andere Begrüßung sehen:
greeting=Huhu
%dev.greeting=Moin
Testen:
http http://localhost:8080/hello/greeting/Welt
Es funktioniert und es kommt "Moin Welt" zurück.
Metrics
Testen:
http http://localhost:8080/q/metrics
Fehler: Not Found
Mit Maven nachinstallieren (alternativ: Gradle oder Quarkus CLI):
Ich möchte eine lokale Oracle Datenbank mit Docker laufen lassen um so einige Sachen schnell lokal testen zu können. Hintergrund ist eine anstehende Cloud zu Cloud Migration einer bestehenden Anwendung, bei der zugleich die Oracle DB und Java aktualisiert werden wird.
Docker Image
Bei PostgreSQL war das mit der gedockerten Datenbank relativ einfach. Oracle macht es etwas schwieriger. Einfache Images, die man auf dem Docker Hub finden kann, existieren nicht. Statt dessen muss man ein GitHub Repository clonen und ein Shell Script ausführen, um ein Image zu erzeugen und in die lokale Registry zu schieben.
Frei verfügbar sind nur die Versionen Oracle Database 18c XE, 21c XE and 23c FREE. Ich entscheide mich, für die beiden Versionen 21c XE und 23c FREE das Image zu erzeugen und dann zuerst mit Version 23c FREE zu testen und ggf. später weitere Tests mit Version 21c XE machen zu können.
cd <workspace>
mkdir oracle
cd oracle
git clone https://github.com/oracle/docker-images.git
cd docker-images/OracleDatabase/SingleInstance/dockerfiles/
./buildContainerImage.sh -h
./buildContainerImage.sh -f 23.2.0
# Oracle Database container image for 'free' version 23.2.0 is ready to be extended:
#
# --> oracle/database:23.2.0-free
#
# Build completed in 608 seconds.
./buildContainerImage.sh -x 21.3.0
# Version 23.2.0 does not have Express Edition available.
Die Erzeugung des zweiten Images hat leider nicht funktioniert. Da das erste Image schon so lange gebraucht hat und ich das zweite Image nur proaktiv anlegen wollte, bin ich auch momentan nicht großartig motiviert, dem jetzt weiter nachzugehen. Version 23c FREE reicht erst einmal.
Image direkt von Oracle
Nach dieser Doku kann man das Image auch direkt aus der Oracle Registry ziehen. Zumindest für Oracle Database 23c Free – Developer Release.
Docker Container
Die Dokumentation hat einen speziellen Abschnitt für 23c FREE
Den Abschnitt auf jeden Fall gut ansehen, ich habe den Container mit folgendem Befehl erzeugt:
Die Wahl der Library fiel auf ZXing (“zebra crossing”), denn das ist die main library that supports QR codes in Java. und ich habe keine Anhaltspunkte finden können, warum ich eine andere Library nehmen sollte.
package deringo;
import java.awt.Desktop;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.NotFoundException;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
public class TestMain {
public static void main(String args[]) throws WriterException, IOException, NotFoundException {
String data = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam";
Path path = Paths.get("./test.png");
// Encoding charset to be used
String charset = "UTF-8";
Map<EncodeHintType, ErrorCorrectionLevel> hashMap = new HashMap<EncodeHintType, ErrorCorrectionLevel>();
// generates QR code with Low level(L) error correction capability
hashMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
// invoking the user-defined method that creates the QR code
generateQRcode(data, path, charset, hashMap, 200, 200);// increase or decrease height and width accodingly
System.out.println("QR Code created successfully.");
Desktop.getDesktop().open(path.toFile());
}
public static void generateQRcode(String data, Path path, String charset, Map<EncodeHintType, ErrorCorrectionLevel> map, int h, int w)
throws WriterException, IOException {
BitMatrix matrix = new MultiFormatWriter().encode(new String(data.getBytes(charset), charset),
BarcodeFormat.QR_CODE, w, h);
MatrixToImageWriter.writeToPath(matrix, getExtension(path), path);
}
/**
* Own function until we have this in JDK<br>
* Finally, there is a new method Path#getExtension available right in the JDK as of Java 21:<br>
* https://stackoverflow.com/questions/3571223/how-do-i-get-the-file-extension-of-a-file-in-java/74315488#74315488
*/
public static String getExtension(Path path) {
String extension = path.getFileName().toString().substring(path.getFileName().toString().lastIndexOf('.') + 1);
return extension;
}
}
ZXing und Docx4J
Der QR Code lässt sich schön als PNG generieren. Allerdings brauche ich den QR Code in einem Word Dokument. Das Word Dokument wird mit Docx4J generiert.
package deringo;
import java.awt.Desktop;
import java.io.File;
import java.nio.file.Files;
import org.docx4j.dml.wordprocessingDrawing.Inline;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.openpackaging.parts.WordprocessingML.BinaryPartAbstractImage;
import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart;
import org.docx4j.wml.Drawing;
import org.docx4j.wml.Jc;
import org.docx4j.wml.JcEnumeration;
import org.docx4j.wml.ObjectFactory;
import org.docx4j.wml.P;
import org.docx4j.wml.PPr;
import org.docx4j.wml.R;
public class TestMain {
public static void main(String[] args) throws Exception {
WordprocessingMLPackage wordPackage = WordprocessingMLPackage.createPackage();
MainDocumentPart mainDocumentPart = wordPackage.getMainDocumentPart();
mainDocumentPart.addStyledParagraphOfText("Title", "Welcome to my QR Code");
mainDocumentPart.addParagraphOfText("Welcome to my QR Code");
File image = new File("test.png" );
byte[] fileContent = Files.readAllBytes(image.toPath());
BinaryPartAbstractImage imagePart = BinaryPartAbstractImage
.createImagePart(wordPackage, fileContent);
Inline inline = imagePart.createImageInline(
"QR Code Image (filename hint)", "Alt Text", 1, 2, false);
P Imageparagraph = addImageToParagraph(inline);
mainDocumentPart.getContent().add(Imageparagraph);
File exportFile = new File("welcome.docx");
wordPackage.save(exportFile);
Desktop.getDesktop().open(exportFile);
}
private static P addImageToParagraph(Inline inline) {
ObjectFactory factory = new ObjectFactory();
P p = factory.createP();
R r = factory.createR();
p.getContent().add(r);
Drawing drawing = factory.createDrawing();
r.getContent().add(drawing);
drawing.getAnchorOrInline().add(inline);
// center image
PPr paragraphProperties = factory.createPPr();
Jc justification = factory.createJc();
justification.setVal(JcEnumeration.CENTER);
paragraphProperties.setJc(justification);
p.setPPr(paragraphProperties);
return p;
}
}
Das Ergebnis sieht brauchbar aus:
Allerdings habe ich die zuvor gespeicherte Datei verwendet. Ich möchte das aber on the fly machen, also ohne, dass ich den QR Code erst speichere und dann in das Word Dokument übernehme.
package deringo;
import java.awt.Desktop;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import javax.imageio.ImageIO;
import org.docx4j.dml.wordprocessingDrawing.Inline;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.openpackaging.parts.WordprocessingML.BinaryPartAbstractImage;
import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart;
import org.docx4j.wml.Drawing;
import org.docx4j.wml.Jc;
import org.docx4j.wml.JcEnumeration;
import org.docx4j.wml.ObjectFactory;
import org.docx4j.wml.P;
import org.docx4j.wml.PPr;
import org.docx4j.wml.R;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
public class TestMain {
public static void main(String[] args) throws Exception {
WordprocessingMLPackage wordPackage = WordprocessingMLPackage.createPackage();
MainDocumentPart mainDocumentPart = wordPackage.getMainDocumentPart();
mainDocumentPart.addStyledParagraphOfText("Title", "Welcome to my QR Code");
mainDocumentPart.addParagraphOfText("Welcome to my QR Code");
BinaryPartAbstractImage imagePart = BinaryPartAbstractImage
.createImagePart(wordPackage, getQRCode());
Inline inline = imagePart.createImageInline(
"QR Code Image (filename hint)", "Alt Text", 1, 2, false);
P Imageparagraph = addImageToParagraph(inline);
mainDocumentPart.getContent().add(Imageparagraph);
File exportFile = new File("welcome.docx");
wordPackage.save(exportFile);
Desktop.getDesktop().open(exportFile);
}
private static P addImageToParagraph(Inline inline) {
ObjectFactory factory = new ObjectFactory();
P p = factory.createP();
R r = factory.createR();
p.getContent().add(r);
Drawing drawing = factory.createDrawing();
r.getContent().add(drawing);
drawing.getAnchorOrInline().add(inline);
// center image
PPr paragraphProperties = factory.createPPr();
Jc justification = factory.createJc();
justification.setVal(JcEnumeration.CENTER);
paragraphProperties.setJc(justification);
p.setPPr(paragraphProperties);
return p;
}
public static byte[] getQRCode() throws Exception {
String data = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam";
// Encoding charset to be used
String charset = "UTF-8";
Map<EncodeHintType, ErrorCorrectionLevel> hashMap = new HashMap<EncodeHintType, ErrorCorrectionLevel>();
// generates QR code with Low level(L) error correction capability
hashMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
BitMatrix matrix = new MultiFormatWriter().encode(new String(data.getBytes(charset), charset),
BarcodeFormat.QR_CODE, 200, 200);
BufferedImage bi = MatrixToImageWriter.toBufferedImage(matrix);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(bi, "png", baos);
byte[] bytes = baos.toByteArray();
return bytes;
}
}
Allerdings ändert sich bei jedem Durchlauf der Initialization Vector (IV). Dadurch kann man den verschlüsselten Wert (der in dem Beispielcode nicht ausgegeben wird) nicht in einem zweiten Durchlauf wieder entschlüsseln. Das entspricht somit noch nicht dem, was ich brauche.
Mein Code
Ich möchte lediglich einen String verschlüsseln, über den QR-Code weitergeben und später soll der verschlüsselte Wert wieder entschlüsselt werden. Daher muss ich, anders als in dem Beispiel oben, Passwort, Salt und IV speichern.
Dazu brauche ich einen IV im String Format, den ich mir als erstes generieren lasse:
public static String generateIV() {
byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv);
return Base64.getEncoder().encodeToString(iv);
}
Passwort, Salt und IV-String werden in den Properties gespeichert:
Damit kann ich die Methoden zum Erstellen von SecretKey und IvParameterSpec erstellen und darauf aufbauend die Methoden encrypt(String) und decrypt(String):
Spring Initializr verwenden. Als Dependencies Spring Web und OAuth2 Client hinzufügen.
Zip-File herunterladen und in das Projekt entpacken. Warten bis Maven alle Dependencies heruntergeladen hat und anschließend Oauth2Application starten.
Unter http://localhost:8080 wird eine Login-Seite angezeigt:
Add a Home Page
Hier halte ich mich an den Guide. Die WebJars kannte ich bereits, aber noch nicht deren Locator. Den muss ich mal in meinem JSF Projekt ausprobieren.
In your new project, create index.html in the src/main/resources/static folder. You should add some stylesheets and JavaScript links so the result looks like this:
None of this is necessary to demonstrate the OAuth 2.0 login features, but it’ll be nice to have a pleasant UI in the end, so you might as well start with some basic stuff in the home page.
If you start the app and load the home page, you’ll notice that the stylesheets have not been loaded. So, you need to add those as well by adding jQuery and Twitter Bootstrap:
The final dependency is the webjars "locator" which is provided as a library by the webjars site. Spring can use the locator to locate static assets in webjars without needing to know the exact versions (hence the versionless /webjars/** links in the index.html). The webjar locator is activated by default in a Spring Boot app, as long as you don’t switch off the MVC autoconfiguration.
With those changes in place, you should have a nice looking home page for your app.
Oauth2Application starten. Mir wird wieder die Login-Seite angezeigt. Ich nehme die spring-boot-starter-oauth2-client Dependency aus der pom.xml heraus, starte Oauth2Application neu und sehe eine leere Seite.
Mir ist nicht ganz klar, woran das liegt, also mache ich erstmal weiter mit dem Guide, in der Hoffnung dass es am Ende laufen wird.
Add a New GitHub App
Webhook: Active Flag deaktivieren
MÖÖÖÖÖÖP FALSCH
Ich habe oben eine GitHub App angelegt und keine OAuth App. Also nochmal von vorn:
In einem anderen Tab des Browsers war ich bereits in GitHub eingeloggt:
Wenn ich den Seitenaufruf in einem neuen privaten Browserfenster tätige, sieht die Anmeldung so aus:
In beiden Fällen wird anschließend eine leere Seite angezeigt:
Auch die index.html bleibt leer:
Also irgendwas ist da noch nicht richtig. Anscheinend hatte ich die index.html nicht richtig gespeichert, die Datei war noch leer. Also den Code hinzugefügt und gespeichert.
Anschließend funktioniert es auch:
Sehr schön, da kann der Zukunftsingo sich ja freuen, hier weiter herumzuexperimentieren, sobald wieder etwas Zeit ist.
Ein Report soll als Word Dokument (.docx) verfügbar gemacht werden.
Dazu wird zuerst eine Word Datei als Template erstellt mit Platzhaltern an den entsprechenden Stellen. Die Ersetzung einzelner Wörter ist relativ trivial, das Befüllen einer Tabelle mit n Zeilen stellte sich dann etwas kniffliger heraus. Diese Notizen und das dazugehörige Beispielprojekt dienen als Erinnerungsstütze, falls der Zukunftsingo nach dem aktuellen Projekt noch einmal etwas mit docx4j machen darf.
Word Template erstellen
Word mit einem leeren Dokument öffnen, einen Platzhalter für eine Überschrift, einen Text und eine Tabelle hinzufügen.
Die Überschriften der Tabelle sind fix und werden nicht ersetzt. Die Anzahl der Zeilen in dem Report ist dynamisch. Die Tabelle erhält keine Zeilen, diese werden später hineingeneriert.
Würde der Report eine Tabelle mit einer fixen Anzahl von Zeilen vorsehen, könnte man die ganze Tabelle in das Dokument eintragen und und später die einzelnen Werte per Wortersetzung hineingenerieren.
Die Tabelle bekommt noch einen Titel, um sie so eindeutig identifizieren zu können. Beispielsweise wenn das Dokument mehrere Tabellen enthält.
Abschließend unter dem Namen template.docx als Word-Dokument speichern, nicht als Strict Open XML-Dokument!
Projekt einrichten
Ein neues Maven Projekt anlegen.
Die Datei template.docx nach src/main/resources kopieren.
Java Klasse anlegen, die das Template läd und eine neue Zieldatei im tmpdir mit teilgeneriertem Name anlegt. Dadurch erhalten wir bei jedem Durchlauf eine neue Datei, um sie ggf. noch einmal miteinander vergleichen zu können, und das Aufräumen übernimmt das Betriebssystem für uns.
Als Dummyaction wird das Template in die Zieldatei kopiert. An dieser Stelle kommt im nächsten Schritt Docx4J ins Spiel um das Template zu befüllen.
Im letzten Schritt lassen wir uns die Zieldatei mit Word öffnen um so direkt das Ergebnis überprüfen zu können.
Mit dem Projekt speichere ich den Weg, wie ich meine Kopie der Artenschutzdatenbank aufgebaut habe. So kann jederzeit eine neue Version der Artenschutzdatenbank erstellt werden, beispielsweise nach einem Update der Original Datenbank. Das Projekt ist weder universell, flexibel, konfigurierbar oder sonst was, sondern dient nur diesem einen Zweck.
Analyse
Auf der WISIA Seite kann man eine Recherche starten, zB zu "testudo":
Von dort gelangt man auf die jeweilige Seite einer Art, zB "Testudo hermanni":
Eine Analyse des HTML Source Codes der Seite zeigt, dass die Seite über ein Frameset zusammengebaut ist und der rechte Teil mit den Informationen eine eigene Seite ist. Diese Seite wird über einen eindeutigen Parameter, der Knoten ID, aufgerufen.
Für Testudo hermanni lautet die Knoten ID: 19442. Der Aufruf der Seite: https://www.wisia.de/GetTaxInfo?knoten_id=19442:
https://www.wisia.de/GetTaxInfo?knoten_id=19442
Auf der Seite finden sich strukturiert alle Informationen zu der Art. Leider ist es technisch nicht so, dass es eine leere Vorlage der Seite ist, die anschließend zB über einen REST Aufruf, der ein JSON Objekt zurückgibt, gefüllt wird. Das hätte es mir bedeutend einfacher gemacht. So muss ich für jede Art die komplette Seite laden und die Informationen selbst extrahieren.
Ablauf
flowchart TD
A[Seite herunterladen] -->|speichern| B
B[Informationen extrahieren] -->|speichern| C
C[Daten validieren] --> D
D[Daten verarbeiten] --> E
E[Daten exportieren] --> F
E --> G
F[SQL Script]
G[serialisierter Objekt Graph]
In einem ersten Schritt lade ich die jeweilige Seite einer Art auf meinen Arbeitsrechner herunter und speichere sie. Anschließend kann ich mit der gespeicherten Seite weiter arbeiten und muss sie nicht jedesmal neu ziehen, um darauf zuzugreifen.
Die Seiten werden als GZIPte Objekte gespeichert. Am Ende hatte ich über 7 GB gespeichert, bei einer Kompression auf ca. 25% habe ich also ca. 30 GB heruntergeladen.
Informationen extrahieren
Im nächsten Stritt werden die Informationen der Seite extrahiert und gespeichert.
Die Informationen werden als GZIPte Objekte gespeichert. Die gespeicherten Objekte haben eine Größe von knapp 43 MB.
Für das Verarbeiten der Seite habe ich HtmlUnit verwendet.
Seiten ohne gültigem Namen werden aussortiert, zu deren Knoten ID ist keine Art zugeordnet.
Seiten mit gültigem Namen aber ohne Taxonomie werden ebenfalls aussortiert.
Der gültige Name / wissenschaftliche Name ist nicht eindeutig, daher muss die eindeutige Knoten ID weiter verwendet werden.
Bei der Taxonomie bin ich davon ausgegangen, dass die Pfade alle eindeutig sind. Ich war überrascht, dass dem nicht so ist. Es existieren einige Einträge mit mehreren Elternknoten. Beispielsweise Gomphus kann über Gomphidae, aber auch über Gomphaceae erreicht werden. Das ist aber anscheinend korrekt, denn ich fand folgendes:
Duplicate name. This name, above species rank, is duplicated within the NCBI classification
Aus den extrahierten Daten muss ich eine Struktur ableiten und diese dann dorthin überführen.
Die verarbeiteten Daten werden als ein serialisiertes, gezippte Objekte gespeichert. Dieses ist weniger als 2MB groß und kann in wenigen Sekunden geladen werden.
Daten exportieren
Ursprünglich hatte ich die Idee, ein SQL-Schema zu schreiben und die Daten als INSERT-INTO-Scripte generieren zu lassen. Kann vielleicht nochmal kommen.
Letztendlich habe ich aber die Objekt Datei verwendet.
Intern benutzt Java UTF-16, "extern" wurde bisher standartmäßig das Encoding des Betriebssystems, zB Windows-1252, verwendet. Das hatte in der Vergangenheit immer wieder zu lustigen Encoding Problemen geführt, so dass ich schon lange meine Projekte und IDE Einstellungen auf UTF-8 umgestellt hatte. Ausnahme sind die Property-Dateien, die erst ab Java 9 UTF-8 können. Und Java 9 ist noch lange nicht in allen Projekten verfügbar.
Ich habe ein kleines Testprojekt aufgesetzt und dafür Java 19 verwendet. Dabei stieß ich irgendwann auf ein Encoding Problem, dass sich recht einfach nachstellen ließ:
package deringo;
public class TestMain {
public static void main(String[] args) {
String s = "abc äüß def";
System.out.println(s);
}
}
Output:
Projekt auf Java 17 umgestellt und der Output ist korrekt:
Die IDE ist Eclipse und der Bug ist bekannt: Bug 579383
Mit Java 17 wurde, als Vorbereitung auf JEP400, eine neue System Property eingeführt: native.encoding