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

Categories
Development Java

WISIA Extraktor

Projekt zum Extrahieren der Daten der WISIA Webseite.

Hintergrund

Ich benötige die Daten der Artenschutzdatenbank des Bundesamt für Naturschutz in Bonn.
Deren Wissenschaftliches Informationssystem zum Internationalen Artenschutz liefert diese Informationen über eine Webseite. Weder API noch Datenbankkopie sind verfügbar.
Daher musste ich die Seite jeder einzelnen Art aufrufen, die Informationen extrahieren und verarbeiten.

Das Projekt

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]

TODO: funktionierendes Mermaid-Plugin installieren.
Workaround: Screenshot

Screenshot Mermaid Diagramm


Seite herunterladen

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.

<!-- https://mvnrepository.com/artifact/net.sourceforge.htmlunit/htmlunit -->
<dependency>
  <groupId>net.sourceforge.htmlunit</groupId>
  <artifactId>htmlunit</artifactId>
  <version>2.70.0</version>
</dependency>

Daten validieren

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

https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?mode=Info&id=107809

Hier die uneindeutigen Taxonomien:

  • Caryophyllidae : [Dicotyledonae, Scleractinia]
  • Arenaria : [Scolopacidae, Caryophyllaceae]
  • Cantharellus : [Fungiidae, Hygrophoraceae]
  • Dracaena : [Teiidae, Agavaceae]
  • Pholidota : [Mammalia, Orchidaceae]
  • Gomphus : [Gomphidae, Gomphaceae]
  • Mytilidae : [Mytiloida, Anisomyaria]
  • Oenanthe : [Umbelliferae, Muscicapidae]
  • Stelis : [Orchidaceae, Megachilidae]

Daten verarbeiten

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.

Categories
Development Java

Default Charset UTF-8

New Java 18 Feature: Default Charset UTF-8

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.

Mit Java 18 wurde endlich JEP 400: UTF-8 by Default umgesetzt.

Das Problem ���

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

Die kann man sich ganz einfach anzeigen lassen:

        String encoding = System.getProperty("native.encoding");
        System.out.println(encoding);

Sowohl unter Java 17 wie unter Java 19 wird bei mir angezeigt:

Was nicht funktioniert hat

Die eclipse.ini erweitern:

-Dfile.encoding=UTF-8
-Dsun.stdout.encoding=UTF-8
-Dsun.stderr.encoding=UTF-8

Die Lösung

In der Run Configuration für das Programm unter Common -> Encoding auf Use system encoding umstellen.

Oder erstmal weiterhin Java 17 verwenden. 🤷‍♂️

Categories
Development Java

KeyStore

Im letzten Post musste ich CVS in Eclipse nachinstallieren.

Der Download über die Update Seite hatte aber nicht auf Anhieb geklappt, denn Eclipse konnte sich nicht mit dem Update Server verbinden. Es gab ein Problem mit SSL: "PKIX path building failed"

Hintergrund ist, dass die Verbindung aus dem Firmen Intranet hinaus in das Internet über einen Proxy von ZScaler läuft, der eigene Zertifikate ausstellt und diese sind nicht im Java Truststore.

Ich hatte schon mal in einem anderen Setup das gleiche Problem: Man In The Middle (ZScaler) aber mit anderen Anforderungen und anderer Lösung.

Für mein Eclipse Problem muss ich zuerst die Zertifikate finden, dann den Eclipse Truststore und dann diesem die Zertifikate hinzufügen.

Zertifikate

Die Zertifikate habe ich ganz einfach aus dem Firefox gezogen: Über Settings -> Privacy & Security -> Certificates exportieren:

Truststore

Der Pfad zum Truststore des von Eclipse verwendeten Javas findet sich in der eclipse.ini Datei:

Allerdings nicht im /bin Folder, sondern in der Datei /lib/security/cacerts

Das Standart Passwort lautet: changeit

KeyStore Explorer

Man könnte die Zertifikate händisch per Command Line hinzufügen.

Oder man macht es sich leicht und verwendet ein Tool mit GUI. Da bietet sich der KeyStore Explorer an.

Einfach den KeyStore Explorer öffnen, cacerts-Datei hinein ziehen, Passwort (changeit) eingeben und anschließend die aus dem Firefox heruntergeladenen Zertifikate hineinziehen und importieren. Anschließend speichern und ggf. Eclipse neu starten.

Categories
Development Java

Eclipse removed CVS

Aus der IDE Eclipse wurde der CVS Support entfernt.

Eine lapidare Mitteilung verkündet das Ende einer Ära: link

Ist halt nur blöd, wenn man ein Update fährt und anschließend ist der CVS Support weg, den man aber essentiell für die Arbeit braucht, da die Projekte nun mal in einem CVS liegen. Warum sollte man auch das gute alte CVS verlassen, ist es doch immerhin so perfekt, dass schon seit Jahren keine Updates dafür entwickelt werden mussten. SVN hat unser CVS schon überlebt und ob Git sich durchsetzen wird, muss sich erst noch zeigen.

Das Gute ist: Man kann aus einer älteren Eclipse Version die CVS Funktionalität nachinstallieren. Die Anleitung dazu habe ich hier gefunden:

On Eclipse, click on Help -> Install New Software...

On the Available Software dialog, click "Add". On the "Add Repository" box, enter Name: CVS Support; Location: https://download.eclipse.org/eclipse/updates/4.22

Click "Eclipse CVS Client" and install the CVS Client components.

Categories
Development Java Linux

SAPJCO 2

Problem

We have a very, very old application that needs to be migrated into AWS. So we copied all files into AWS EC2 instance and tried to start the application. After fixing a lot of minor problems we faced a tough challenge with a SAPJCO RFC Call.

The Exception message was something like this:

Exception in thread "main" java.lang.ExceptionInInitializerError: JCO.classInitialize(): Could not load middleware layer 'com.sap.mw.jco.rfc.MiddlewareRFC'
JCO.nativeInit(): Could not initialize dynamic link library sapjcorfc [sapjcorfc (Not found in java.library.path)]. java.library.path [/usr/lib/jvm/java-1.6.0-ibm.x86_64/jre/lib/amd64/default:/usr/lib/jvm/java-1.6.0-ibm.x86_64/jre/lib/amd64:/usr/lib]
        at com.sap.mw.jco.JCO.<clinit>(JCO.java:871)
        at java.lang.J9VMInternals.initializeImpl(Native Method)
        at java.lang.J9VMInternals.initialize(J9VMInternals.java:199)

I guess, with a JCO Version 3 we would not have much trouble, but in this ancient application JCO Version 2 is used and we cannot update to Version 3 without a huge efford. In other projects I had the luck that I could migrate to Version.

The application is running on a Linux system. But belive me: it would have been much harder on a Windows machine.

Analysis

To find the cause of the problem I wrote the simpliest JCO Test Programm I can image:

import com.sap.mw.jco.JCO;

public class TestMain {

    public static void main(String[] args) {
        System.out.println(JCO.getVersion());
    }

}

My analysis programm structure:

app
├─ JCo/
│  ├─ librfccm.so
│  ├─ libsapjcorfc.so
│  ├─ sapjco.jar
├─ TestMain.java

Compile from command line:

javac -cp ".:/app/JCo/sapjco.jar" TestMain.java

Run from command line:

java -cp ".:/app/JCo/sapjco.jar" TestMain

That gave me another error:

Exception in thread "main" java.lang.ExceptionInInitializerError: JCO.classInitialize(): Could not load middleware layer 'com.sap.mw.jco.rfc.MiddlewareRFC'
JCO.nativeInit(): Could not initialize dynamic link library sapjcorfc [/app/JCo/libsapjcorfc.so: librfccm.so: cannot open shared object file: No such file or directory]. java.library.path [/app/JCo]
        at com.sap.mw.jco.JCO.<clinit>(JCO.java:871)
        at TestMain.main(TestMain.java:11)

Need to set an environment property first:

export LD_LIBRARY_PATH=/app/JCo

Run command line to start programm again and got another error:

Exception in thread "main" java.lang.ExceptionInInitializerError: JCO.classInitialize(): Could not load middleware layer 'com.sap.mw.jco.rfc.MiddlewareRFC'
JCO.nativeInit(): Could not initialize dynamic link library sapjcorfc [/app/JCo/libsapjcorfc.so: libstdc++.so.5: cannot open shared object file: No such file or directory]. java.library.path [/app/JCo]
        at com.sap.mw.jco.JCO.<clinit>(JCO.java:871)
        at TestMain.main(TestMain.java:11)

The interesting part of the error message:

Could not initialize dynamic link library sapjcorfc [/app/JCo/libsapjcorfc.so: libstdc++.so.5

Solution

We need the libstdc++.so.5 library, but installed is libstdc++.so.6

To get libstdc++.so.5 we installed package compat-libstdc++-33-3.2.3-66.x86_64:

yum install compat-libstdc++-33-3.2.3-66.x86_64

## to be honest, I am not exactly 100% sure, what I did in my investigations, so the command may be a little differend, ex:
# yum install compat-libstdc++-33-3
# yum install compat-libstdc++-33
# yum install compat-libstdc++-33 libstdc++.so.5

Test

Run from command line:

java -cp ".:/app/JCo/sapjco.jar" TestMain

That gave me no error, but SAPJCo Version number.:

2.1.10 (2011-05-10)

Finally it worked 😎

Anekdotum

Quote from an StackOverflow post from 2010:

libstdc++.so.5 is a very old version of the standard c++ library.

Some Analysis Details

Writing this article is giving me the feeling, that this was all super easy. But in reality it was a real pain in the allerwertesten.

To isolate the source of the problem, I did not only write the small Java (JCO.getVersion) application, I also set up a Docker environment.

One challenge was to find a useful Docker image to start from. I started with an OpenJDK Image that was already deprecated. Deprecated was not the problem, but I could not install libstdc++.so.5.

Next I tried to use the newer, undeprecated Eclipse-Temurin Image. But still could not install libstdc++.so.5

So I finally ended in a Debian Image and self installed Java where I was able to install libstdc++5.

FROM debian:bookworm-slim
 RUN apt-get update && apt-get install -y locales openjdk-11-jdk libstdc++5 \
     && rm -rf /var/lib/apt/lists/* \
     && localedef -i de_DE -c -f UTF-8 -A /usr/share/locale/locale.alias de_DE.UTF-8
 ENV LANG de_DE.utf8

COPY ./JCo/* /app/JCo/
COPY TestMain.java /app
WORKDIR /app
 ENV LD_LIBRARY_PATH=/app/JCo
 
 RUN javac -cp ".:/app/JCo/sapjco.jar" TestMain.java 

 CMD ["java", "-cp", ".:/app/JCo/sapjco.jar", "-Djava.library.path=/app/JCo", "TestMain"]

Note:

The locale might not be neccessary.
The "-Djava.library.path=/app/JCo" Parameter is not neccessary. But I leave it as an example, how one can use it.

Build and start the app:

docker build -t my-java-app .
docker run -it --rm --name my-running-app my-java-app
2.1.10 (2011-05-10)

Work in container

To work in the running container:

docker exec -it my-running-app /bin/sh

But there is one problem: You can only interact with a running container. But the TestMain-Programm is executed and immediately closed.

So I wrote another Test Programm, that keeps running, so I can enter the running container and test stuff (install packages, compile Java programm, etc.):

import java.io.BufferedReader;
import java.io.InputStreamReader;

import com.sap.mw.jco.JCO;

public class TestMain {

    public static void main(String[] args) {
        System.out.println("Hello World");
        //System.out.println("JCO Version: " + JCO.getVersion());

        while (true) {
            BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
            System.out.println("Enter Input : ");
            try {
                String s = br.readLine();
                System.out.println(s);
            }catch(Exception e) {
                System.out.println(e);
            }
        }
        
    }

}
Categories
Development Java

OneLogin AccessFilter

Wir wollen unsere Anwendung durch einen Filter schützen, der nur Anfragen von eingeloggten Benutzern mit der richtigen Rolle hindurch lässt.

Für diesen PoC bauen wir einen Reverse Proxy für den Login und einen Anwendungsserver für den Filter. Der Anwendungsserver bekommt einen Zugang über den RP und einen Zugang ohne RP, um so zu zeigen, dass nur über den RP auf die Anwendung zugegriffen werden kann.

In produktiven Umgebungen darf es den Zugang ohne RP so ungeschützt nicht geben, da es sehr einfach ist, zB über ein Browser-Plugin, den RP mittels selbstgesetzter Header zu faken.

Reverse Proxy

Der Reverse Proxy bekommt grundsätzlich die gleiche Konfiguration wie in den Beispielen zuvor.

Hier müssen wir aber noch den Scope params hinzufügen, um so die OneLogin-Rollen des Benutzers zu übertragen:

## OIDCScope params
## to put params including roles into header
OIDCScope "openid email profile groups params"

Im Header sieht das dann beispielsweise so aus:

oidc_claim_params
	{"post_logout_redirect_uri": "http://localhost", "roles": "user;admin"}

ACHTUNG

Das die Rollen als Parameter übertragen werden, funktioniert nur, wenn man explizit angibt, dass diese als Parameter übergeben werden sollen!

Dazu muss die Application in OneLogin entsprechend konfiguriert werden:

Applications -> Applications -> select Application -> Parameters -> "+"-Button:

Im nächsten Fenster des Dialoges die Value ("User Roles") setzen:

Access Filter

Der Access Filter prüft, ob eine UserID vorhanden ist. Das ist in unserem Beispiel die Email, es könnte aber auch der Preferred Username genommen werden.

Des weiteren prüft der Access Filter, ob der Benutzer die erforderliche Rolle user hat.

Sind beide Bedingungen erfüllt, kann auf die Anwendung zugegriffen werden, ansonsten wird lediglich "Access denied" angezeigt.

package deringo.oneloginjavaappsample;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang3.StringUtils;
import org.json.JSONObject;


@WebFilter("/*")
public class AccessFilter implements Filter {

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		System.out.println("AccessFilter init");
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		System.out.println("AccessFilter doFilter");
		
		HttpServletRequest httpRequest = (HttpServletRequest) request;
		
		String oidc_claim_email = httpRequest.getHeader("oidc_claim_email");
		String oidc_claim_preferred_username = httpRequest.getHeader("oidc_claim_preferred_username");
		String userid = oidc_claim_email;
		
		String oidc_claim_params = httpRequest.getHeader("oidc_claim_params");
		List<String> roles = getRoles(oidc_claim_params);
		boolean hasRequiredRole = roles.contains("user");
		
		if (!StringUtils.isBlank(userid) && hasRequiredRole) {
			// Grant access
			chain.doFilter(request, response);
		} else {
			response.getWriter().println("Access denied");
			response.getWriter().close();
		}
	}

	@Override
	public void destroy() {
		System.out.println("AccessFilter destroy");
	}

	private static List<String> getRoles(String oidc_claim_params) {
		List<String> rolesList = new ArrayList<>();
		try {
			JSONObject o = new JSONObject(oidc_claim_params);
			String rolesS = o.get("roles").toString();
			String[] roles = StringUtils.split(rolesS, ";");
			rolesList = Arrays.asList(roles);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return rolesList;
	}
}

Docker

Über Docker Compose werden beide Server gestartet.

Über Port 80 ist der Reverse Proxy erreichbar, über Port 8080 direkt der Anwendungsserver.

version: '3.8'
services:

   reverseproxy:
      build: ./reverseproxy
      hostname: reverseproxy
      volumes:
        - ./reverseproxy/public_html:/usr/local/apache2/htdocs
        - ${PWD}/reverseproxy/conf/reverseproxy_httpd.conf:/usr/local/apache2/conf/httpd.conf
        - ${PWD}/reverseproxy/conf/reverseproxy.conf:/usr/local/apache2/conf/reverseproxy.conf
      ports:
        - 80:80
      
   sampleapp:
      build: ./oneloginjavaappsample
      hostname: sampleapp
      ports:
        - 8080:8080

Im Docker File des Anwendungsservers kopieren wir erst nur das Maven POM, lassen dann Maven bauen, um so die Abhängigkeiten herunterzuladen. Erst danach kopieren wir das gesamte Projekt in den Container und lassen die Anwendung bauen. So wird nach Code Änderungen, ohne Anpassungen in Maven, der Bau des Images beschleunigt, da nicht jedes Mal die Bibliotheken heruntergeladen werden müssen.

   FROM tomcat:8.5-jdk8-openjdk-slim
    RUN apt update && apt install -y \
        maven
   COPY pom.xml /app/pom.xml
WORKDIR /app
    RUN mvn package
   COPY . /app
    RUN mvn package
WORKDIR $CATALINA_HOME
    RUN mv /app/target/ROOT.war webapps
 EXPOSE 8080
    CMD ["catalina.sh", "run"]

Test

Zugriff auf den Anwendungsserver über den Reverse Proxy nach Anmeldung:

http://localhost/private/test.html

Direkter Zugriff auf den Anwendungsserver ohne Anmeldung:

http://localhost:8080/index.html

GitHub

Die Dateien zu diesem Post sind im OneLogin-GitHub-Projekt unter version6 zu finden.