Donnerstag, Mai 30, 2019

Groovy Maven POM Checker

Für das Releasen eines Maven Projekts gibt es das Maven Release-Plugin. Also kein Grund das Rad neu zu erfinden?

Richtig. Naja. Also für eine kleine Übung mit Groovy ist das Thema perfekt. Technisch interessieren mich hier die folgenden Dinge:

  • Wie bekomme ich farbige Ausgaben auf die Konsole?
  • Wie leicht kann ich XML Dokumente mit Groovy einlesen?
  • Wie greife ich auf andere Command-Line Werkzeuge zu?

Das Ergebnis findet sich in meinem Repository auf GitHub: validate-pom.groovy.

Gleich zu Beginn definiere ich mir ein paar Helfer-Methoden, die die Konsolenausgaben bunt anpinseln. Die Farbgebung funktioniert übrigens nur in einer Cygwin Konsole. Unter Linux habe ich das bisher nicht ausprobiert. Mit einer Powershell funktioniert das jedenfalls nicht. Z.B.:

def red() {
    print "\033[31;1m"
}

In Zeile 38 öffne ich die pom.xml und suche nach allen SNAPSHOT Abhängigkeiten. Falls eine SNAPSHOT Abhängigkeit gefunden wird, wird diese mit gelben Textzeichen ausgegeben.

pom.dependencies.dependency.each { dependency ->
    if (dependency.version.toString().contains('SNAPSHOT')) {
        yellow()
        enableWarning = true
    }
    println "    ${dependency.groupId}:${dependency.artifactId}:${dependency.version}"
    reset()
}

Als nächstes wird die changes.xml untersucht. Ich untersuche das letzte release Element, ob dort das aktuelle Datum drin steht.

def changesXmlAsFile = new File("./src/changes/changes.xml")
def changesXml = new XmlSlurper().parse(changesXmlAsFile)
def lastReleaseElement = changesXml.body.release[0]
def today = new Date().format('yyyy-MM-dd')
if (lastReleaseElement.@date.toString().contains('??-??')) {
    enableWarning = true
    yellow()
    println "    Release date is undefined: ${lastReleaseElement.@date.toString()}"
    reset()
} else if (!lastReleaseElement.@date.toString().contains(today)) {
    enableWarning = true
    yellow();
    println "    Release date is not today: ${lastReleaseElement.@date.toString()}"
    reset()
} else {
    ok()
}

Im letzten Schritt wird git aufgerufen und geprüft, ob un-commited changes vorliegen.

def sout = new StringBuilder()
def serr = new StringBuilder()
def proc = 'git status'.execute()
proc.consumeProcessOutput(sout, serr)
proc.waitForOrKill(1000)
println ""
println "Git repository:"
if (sout.toString().contains('git add')) {
    enableWarning = true
    yellow()
    println "    Uncommited changes! Check with git status."
} else {
    ok()
}

Ist das nicht der Fall, dann ist alles in Ordnung. Das Skript lässt sich natürlich wunderbar mit weiteren Funktionen erweitern. Wie oben bereits im Link angegeben, liegt das Skript in einem GIT Repository auf GitHub. In meiner Arbeitsumgebung habe ich das Projekt reingeclont. Im Anschluss passe ich die .bashrc an und lege einen alias auf das Skript. Z.B.:

alias pomcheck='groovy path.to.script/validate-pom.groovy

Fertig.

Freitag, Mai 03, 2019

Spring RestController und Exception Handling

Wer das erste mal einen klassischen Rest-Service mit Spring schreibt, also einen @RestController implementiert, der JSON (application/json; charset=utf-8) als Reponse ausliefert, kommt irgendwann auf die Idee, ebenfalls Fehlerzustände als JSON Response mit einem passenden HttpStatus Code auszuliefern.

Per Default erhält der Client den HTTP-Statuscode 500, wenn innerhalb des @RestController eine Exception geworfen wird. Das Ganze eingebettet in der Standardfehlerseite des Webservers. Als Antwort auf einen Restservice denkbar ungeeignet.

Eventuell möchte man einen Request auf die fachliche Korrektheit seiner Parameter überprüfen. Falls die Prüfung negativ ausfällt, sollen spezielle Http Status Codes (siehe Wikipedia HTTP StatusCodes) ausgeliefert werden. Z.B. könnte sich der Statuscode 400 BAD REQUEST oder 422 UNPROCESSABLE ENTITY für das Beispiel anbieten.

Wie sieht eine Implementierung dazu aus? Auf GitHub habe ich ein Beispielprojekt angelegt. Der Webservice nimmt Benutzer Registrierungen entgegen und vermerkt, ob der Benutzer dem Speichern von Cookies zugestimmt hat. Die Klasse de.awtools.registration.CookieController ist z.B. ein einfacher @RestController. Mit Spring hat man drei Möglichkeiten das beschriebene Problem zu lösen. Ich habe mich hier für die Lösung mit einem @ControllerAdvice entschieden. Das hat den Vorteil, dass dieser Advice für alle meine @RestController gilt. Der ControllerAdvice sieht folgendermaßen aus:

package de.awtools.registration.config;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import de.awtools.registration.RequestValidationException;

@ControllerAdvice
public class GlobalExceptionAdvice extends ResponseEntityExceptionHandler {

    @ExceptionHandler(value = { RequestValidationException.class })
    protected ResponseEntity<Object> handleConflict(Exception ex,
            WebRequest request) {

        String bodyOfResponse = "Unknown";
        if (ex instanceof RequestValidationException) {
            RequestValidationException rvex = (RequestValidationException) ex;
            bodyOfResponse = new StringBuilder().append('{')
                    .append("\"message\": \"")
                    .append(rvex.getValidation().getValidationCode().toString())
                    .append("\"").append('}').toString();
        }

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON_UTF8);

        return handleExceptionInternal(ex, bodyOfResponse,
                headers, HttpStatus.BAD_REQUEST, request);
    }

}

Hier werden alle Exceptions vom Typ RequestValidationException gefangen und ausgewertet. D.h. der Http-Statuscode 400 wird ausgeliefert und die Exception, bzw. die Nachricht aus der Exception, wird in ein einfaches JSON Objekt verpackt.

Weitere Beispiele und Erklärungen:

AssertJ und java.util.List

AssertJ hat eine praktische Möglichkeit, Listen in JUnit Tests abzuprüfen. Insbesondere, wenn in der Liste komplexe Objekte abgelegt sind, s...