Apache Camel: Web-Interface


Apache Camel ist nicht dazu gedacht als Webserver für eine Homepage zu agieren. Dafür gibt es eigene Applikationen. Es kann jedoch durchaus vorkommen, dass bei einer Integration ein Web-Interface zur Verfügung gestellt werden muss. Webbrowser gibt es auf fast jedem System. Damit liegt es nahe Konfiguration und Wartung auch über diesen erledigen zu können.

Natürlich könnte man für jede Resource eine eigene Route schreiben. Moderne Webseiten haben jedoch eine Vielzahl von Ressourcen wie CSS-, HTML- und JavaScript-Dateien. Unser Webserver soll diese dynamisch anhand eines Ordners ausliefern. Ebenso sollen Unterverzeichnisse angesprochen werden können.

Setup

Bis auf eine Webserver- und eine Json-Komponente wird keine weitere Abhängigkeit in unserem Projekt benötigt. Als Webserver benutzen wir Jetty. Der genaue Grund wird später noch diskutiert. Als Json-Komponente benutzen wir Gson. Beide sind als Dependency in das Projekt aufzunehmen. Für ein Maven-Projekt würden die Abhängigkeiten wie folgt eingebunden werden:

    <dependency>
      <groupId>org.apache.camel</groupId>
      <artifactId>camel-core</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.camel</groupId>
      <artifactId>camel-main</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.camel</groupId>
      <artifactId>camel-jetty</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.camel</groupId>
      <artifactId>camel-gson</artifactId>
    </dependency>

Um nicht alles von Scratch zu implementieren und dem Benutzer ein gut aussehendes Interface zu bieten benutzen wir das Bootstrap-Template. Dieses beinhaltet eine CSS- und eine JavaScript-Datei, welche jeweils in einem Unterordner abgespeichert werden. Zudem benötigen wir eine simple HTML-Datei, welche auf beide referenziert.

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Camel Interface</title>
    <link href="css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">

<script src="js/bootstrap.bundle.min.js"></script>
</body>
</html>

Für die Ordnungsstruktur habe ich im src-Ordner einen weiteren Ordner mit dem Namen data angelegt und alle Dateien hinein kopiert.

Der RouteBuilder

Bevor wir die Details besprechen möchte ich den RouteBuilder hier kurz vorstellen.

from("jetty:http://localhost:80/?matchOnUriPrefix=true")
                .routeId("resourceRoute")
                .onException(Exception.class)
                    .logStackTrace(true)
                    .setHeader(Exchange.HTTP_RESPONSE_CODE, constant(500))
                    .setBody(constant("Internal server error"))
                    .removeHeaders("*")
                    .handled(true)
                .end()
                .filter(header(Exchange.HTTP_PATH).isEqualTo(""))
                    .setHeader(Exchange.HTTP_PATH, constant("index.html"))
                .end()
                .process(exchange -> {
                    final Message message = exchange.getIn();
                    try {
                        final Path path = Paths.get(DATA_PATH +
                                message.getHeader(Exchange.HTTP_PATH, String.class).replace("..",""));
                        message.setBody(Files.readAllBytes(path));
                    } catch (InvalidPathException | IOException exception) {
                        log.error("Error while reading file", exception);
                        message.setHeader(Exchange.HTTP_RESPONSE_CODE, 404);
                    }
                })
                .removeHeaders("*", Exchange.HTTP_RESPONSE_CODE);

Das Exception-Handling in Zeile 3 sorgt dafür, dass die Exception gelogged wird und der Server mit einer simplen 500er Fehlernachricht antwortet.

In Zeile 10 wird jeder Request ohne einer Angabe der gewünschten Resource auf index.html gemapped. Dadurch stellen wir sicher, dass wir eine Antwort geben, auch wenn der Endbenutzer nicht den genauen Pfad zur Startseite kennt. Im Header CamelHttpPath welcher in Exchange.HTTP_PATH hinterlegt ist, wird beim aufrufen der Route automatisch die ausgewählte Resource eingetragen. Jetzt wird auch ersichtlich, warum wir die Jetty-Komponente verwenden. Mit der Option matchOnUriPrefix=true werden alle Requests entgegen genommen, selbst die aus Unterordnern. Wir müssen damit nicht für jeden Ordner einen neuen Consumer definieren.

Der Processor macht nichts weiter als die geforderte Datei im Dateisystem zu suchen. Wird diese nicht gefunden so wird eine 404 Nachricht generiert.

Für statische Dateien sollte ihr nun einen funktionierenden Webserver haben.

Servicekommunikation

Als nächstes wollen wir über Json mit einer Camel-Route interagieren. Dazu erstellen wir eine Formular und senden den Inhalt dessen an einen eigenen Consumer. Auf unserer Html-Seite sieht das dann ungefähr so aus:

            <form class="needs-validation" novalidate>
                <div class="row">
                    <div class="col-md-6 mb-3">
                        <label for="firstName">First name</label>
                        <input type="text" class="form-control" id="firstName" placeholder="" value="" required>
                    </div>
                    <div class="col-md-6 mb-3">
                        <label for="lastName">Last name</label>
                        <input type="text" class="form-control" id="lastName" placeholder="" value="" required>
                    </div>
                </div>

                <hr class="mb-4">
                <button class="btn btn-primary btn-lg btn-block" id="submitButton" type="button">Continue to checkout</button>
            </form>

Mit dem passenden JavaScript um eine POST-Nachricht als Json an das Backend zu senden.

    $(document).ready(function () {
        $("#submitButton").on("click", function(){
            $.ajax({
                type: "POST",
                url: "interface",
                data: JSON.stringify({firstName: $("#firstName").val(), lastName: $("#lastName").val()}),
                dataType: "json",
                contentType: "application/json; charset=utf-8"
            })
            .done(function(result) {
                alert( result );
            })
            .fail(function(error) {
                alert( error );
            });
        });
    });

Unsere Camel-Route nimmt einen POST entgegen und wandelt das Json in eine Map um, mit derer wir eine Antwort erstellen und zurück an den Benutzer senden.

        from("jetty:http://localhost/interface?httpMethodRestrict=POST")
                .unmarshal().json(JsonLibrary.Gson, Map.class)
                .transform().simple("Hello ${body[firstName]} ${body[lastName]}")
                .marshal().json(JsonLibrary.Gson, Map.class);

Natürlich können wir mehr als nur Nachrichten austauschen. Es wäre durchaus denkbar mehrere Prozesse anzustoßen oder komplexe Berechnungen durchzuführen. Letztendlich haben wir durch das Webinterface jedoch eine komfortable Möglichkeit Benutzereingaben entgegen zu nehmen und mit Routen aus Apache Camel zu interagieren ohne ein zusätzliches Framework bereitstellen zu müssen.

Das gesamte Beispielprojekt ist auf meinem Github verfügbar.