Categories
Database Development Java

DBF Datei

Die Sourcen des DBFSample finden sich wie immer im GitHub.

Das DBFSample ist ein PoC um eine DBF Datei mit Java verarbeiten zu können.

Im Projekt haben wir einige DBF Dateien erhalten, deren Daten wir importieren/verarbeiten müssen. Das soll nicht meine Aufgabe sein, aber ich möchte für den Fall vorbereitet sein, dass ich dabei unterstützen darf.

Ich brauche also erstmal nur verstehen, was eine DBF Datei ist und wie ich grundlegend damit arbeiten kann.

Was ist eine DBF Datei

Eine DBF-Datei ist eine Standarddatenbankdatei, die von dBASE, einer Datenbankverwaltungssystemanwendung, verwendet wird.
Es organisiert Daten in mehreren Datensätzen mit Feldern, die in einem Array-Datentyp gespeichert sind.

Aufgrund der frühzeitigen Einführung in der Datenbank und einer relativ einfachen Dateistruktur wurden DBF-Dateien allgemein als Standardspeicherformat für strukturierte Daten in kommerziellen Anwendungen akzeptiert.

https://datei.wiki/extension/dbf

Wie kann ich eine DBF Datei öffnen?

DBeaver

Da es sich um ein Datenbankformat handelt und ich grade das Tool DBeaver in meinen Arbeitsalltag eingeführt habe, lag es für mich nahe, die Datei mit DBeaver zu öffnen.

Dazu musste ich einen Treiber zu DBeaver hinzufügen um anschließend die Datei öffnen zu können. Ich konnte dann die Tabellenstruktur sehen, aber nicht auf die Tabelle zugreifen. Es gab eine Fehlermeldung, dass eine weitere Datei fehlen würde.

java.sql.SQLException: nl.knaw.dans.common.dbflib.CorruptedTableException: Could not find file 'C:\dev\tmp\adress.dbt' (or multiple matches for the file)

DBeaver Stack-Trace

Diese andere Datei gibt es nicht und sie ist auch nicht für den Zugriff erforderlich, wie der erfolgreiche Zugriff über die anderen Wege beweist.

Etwas ausführlicher hatte ich es im Artikel zu DBeaver geschrieben.

Excel

Excel öffnen, DBF Datei reinziehen, Daten ansehen. Fertig, so einfach kann es gehen.

Ich hatte mich allerdings durch die Bezeichnung Standarddatenbankdatei ablenken lassen, so dass ich zuerst die Wege über DBeaver und Java versucht hatte.

Java

Für den Zugriff mit Java habe ich die Bibliothek JavaDBF verwendet.

Die beiden Testklassen JavaDBFReaderTest und JavaDBFReaderWithFieldNamesTest waren schnell angepasst und eine weiter Klasse zum Auslesen aller Daten ReadItAll war dann auch problemlos möglich. Dabei ignoriere ich die Datentypen und lese einfach alles als Strings ein. Für den PoC reicht das.

DBF in PostgresDB speichern

Als Beispiel, wie ich mit den Daten weiterarbeiten kann, importiere ich sie in eine Postgres Datenbank.

Dazu lese ich zuerst die sample.dbf ein und erzeuge dann eine Tabelle sample mit allen Columns, die in sample.dbf vorhanden sind. Anschließend wird die Tabelle zeilenweise gefüllt.

Das meiste ist hardcodiert und die Spalten sind alles Text-Spalten, da ich die Datentypen aus der DBF Datei nicht auslese, aber für den PoC reicht das.

Categories
Development Java

JEXIEPORTER

JEXIEPORTER = JSON Excel XML Importer Exporter

Die Sourcen des JEXIEPORTER finden sich wie immer im GitHub.

Der JEXIEPORTER ist ein PoC um Java Objekt in XML oder JSON Strings zu transformieren und aus den XML/JSON-Strings wieder Java Objekte zu transformieren.
Außerdem um CSV-Dateien sowie Excel- und ODF/ODS-Dateien zu lesen und zu schreiben.

Der JEXIEPORTER führt folgende Schritte durch:

  • import sampledata.csv
  • convert data to xml
  • convert data to json
  • convert xml to objects
  • convert json to objects
  • export data as CSV to a temporary file and opens system csv-viewer
  • export data as xls to a temporary file and opens system xls-viewer
  • import temporary xlx file

Beispieldaten

Die Beispieldaten sampledata.csv habe ich auf GenerateData generieren lassen und habe dann die trennenden Komma durch Semikolon ersetzt.

Informationsquellen

CSV-Import

CSV-Export

Excel / Apache POI

Categories
Development Java

iText

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:

test

Categories
Database Development Java

Redis

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

Redis

Typische Datenstrukturen von Redis
• Strings
• Lists
• Sets
• Hashes

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

Redis Clients

Es gibt eine große Anzahl von Clients für Redis:

Redis

Redis Homepage: https://redis.io/

Online Spielwiese: https://try.redis.io/

Alle verfügbaren Kommandos: https://redis.io/commands

Ein Redis Client für Java ist Redisson. Code Example: https://redisson.org/redis-java-client-with-code-example.html

Redis - Online Demo

Categories
Development Java

Quarkus

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

Zeige „RESTeasy Reactive“-> @Path /hello
http://localhost:8080/hello

Hot Deployment

curl http://localhost:8080/hello

Zeige Klasse GreetingResource

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):

./mvnw quarkus:add-extension -Dextensions='quarkus-smallrye-metrics'

Testen:

http http://localhost:8080/q/metrics

ggf. ein paar Mal wiederholen dann gehts; wieder ohne Neustart

OpenAPI

Testen:

http http://localhost:8080/q/openapi

Fehler: Not Found

Mit Maven nachinstallieren (alternativ: Gradle oder Quarkus CLI):

./mvnw quarkus:add-extension -Dextensions='quarkus-smallrye-openapi'

Testen:

http http://localhost:8080/q/openapi

ggf. ein paar Mal wiederholen dann gehts; wieder ohne Neustart

In "richtigem" Browser zeigen das Swagger direkt mit enthalten ist:

Swagger

Abschluss

Fragen

Wer hat schon Erfahrungen mit Quarkus sammeln können?
Wie war der persönliche Eindruck?

Wurde ggf. schon mal in einem Projekt gegen Quarkus entschieden?
Warum?

Diskussion

Quarkus vs. Spring Boot?

Categories
Database Development Java

Oracle Database

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:

docker run --name ingosOracleDB \
-p 1521:1521 \
-e ORACLE_PWD=ingo5Password \
-e ORACLE_CHARACTERSET=AL32UTF8 \
oracle/database:23.2.0-free

Connection Test

TOAD

Mit nachfolgenden Einstellungen konnte ich jeweils eine Verbindung aufbauen:

Java

Auf der Seite für JDBC Download von Oracle können wir sehen, das der OJDBC11-Treiber für JDK17 zertifiziert ist:

Anstelle des direkten Downloads kann man auch Maven verwenden, dort wird allerdings Kompatibilität nur bis JDK15 angegeben:

Ich vertraue da mehr der Oracle Seite und werde den Treiber verwenden und das Java Projekt mit JDK17 konfigurieren.

Testprojekt

Die pom.xml des Test Projektes:

<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>deringo</groupId>
  <artifactId>testproject</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>Test Project</name>
  <description>Projekt zum Testen von Sachen</description>

  <properties>
    <java.version>17</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>com.oracle.database.jdbc</groupId>
      <artifactId>ojdbc11</artifactId>
      <version>23.2.0.0</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.11.0</version>
        <configuration>
          <source>${java.version}</source>
          <target>${java.version}</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Die Test Klasse, basierend auf dem Code-Snippet von Oracle:

package deringo.testproject;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

import oracle.jdbc.datasource.impl.OracleDataSource;


public class TestMain {

    public static void main(String[] args) throws Exception {
        OracleDataSource ods = new OracleDataSource();
        ods.setURL("jdbc:oracle:thin:@localhost:1521/FREEPDB1"); // jdbc:oracle:thin@[hostname]:[port]/[DB service name]
        ods.setUser("PDBADMIN");
        ods.setPassword("ingo5Password");
        Connection conn = ods.getConnection();

        PreparedStatement stmt = conn.prepareStatement("SELECT 'Hello World!' FROM dual");
        ResultSet rslt = stmt.executeQuery();
        while (rslt.next()) {
            System.out.println(rslt.getString(1));
        }
    }

}

Nach dem Starten des Programmes lautet die Ausgabe auf der Console dann auch "Hello World!".

Categories
Development Java

QR Codes in Java

Für ein Projekt soll ich einen QR-Code generieren, der eine URL beinhaltet, die den Zugriff auf eine bestimmte Ressource ermöglicht.

In meinem letzten Post habe ich dokumentiert, wie die URL-Parameter verschlüsselt werden können.

Um mich mich dem Thema der QR Code Generierung zu öffnen habe ich insbesondere diese beiden Artikel verwendet: Generating Barcodes and QR Codes in Java auf Baeldung und Generating QR Code in Java auf Javapoint.

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.

ZXing

Here, we need to add two Maven dependencies: the core image library and the Java client:

<dependencies>
  <!-- ZXing for QR-Code Generation -->
  <!-- https://mvnrepository.com/artifact/com.google.zxing/core -->
  <dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>core</artifactId>
    <version>3.5.1</version>
  </dependency>
  <!-- https://mvnrepository.com/artifact/com.google.zxing/javase -->
  <dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>javase</artifactId>
    <version>3.5.1</version>
  </dependency>
</dependencies>

QR Code generieren:

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.

Den Einstieg in Docx4J habe ich bereits vollzogen. Besonders hilfreich ist Introduction To Docx4J auf Baeldung.

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;
    }
}

Categories
Development Java

Java Encryption and Decryption

Für ein Projekt soll ich einen QR-Code generieren, der eine URL beinhaltet, die den Zugriff auf eine bestimmte Ressource ermöglicht.

Das einfachste Beispiel wäre so etwas wie: https://Kaulbach.de/QR-Ressouce/id=1

Ich möchte allerdings nicht, dass jemand, der die URL kennt, alle Ressourcen ansehen kann, indem er einfach den id-Parameter hochzählt.

Daher soll der id-Parameter verschlüsselt werden.

Tutorial

Ich arbeite mich durch den Artikel Java AES Encryption and Decryption auf Baeldung.

Verwendet wird Advanced Encryption Standard (AES) in der Variante AES/CBC/PKCS5Padding "because it's widely used in many projects".

AES Parameters

In the AES algorithm, we need three parameters: input data, secret key, and IV

Im Code wird dann auch noch ein Salt verwendet, also sind es sogar vier Parameter.

Code

Der komplette Code des Tutorials befindet sich in GitHub.

Hier der Minimal-Code, der für mich relevant ist:

package deringo;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Base64;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

public class AESUtil {

    public static void main(String[] args) throws Exception {
        String input = "baeldung";
        String password = "myPassword";
        String salt = "mySalt";
        SecretKey key = getKeyFromPassword(password, salt); //generateKey(128);
        IvParameterSpec ivParameterSpec = generateIv();
        String algorithm = "AES/CBC/PKCS5Padding";
        String cipherText = encrypt(algorithm, input, key, ivParameterSpec);
        String plainText = decrypt(algorithm, cipherText, key, ivParameterSpec);
        System.out.println( input + " : " + plainText);
    }
    
    public static SecretKey getKeyFromPassword(String password, String salt)
        throws NoSuchAlgorithmException, InvalidKeySpecException {
        
        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(), 65536, 256);
        SecretKey secret = new SecretKeySpec(factory.generateSecret(spec)
            .getEncoded(), "AES");
        return secret;
    }
    
    public static IvParameterSpec generateIv() {
        byte[] iv = new byte[16];
        new SecureRandom().nextBytes(iv);
        return new IvParameterSpec(iv);
    }
    
    public static String encrypt(String algorithm, String input, SecretKey key,
        IvParameterSpec iv) throws NoSuchPaddingException, NoSuchAlgorithmException,
        InvalidAlgorithmParameterException, InvalidKeyException,
        BadPaddingException, IllegalBlockSizeException {
        
        Cipher cipher = Cipher.getInstance(algorithm);
        cipher.init(Cipher.ENCRYPT_MODE, key, iv);
        byte[] cipherText = cipher.doFinal(input.getBytes());
        return Base64.getEncoder()
            .encodeToString(cipherText);
    }
    
    public static String decrypt(String algorithm, String cipherText, SecretKey key,
        IvParameterSpec iv) throws NoSuchPaddingException, NoSuchAlgorithmException,
        InvalidAlgorithmParameterException, InvalidKeyException,
        BadPaddingException, IllegalBlockSizeException {
        
        Cipher cipher = Cipher.getInstance(algorithm);
        cipher.init(Cipher.DECRYPT_MODE, key, iv);
        byte[] plainText = cipher.doFinal(Base64.getDecoder()
            .decode(cipherText));
        return new String(plainText);
    }

}

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:

##################################### 
### SimpleCryptoUtil
SimpleCryptoUtil.password=myPassword
SimpleCryptoUtil.salt=mySalt
SimpleCryptoUtil.iv=2WbRy1wh3HsLe8Vir3TxkA==
#####################################

Damit kann ich die Methoden zum Erstellen von SecretKey und IvParameterSpec erstellen und darauf aufbauend die Methoden encrypt(String) und decrypt(String):

package deringo;

import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Base64;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.quinscape.melba.support.Config;

public class SimpleCryptoUtil {
    private static Logger logger = LoggerFactory.getLogger(SimpleCryptoUtil.class);
    
    public static String encrypt(String input) {
        try {
            String algorithm = "AES/CBC/PKCS5Padding";
            Cipher cipher = Cipher.getInstance(algorithm);
            cipher.init(Cipher.ENCRYPT_MODE, getKey(), getIv());
            byte[] cipherText = cipher.doFinal(input.getBytes());
            return Base64.getEncoder().encodeToString(cipherText);
        } catch (GeneralSecurityException e) {
            logger.error(e.getMessage());
            return null;
        }
    }
    
    public static String decrypt(String input) {
        try {
            String algorithm = "AES/CBC/PKCS5Padding";
            Cipher cipher = Cipher.getInstance(algorithm);
            cipher.init(Cipher.DECRYPT_MODE, getKey(), getIv());
            byte[] plainText = cipher.doFinal(Base64.getDecoder().decode(input));
            return new String(plainText);
        } catch (GeneralSecurityException e) {
            logger.error(e.getMessage());
            return null;
        }
    }
    
    private static SecretKey getKey() throws InvalidKeySpecException, NoSuchAlgorithmException {
        String password = Config.get("SimpleCryptoUtil.password");
        String salt     = Config.get("SimpleCryptoUtil.salt");
        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(), 65536, 256);
        SecretKey secret = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
        return secret;
    }
    
    private static IvParameterSpec getIv() {
        String iv = Config.get("SimpleCryptoUtil.iv");
        return new IvParameterSpec(Base64.getDecoder().decode(iv));
    }

    protected static String generateIV() {
        byte[] iv = new byte[16];
        new SecureRandom().nextBytes(iv);
        return Base64.getEncoder().encodeToString(iv);
    }
}

Die Verwendung ist dann so einfach wie gewünscht:

public static void main(String[] args) throws Exception {
  String secret = "geheim";
  String verschluesselt = SimpleCryptoUtil.encrypt(secret);
  System.out.println(secret + " : " + verschluesselt);
  String entschluesselt = SimpleCryptoUtil.decrypt(verschluesselt);
  System.out.println(verschluesselt + " : " + entschluesselt);
}

Categories
Development Java

Spring Boot OAuth2

Ich wollte mal Authentifizierung mit Spring Boot und GitHub-Login antesten. Als inspiration dient dieser Guide.

Die Sourcen lege ich in ein GitHub-Repository.

Creating a New Project

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:

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <title>Demo</title>
    <meta name="description" content=""/>
    <meta name="viewport" content="width=device-width"/>
    <base href="/"/>
    <link rel="stylesheet" type="text/css" href="/webjars/bootstrap/css/bootstrap.min.css"/>
    <script type="text/javascript" src="/webjars/jquery/jquery.min.js"></script>
    <script type="text/javascript" src="/webjars/bootstrap/js/bootstrap.min.js"></script>
</head>
<body>
	<h1>Demo</h1>
	<div class="container"></div>
</body>
</html>

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:

<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>jquery</artifactId>
	<version>3.4.1</version>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>bootstrap</artifactId>
	<version>4.3.1</version>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>webjars-locator-core</artifactId>
</dependency>

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 src/resources application.yml anlegen:

spring:
  security:
    oauth2:
      client:
        registration:
          github:
            clientId: github-client-id
            clientSecret: github-client-secret

Boot Up the Application

http://localhost:8080 aufrufen.

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.

Categories
Development Java

Docx4J

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.

public class Main {

    public static void main(String[] args) throws Exception {
        File template = getTemplateFile();
        File output   = getOutputFile();
        
        Files.copy(Paths.get(template.toURI()), Paths.get(output.toURI()), StandardCopyOption.REPLACE_EXISTING);

        Desktop.getDesktop().open(output);
    }
    
    private static File getTemplateFile() throws Exception {
        File template = new File( Main.class.getClassLoader().getResource("template.docx").toURI() );
        return template;
    }

    private static File getOutputFile() throws Exception {
        String tmpdir = System.getProperty("java.io.tmpdir");
        Path tmpfile = Files.createTempFile(Paths.get(tmpdir), "MyReport" + "-", ".docx");
        return tmpfile.toFile();
    }
}

Um das Template befüllen zu können legen wir einen Report mit Beispieldaten an:

public class Report {

    public String ueberschrift = "Sample Report";
    public String text = "Lorem Ipsum";
    public List<String[]> tabelle;
    
    public Report() {
        tabelle = new ArrayList<>();
        tabelle.add(new String[] {"Montag", "Regen", "4°C"});
        tabelle.add(new String[] {"Dienstag", "Wolkig", "6°C"});
        tabelle.add(new String[] {"Mittwoch", "Sonne", "10°C"});
    }
}

Docx4J

Projekt Homepage: Link

Projekt GitHub Repository: Link

Artikel zum Einstieg: Baeldung

Dem Projekt dank Maven einfach die notwendigen Abhängigkeiten in der pom.xml hinzufügen:

<dependencies>
  <!-- https://mvnrepository.com/artifact/org.docx4j/docx4j-JAXB-ReferenceImpl -->
  <dependency>
    <groupId>org.docx4j</groupId>
    <artifactId>docx4j-JAXB-ReferenceImpl</artifactId>
    <version>11.4.9</version>
  </dependency>
</dependencies>

Template befüllen

Template laden

WordprocessingMLPackage wordPackage = WordprocessingMLPackage.load(template);

Text ersetzen

Wir suchen alle Texte und wenn ein Platzhalter gefunden wird, wird der Text mit dem Wert aus dem Report gefüllt:

private static void textErsetzen(WordprocessingMLPackage wordPackage, Report report) throws Exception {
  List<Object> texts = wordPackage.getMainDocumentPart().getJAXBNodesViaXPath(XPATH_TO_SELECT_TEXT_NODES, true);
  for (Object obj : texts) {  
    Text text = (Text) ((JAXBElement<?>) obj).getValue();

    String textValue;
    if ("ÜBERSCHRIFT1".equals(text.getValue())) {
      textValue = report.ueberschrift;
    } else if ("TEXT1".equals(text.getValue())) {
      textValue = report.text;
    } else {
      textValue = text.getValue();
    }

    text.setValue(textValue);
  } 
}

Resultat:

Tabellenzeilen erzeugen

Wir laufen über alle Elemente und wenn eine Tabelle gefunden wird, wird deren Titel überprüft und dann die Tabelle befüllt.

private static void tabellenzeilenErzeugen(WordprocessingMLPackage wordPackage, Report report) {
  for (Object o : wordPackage.getMainDocumentPart().getContent()) {
    if (o instanceof JAXBElement) {
      @SuppressWarnings("unchecked")
      Tbl tbl = ((JAXBElement<Tbl>)o).getValue();
      String tblCaption = tbl.getTblPr().getTblCaption() != null ? tbl.getTblPr().getTblCaption().getVal() : null;
      if ("MyTabelle1".equals(tblCaption)) {
        fillTabelle1(tbl, report);
      } else {
        System.out.println("tblCaption: " + tblCaption);
      }
    }
  }
}

private static void fillTabelle1(Tbl tbl, Report report) {
  for (String[] zeile : report.tabelle) {
    addRow(tbl, zeile[0], zeile[1], zeile[2]);
  }
}

Bei den Einträgen in der Tabelle setzen wir noch den gewünschten Font.

private static void addRow(Tbl tbl, String... cells) {
  ObjectFactory factory = new ObjectFactory();

  // setup Font
  String fontName = "Arial";
  RFonts fonts = factory.createRFonts();
  fonts.setAscii(fontName);
  fonts.setCs(fontName);
  fonts.setHAnsi(fontName);
  RPr rpr = factory.createRPr();
  rpr.setRFonts(fonts);

  // new Row
  Tr tr = factory.createTr();
  for (String cell : cells) {
    Tc tc = factory.createTc();

    P p = factory.createP();
    R r = factory.createR();
    r.setRPr(rpr);

    Text text = factory.createText();

    text.setValue(cell);
    r.getContent().add(text);
    p.getContent().add(r);

    tc.getContent().add(p);

    tr.getContent().add(tc);
  }
  tbl.getContent().add(tr);
}

Resultat:

Report speichern

wordPackage.save(output);