Client-Vertrag aus UI-Perspektive
Die Benutzeroberfläche bildet in diesem Projekt nicht nur eine grafische Schicht über dem Backend, sondern ist auch Teil des Gesamtvertrags zwischen Benutzer, Client und Server. In diesem Teil steht der Datenfluss aus Sicht der UI im Mittelpunkt: wie Eingaben in strukturierte Anfragen übersetzt werden, wie der Client diese weiterleitet und welche Rückmeldungen die Benutzeroberfläche anschließend verarbeitet.
Der Quelltext für diese Version befindet sich auf GitHub unter https://github.com/svenruppert/url-shortener/tree/feature/advent-2025-day-03
Hier ist der Screenshot der Version, die wir nun implementieren.



Die Grundlage des Vertrags ist die Klasse ShortenRequest, die in diesem Entwicklungsschritt um das neue Feld expiresAt erweitert wurde. Dieses Feld dient als zentraler Informationsträger für Ablaufdaten und ist vollständig optional – das bedeutet, dass bestehende Clients auch ohne dieses Attribut weiterhin funktionsfähig bleiben. Der UI-Client ist somit abwärtskompatibel und zugleich zukunftssicher.
public class ShortenRequest {
private String url;
private String shortURL;
private Instant expiresAt;
public ShortenRequest(String url, String shortURL, Instant expiresAt) {
this.url = url;
this.shortURL = shortURL;
this.expiresAt = expiresAt;
}
public Instant getExpiresAt() { return expiresAt; }
public void setExpiresAt(Instant expiresAt) { this.expiresAt = expiresAt; }
}
Die CreateView übergibt dieses Objekt an den URLShortenerClient, der die Kommunikation mit dem Server übernimmt. Entscheidend ist hier, dass die Benutzeroberfläche nicht direkt HTTP-Verbindungen aufbaut, sondern diese Aufgabe an eine dedizierte Client-Komponente delegiert. Dadurch bleibt das UI schlank und testbar, während der Client zentral für Logging, Fehlerbehandlung und Request-Erzeugung zuständig ist.
Die zentrale Schnittstelle ist dabei die Methode createCustomMapping, die den erweiterten Vertrag abbildet:
public ShortUrlMapping createCustomMapping(String alias, String url, Instant expiredAt) throws IOException {
logger().info("Create custom mapping alias='{}' url='{}' expiredAt='{}'", alias, url, expiredAt);
URL shortenUrl = serverBaseAdmin.resolve(PATH_ADMIN_SHORTEN).toURL();
HttpURLConnection connection = (HttpURLConnection) shortenUrl.openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setRequestProperty(CONTENT_TYPE, JSON_CONTENT_TYPE);
var shortenRequest = new ShortenRequest(url, alias, expiredAt);
String body = shortenRequest.toJson();
try (OutputStream os = connection.getOutputStream()) {
os.write(body.getBytes(UTF_8));
}
int status = connection.getResponseCode();
if (status == 200 || status == 201) {
try (InputStream is = connection.getInputStream()) {
String jsonResponse = new String(is.readAllBytes(), UTF_8);
ShortUrlMapping shortUrlMapping = fromJson(jsonResponse, ShortUrlMapping.class);
return shortUrlMapping;
}
}
if (status == 409) {
throw new IllegalArgumentException("Alias already in use");
}
throw new IOException("Unexpected status: " + status);
}
Dieses Beispiel verdeutlicht, wie präzise die UI und der Client aufeinander abgestimmt sind. Die UI übergibt eine vollständig ausgefüllte Domäneninstanz (ShortenRequest), die alle erforderlichen Felder enthält. Der Client übernimmt die Serialisierung, führt die Kommunikation durch und liefert ein ShortUrlMapping als Antwort zurück. Das UI zeigt die relevanten Daten anschließend unmittelbar an.
Ein zentrales Gestaltungsprinzip in diesem Zusammenspiel lautet „Daten statt Kommandos“. Die UI sendet keine spezifischen Steuerbefehle, sondern immer vollständige Datenobjekte. Das Backend entscheidet anhand dieser Objekte, wie die Operation durchzuführen ist. Diese Entkopplung hat mehrere Vorteile:
- Erweiterbarkeit: Neue Felder (z. B. expiresAt) können hinzugefügt werden, ohne bestehende APIs zu brechen.
- Nachvollziehbarkeit: Jede Operation ist über das Request-Objekt vollständig nachvollziehbar.
- Sicherheit: Der Client kann Eingaben validieren, bevor sie in HTTP-Anfragen überführt werden.
In der UI werden die Antworten des Clients genutzt, um Feedback zu geben und die aktuelle Ansicht zu aktualisieren. Dabei wird nicht nur der Shortcode angezeigt, sondern auch das gesamte Mapping-Objekt, das bereits alle serverseitig berechneten Werte enthält. Die Benutzeroberfläche bleibt somit stets im Einklang mit dem tatsächlichen Systemzustand.
Dieses Muster – eine klare Vertragsstruktur zwischen UI, Client und Server – schafft langfristig Stabilität und Wartbarkeit. Änderungen an der Backend-API erfordern keine Anpassungen an der UI-Logik, solange der zugrunde liegende Datenvertrag unverändert bleibt. Damit etabliert sich ein verbindlicher Kommunikationspfad, der die technische Evolution ermöglicht, ohne die Benutzerinteraktion zu beeinträchtigen.
Client-Schicht (URLShortenerClient): Erweiterungen
Die Client-Schicht bildet die technische Brücke zwischen der Benutzeroberfläche und dem Server. Sie übernimmt die Aufgabe, Datenobjekte in HTTP-Anfragen zu übersetzen, die Kommunikation zu überwachen und die Ergebnisse wieder in Domänenobjekte zu überführen. In diesem Kapitel wird gezeigt, wie die bestehende Klasse URLShortenerClient erweitert wurde, um das neue Ablaufkonzept (expiresAt) zu unterstützen und zugleich eine saubere, valide Kommunikation mit dem Server sicherzustellen.
Der Ausgangspunkt war die bereits vorhandene Funktion createCustomMapping(String alias, String url), die nun durch eine überladene Variante ergänzt wurde. Diese akzeptiert ein zusätzliches Ablaufdatum (Instant expiredAt) und führt alle notwendigen Schritte durch, um die Daten vollständig und konform an den Server zu übertragen.
public ShortUrlMapping createCustomMapping(String alias, String url, Instant expiredAt)
throws IOException {
logger().info("Create custom mapping alias='{}' url='{}' expiredAt='{}'", alias, url, expiredAt);
if (alias != null && !alias.isBlank()) {
var validate = AliasPolicy.validate(alias);
if (!validate.valid()) {
var reason = validate.reason();
throw new IllegalArgumentException(reason.defaultMessage);
}
}
URL shortenUrl = serverBaseAdmin.resolve(PATH_ADMIN_SHORTEN).toURL();
HttpURLConnection connection = (HttpURLConnection) shortenUrl.openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setRequestProperty(CONTENT_TYPE, JSON_CONTENT_TYPE);
var shortenRequest = new ShortenRequest(url, alias, expiredAt);
String body = shortenRequest.toJson();
logger().info("createCustomMapping - body - '{}'", body);
try (OutputStream os = connection.getOutputStream()) {
os.write(body.getBytes(UTF_8));
}
int status = connection.getResponseCode();
if (status == 200 || status == 201) {
try (InputStream is = connection.getInputStream()) {
String jsonResponse = new String(is.readAllBytes(), UTF_8);
logger().info("createCustomMapping - jsonResponse - {}", jsonResponse);
ShortUrlMapping shortUrlMapping = fromJson(jsonResponse, ShortUrlMapping.class);
logger().info("shortUrlMapping .. {}", shortUrlMapping);
return shortUrlMapping;
}
}
if (status == 409) {
throw new IllegalArgumentException("Alias already in use");
}
throw new IOException("Unexpected status: " + status);
}
Mit dieser Methode wird das erweiterte Ablaufdatum nahtlos in den bestehenden Kommunikationsfluss integriert. Die UI ruft diese Methode über die CreateView auf, wodurch die Erweiterung für den Benutzer transparent bleibt – die neue Funktionalität ist sofort verfügbar, ohne dass sich die Benutzerführung verändert.
Neben der Erweiterung der Methodensignatur wurde auch die Konsistenz der HTTP-Kommunikation verbessert. Anstatt manuell gesetzter Header wird nun die Konstante JSON_CONTENT_TYPE konsequent verwendet, um Formatfehler zu vermeiden und eine eindeutige Typisierung sicherzustellen. Diese Vereinheitlichung reduziert das Risiko inkonsistenter Requests und erleichtert spätere Protokollerweiterungen (z. B. zur Authentifizierung oder Signierung von Requests).
Ein weiterer wichtiger Punkt ist das Logging. Der URLShortenerClient protokolliert alle relevanten Schritte – von der Request-Erstellung bis zur Antwortverarbeitung. Diese Transparenz ist entscheidend, um im Fehlerfall den genauen Ablauf nachvollziehen zu können. Besonders während der Entwicklungsphase und bei der Integration neuer Features wie expiresAt liefert das Logging wertvolle Einblicke in Timing, Format und Status der Datenübertragung.
Ein typischer Log-Ausschnitt könnte wie folgt aussehen:
INFO Create custom mapping alias='test123' url='https://example.com' expiredAt='2025-12-31T23:59:00Z'
INFO createCustomMapping - body - '{"url": "https://example.com", "alias": "test123", "expiresAt": "2025-12-31T23:59:00Z"}'
INFO createCustomMapping - jsonResponse - '{"shortCode": "ex-9A7", "originalUrl": "https://example.com", "expiresAt": "2025-12-31T23:59:00Z"}'
INFO shortUrlMapping .. ShortUrlMapping[shortCode=ex-9A7, url=https://example.com, expiresAt=2025-12-31T23:59:00Z]
Dieses strukturierte Logging folgt einem klaren Muster, das alle beteiligten Schritte nachvollziehbar macht. Es ist nicht nur für Debuggingzwecke gedacht, sondern kann langfristig auch in eine Audit- oder Monitoringlösung integriert werden.
Abschließend ist festzuhalten, dass die Client-Schicht nun vollständig kontextsensitiv arbeitet. Sie erkennt optional übergebene Ablaufdaten, validiert Eingaben, kommuniziert klar strukturierte JSON-Payloads und interpretiert serverseitige Antworten korrekt. Dadurch entsteht eine robuste, klar definierte Schnittstelle zwischen Benutzeroberfläche und Backend, die zukünftigen Erweiterungen – etwa benutzerdefinierte Policies oder Metadaten – problemlos aufnehmen kann.
Server-API und Handler
Die Server-Schicht bildet das Rückgrat der gesamten Architektur. Sie nimmt Anfragen entgegen, interpretiert die Datenstrukturen und koordiniert die Erstellung, Speicherung oder Löschung der Kurzlinks. Mit der Einführung des Ablaufdatums (expiresAt) wurde die API um eine wesentliche semantische Dimension erweitert. Das Ziel bestand darin, diese neue Information in den bestehenden Request-Response-Fluss zu integrieren, ohne Kompatibilitätsprobleme mit älteren Clients zu verursachen.
Die zentrale Anlaufstelle für eingehende POST-Anfragen zum Erstellen eines Kurzlinks ist der ShortenHandler. Dieser verarbeitet die JSON-Nutzlast, führt Validierungen durch und interagiert mit dem UrlMappingStore. Dabei wurde die Verarbeitung so erweitert, dass das Ablaufdatum korrekt aus dem JSON-Objekt extrahiert und an die Persistenz weitergereicht wird.
final String body = readBody(ex.getRequestBody());
ShortenRequest req = fromJson(body, ShortenRequest.class);
if (isNullOrBlank(req.getUrl())) {
writeJson(ex, BAD_REQUEST, "Missing 'url'");
return;
}
final Result<ShortUrlMapping> urlMappingResult =
store.createMapping(req.getShortURL(), req.getUrl(), req.getExpiresAt());
urlMappingResult
.ifPresentOrElse(success -> logger().info("mapping created success {}", success.toString()),
failed -> logger().info("mapping created failed - {}", failed));
urlMappingResult
.ifSuccess(mapping -> {
final Headers h = ex.getResponseHeaders();
h.add("Location", "/r/" + mapping.shortCode());
writeJson(ex, fromCode(201), toJson(mapping));
})
.ifFailure(errorJson -> {
try {
var parsed = JsonUtils.parseJson(errorJson);
var errorCode = Integer.parseInt(parsed.get("code"));
var message = parsed.get("message");
writeJson(ex, fromCode(errorCode), message);
} catch (Exception e) {
writeJson(ex, CONFLICT, errorJson);
}
});
Der ShortenHandler nutzt bewusst kein Framework, sondern arbeitet mit nativen Java-APIs (HttpExchange, HttpURLConnection), um den Kontrollfluss vollständig transparent zu halten. Diese Entscheidung dient nicht nur der Nachvollziehbarkeit, sondern ermöglicht auch ein präzises Verständnis dafür, wie HTTP auf niedrigster Ebene funktioniert. Der Code zeigt klar die Schritte des Serverzyklus: Body lesen, deserialisieren, validieren, Domain-Logik aufrufen und Antwort schreiben.
Ein weiteres Beispiel ist der ListHandler, der geringfügig angepasst wurde, um die moderne Sequenced-API (List.getFirst()) zu nutzen und damit die Lesbarkeit zu erhöhen:
private static String first(Map<String, List<String>> q, String key) {
var v = q.get(key);
return (v == null || v.isEmpty()) ? null : v.getFirst();
}
Ein besonderes Augenmerk gilt der robusten Verarbeitung von JSON-Daten. Hier wurde sichergestellt, dass Zeilenumbrüche und Escape-Sequenzen keine Parsing-Probleme verursachen. Im Modul JsonUtils wurde dazu vor dem Parsen ein Bereinigungsschritt eingeführt:
s = s.replaceAll(“\\n”, “”);
Dies verhindert, dass mehrzeilige JSON-Daten zu Fehlern führen – ein typischer Stolperstein bei APIs, die manuell erzeugte oder geloggte Nutzlasten verarbeiten.
In der Gesamtsicht bleibt die Server-API bewusst flach und deklarativ. Jede Operation steht für eine klar umrissene Domänenaktion, es gibt keine übermäßige Verzweigung oder versteckte Zustandsänderung. Durch die Erweiterung um expiresAt bleibt dieser Stil erhalten, und das System reagiert weiterhin deterministisch: Eine Anfrage erzeugt ein Mapping, optional mit Ablaufdatum, und gibt den vollständigen Datensatz als JSON zurück.
Diese Einfachheit ist kein Zufall, sondern Ausdruck eines Designprinzips, das sich durch alle Ebenen der Anwendung zieht: Explizite Datenflüsse statt impliziter Magie. Das Ergebnis ist ein System, das sowohl für Benutzer als auch für Entwickler verständlich bleibt und sich zuverlässig erweitern lässt.
Persistenz und Store-Implementierungen
Die Persistenzschicht ist das Fundament, auf dem die Zuverlässigkeit des gesamten Systems ruht. Mit der Einführung des Ablaufdatums (expiresAt) musste sie entsprechend erweitert werden, damit diese neue Information zuverlässig gespeichert, abgefragt und ausgewertet werden kann – unabhängig davon, ob die Daten im Arbeitsspeicher oder in einer dauerhaften Datenbank wie EclipseStore gespeichert werden.
Im Zentrum dieser Schicht steht das Interface UrlMappingUpdater, das um eine neue Methode erweitert wurde. Diese Methode ergänzt die bisherigen Signaturen um den zusätzlichen Parameter Instant expiredAt, sodass die Persistenzschicht nun Ablaufzeiten explizit übernehmen kann.
public interface UrlMappingUpdater {
Result<ShortUrlMapping> createMapping(String originalUrl);
Result<ShortUrlMapping> createMapping(String alias, String url);
Result<ShortUrlMapping> createMapping(String alias, String url, Instant expiredAt);
boolean delete(String shortCode);
}
Damit wird klar festgelegt, dass jede Implementierung Ablaufinformationen verarbeiten muss. Diese Anpassung folgt dem Prinzip des kontraktbasierten Designs – das Interface definiert, welche Fähigkeiten die konkrete Implementierung besitzen muss, ohne deren technische Details vorzugeben.
InMemory-Implementierung
Die erste Anpassung erfolgte im InMemoryUrlMappingStore. Diese Klasse wird primär für Tests und volatile Laufzeitumgebungen verwendet und speichert alle Mappings in einer ConcurrentHashMap. Durch die Erweiterung der createMapping-Methoden werden nun auch Ablaufdaten korrekt übernommen und an den MappingCreator weitergegeben.
@Override
public Result<ShortUrlMapping> createMapping(String alias, String originalUrl, Instant expiredAt) {
logger().info("alias: {} - originalUrl: {} - expiredAt: {} ", alias, originalUrl, expiredAt);
return creator.create(alias, originalUrl, expiredAt);
}
Im MappingCreator selbst wird der Ablaufzeitpunkt in das ShortUrlMapping integriert und direkt beim Erstellen gespeichert:
public Result<ShortUrlMapping> create(String alias, String url, Instant expiredAt) {
logger().info("createMapping - alias='{}' / url='{}' / expiredAt='{}'", alias, url, expiredAt);
final String shortCode;
if (!isNullOrBlank(alias)) {
if (repository.containsKey(alias)) {
return Result.failure("Alias already exists");
}
shortCode = alias;
} else {
shortCode = generator.generate();
}
var mapping = new ShortUrlMapping(shortCode, url, Instant.now(clock), Optional.ofNullable(expiredAt));
store.accept(mapping);
return Result.success(mapping);
}
Dieses Muster zeigt, dass Ablaufinformationen – sofern vorhanden – direkt Teil des Domänenobjekts ShortUrlMapping sind. Der Code bleibt dabei bewusst einfach: kein zusätzlicher Zustand, keine Sonderbehandlung, sondern lediglich ein optionaler Wert.
EclipseStore-Implementierung
Für die dauerhafte Speicherung wurde auch der EclipseStoreUrlMappingStore angepasst. Hier gilt dasselbe Prinzip, jedoch mit Fokus auf die Langzeitpersistenz.
@Override
public Result<ShortUrlMapping> createMapping(String alias, String originalUrl, Instant expiredAt) {
logger().info("alias: {} - originalUrl: {} - expiredAt: {}", alias, originalUrl, expiredAt);
return creator.create(alias, originalUrl, expiredAt);
}
Durch die enge Kopplung mit demselben MappingCreator bleibt das Verhalten zwischen den InMemory- und EclipseStore-Varianten vollständig konsistent. Der einzige Unterschied besteht in der Speicherdauer: Während die Daten im InMemory-Store beim Neustart verloren gehen, bleiben sie in EclipseStore persistent.
Einheitlichkeit und Kompatibilität
Ein zentrales Ziel dieser Anpassungen war die vollständige Gleichbehandlung aller Persistenzarten. Ob Tests, Entwicklungsmodus oder Produktivbetrieb – alle Pfade verwenden dieselben Objekte und Methoden. Damit entfällt die Gefahr divergierender Logik zwischen den verschiedenen Speicherarten. Änderungen an der Domäne, wie etwa die Einführung von expiresAt, müssen daher nur an einer Stelle umgesetzt werden.
Vorteile des Ansatzes
Diese konsequente Vereinheitlichung bringt mehrere Vorteile:
- Transparenz: Jede Mapping-Operation ist nachvollziehbar und im Log dokumentiert.
- Konsistenz: InMemory- und EclipseStore-Stores verhalten sich identisch.
- Erweiterbarkeit: Neue Speichermechanismen (z. B. SQL, Key-Value-Store, Cloud) können leicht ergänzt werden, solange sie den Interface-Vertrag erfüllen.
Mit dieser Erweiterung wird die Persistenzschicht zum verlässlichen Rückgrat des Systems. Das Ablaufdatum ist nun ein vollwertiger Bestandteil des Datenmodells – präzise erfasst, sicher gespeichert und jederzeit abrufbar. Dadurch können zukünftige Funktionen wie automatische Bereinigung abgelaufener Einträge oder zeitbasierte Statistiken direkt auf dieser Basis umgesetzt werden.
Domänenmodell und Defaultwerte
Die Klasse ShortenRequest wurde um das Feld expiresAt ergänzt. Dieses Feld ermöglicht die Übergabe eines optionalen Ablaufzeitpunkts, der vom Benutzer in der UI festgelegt wird und als Instant im JSON-Format an den Server übermittelt wird. Diese Information wird damit ein vollwertiger Bestandteil des Datenmodells und lässt sich sowohl in der Transportschicht als auch in der Persistenz weiterverarbeiten.
public class ShortenRequest {
private String url;
private String shortURL;
private Instant expiresAt;
public ShortenRequest(String url, String shortURL, Instant expiresAt) {
this.url = url;
this.shortURL = shortURL;
this.expiresAt = expiresAt;
}
public Instant getExpiresAt() { return expiresAt; }
public void setExpiresAt(Instant expiresAt) { this.expiresAt = expiresAt; }
public String toJson() {
var a = shortURL == null ? "\"null\"" : "\"" + JsonUtils.escape(shortURL) + "\"";
var b = expiresAt == null ? "\"null\"" : "\"" + JsonUtils.escape(expiresAt.toString()) + "\"";
return """
{
\"url\": \"%s\",
\"alias\": %s,
\"expiresAt\": %s
}
""".formatted(JsonUtils.escape(url), a, b);
}
}
Wichtig ist, dass der expiresAt-Wert nicht verpflichtend ist. Dadurch bleiben bestehende Clients und Serveraufrufe kompatibel, auch wenn sie das Feld nicht setzen. Das Domänenmodell wurde bewusst so gestaltet, dass es abwärtskompatibel und erweiterbar bleibt – ein wesentliches Prinzip bei der Einführung neuer Features.
ShortUrlMapping als zentrales Bindeglied
Die Klasse ShortUrlMapping repräsentiert das zentrale Datenelement zwischen Client und Server. Sie enthält alle relevanten Informationen eines Kurzlinks: den generierten Shortcode, die Ziel-URL, das Erstellungsdatum und optional das Ablaufdatum. Durch die Verwendung von Optional<Instant> wird das mögliche Fehlen eines Ablaufzeitpunkts explizit modelliert.
public record ShortUrlMapping(String shortCode, String originalUrl, Instant createdAt, Optional<Instant> expiresAt) { }
Diese Entscheidung unterstreicht die funktionalen Eigenschaften des Modells: ein unveränderliches Datenelement, das nur bei seiner Erstellung vollständig definiert wird. Änderungen an einem Mapping erfolgen immer durch Neuanlage oder explizite Updates – nie durch stille Mutationen.
DefaultValues – zentrale Systemkonstanten
Neben den Modellklassen wurde auch die Klasse DefaultValues erweitert. Sie enthält Konstanten, die in der gesamten Anwendung verwendet werden, insbesondere die Basis-URL für generierte Kurzlinks.
public final class DefaultValues {
// TODO - must be editable by user
public static final String SHORTCODE_BASE_URL = "https://3g3.eu/";
public static final int ADMIN_SERVER_PORT = 9090;
public static final String ADMIN_SERVER_HOST = "localhost";
public static final String ADMIN_SERVER_PROTOCOL = "http";
// weitere Pfaddefinitionen ...
}
Die Konstante SHORTCODE_BASE_URL dient als Basiskomponente für die Generierung und Anzeige von Kurzlinks in der Benutzeroberfläche. Obwohl sie aktuell statisch definiert ist, wurde bereits vermerkt, dass sie in einer zukünftigen Iteration dynamisch konfigurierbar sein soll. Damit wird die Grundlage für flexible Deployment-Szenarien gelegt, in denen unterschiedliche Umgebungen (z. B. Entwicklung, Test, Produktion) eigene Basis-URLs verwenden können.
AliasPolicy mit Logging
Ein weiterer Bestandteil des Domänenmodells ist die AliasPolicy, die Regeln für gültige Aliasnamen definiert. Sie wurde im Rahmen der Erweiterungen um Logging ergänzt, um die Validierungsprozesse besser nachvollziehbar zu machen:
public final class AliasPolicy implements HasLogger {
public static Validation validate(String alias) {
HasLogger.staticLogger().info("validate - {}", alias);
if (alias == null || alias.isBlank()) return Validation.fail(Reason.NULL_OR_BLANK);
if (alias.length() < MIN) return Validation.fail(Reason.TOO_SHORT);
if (alias.length() > MAX) return Validation.fail(Reason.TOO_LONG);
if (!ALLOWED_PATTERN.matcher(alias).matches()) return Validation.fail(Reason.INVALID_CHARS);
return Validation.success();
}
}
Durch diese Protokollierung werden fehlerhafte Aliase sofort sichtbar, was die Fehlersuche im Zusammenspiel von UI und Backend erheblich erleichtert.
Mit diesen Anpassungen wird das Domänenmodell zu einem robusten, klar strukturierten Kern der Anwendung. Die zentrale Entität ShortUrlMapping spiegelt den realen Zustand eines Kurzlinks vollständig wider, während ShortenRequest die Erstellung neuer Einträge steuert und DefaultValues systemweite Konstanten bereitstellt. Alle Erweiterungen bleiben mit dem ursprünglichen Designprinzip vereinbar: einfache, funktionale Strukturen, die präzise definieren, was ein Benutzer erzeugen, verändern oder abrufen kann.
JSON-Serialisierung und Deserialisierung
Die Zuverlässigkeit der Kommunikation zwischen den Komponenten einer Anwendung hängt entscheidend von der Qualität der Serialisierungsschicht ab. In diesem Kapitel wird beschrieben, wie die JSON-Verarbeitung erweitert und stabilisiert wurde, um das neue Feld expiresAt sicher zu transportieren und zugleich die Lesbarkeit sowie die Fehlertoleranz des Codes zu verbessern.
Erweiterung der Serialisierung in JsonUtils
Die Klasse JsonUtils bildet das Rückgrat der JSON-Verarbeitung im Projekt. Sie stellt sowohl generische Hilfsmethoden als auch spezifische Serialisierungsroutinen für die wichtigsten Domänenobjekte bereit. Mit der Einführung des Ablaufdatums musste sichergestellt werden, dass dieses Feld korrekt in JSON-Dokumente integriert wird, ohne ältere Datenformate zu beeinträchtigen.
In der Methode zur Serialisierung von ShortenRequest wurde daher das Feld expiresAt ergänzt:
if (dto instanceof ShortenRequest req) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("url", req.getUrl());
m.put("alias", req.getShortURL());
m.put("expiresAt", req.getExpiresAt());
return toJson(m);
}
Durch diese Änderung wird das Ablaufdatum automatisch mit ausgegeben, sofern es vorhanden ist. Wird es nicht gesetzt, erscheint das Feld im JSON als null und bleibt damit syntaktisch gültig. Diese explizite Darstellung von null-Werten verbessert die Lesbarkeit und erlaubt es dem Server, klar zwischen „nicht gesetzt“ und „bewusst leer“ zu unterscheiden.
Erweiterung der Deserialisierung
Analog zur Serialisierung wurde auch die Deserialisierung erweitert, sodass expiresAt aus JSON-Daten korrekt abgelesen wird. In der Methode fromJson erfolgt die Anpassung:
if (type == ShortenRequest.class) {
String url = m.get("url");
String alias = m.get("alias");
Instant expiresAt = parseInstantSafe(m.get("expiresAt"));
return (T) new ShortenRequest(url, alias, expiresAt);
}
Die Funktion parseInstantSafe konvertiert den ISO-8601-String in ein Instant-Objekt und behandelt ungültige oder leere Werte tolerant. Diese Fehlerresistenz ist insbesondere für Clients wichtig, die eventuell abweichende oder ältere JSON-Strukturen senden.
Bereinigung eingehender JSON-Daten
Ein häufiges Problem bei APIs ist das Einlesen von JSON-Daten, die unbeabsichtigte Zeilenumbrüche oder zusätzliche Escape-Zeichen enthalten. Um Parsing-Fehler zu verhindern, wurde in JsonUtils.parseJson eine einfache Vorverarbeitung eingeführt:
s = s.replaceAll(“\\n”, “”);
Dieser Schritt entfernt alle Zeilenumbrüche, bevor der Parser ausgeführt wird. Damit werden sowohl manuell formatierte JSON-Dateien als auch geloggte Nachrichten zuverlässig erkannt und korrekt interpretiert. Diese Anpassung macht das System robuster gegen uneinheitliche Formatierungen, die in realen Umgebungen häufig auftreten. (Mir ist an dieser Stelle aber bewusst, dass es noch lange nicht ausreichend ist…)
Optimierung der JSON-Ausgabe
Im Zuge dieser Änderungen wurde auch die Methode toJsonListing überarbeitet. Statt eines komplizierten StringBuilder-Aufbaus wird nun eine einfache, lesbare Stringverkettung verwendet:
return "{" +
"\"mode\":\"" + escape(mode) + "\"," +
"\"count\":" + count + "," +
"\"items\":" + toJsonArrayOfObjects(items) +
"}";
Diese Vereinfachung reduziert die Fehleranfälligkeit und erleichtert das Debugging. Gerade in Systemen, die ohne Frameworks auskommen, ist die Lesbarkeit des Codes ein entscheidender Faktor für Wartbarkeit und Fehlerdiagnose.
Konsistenz und Interoperabilität
Ein wichtiger Aspekt bei der Überarbeitung der Serialisierung war die Wahrung der Interoperabilität zwischen unterschiedlichen Clients und API-Versionen. Da alle Felder weiterhin auf String-Basis serialisiert werden und die JSON-Struktur explizit ist, bleibt der Datenaustausch auch mit älteren Clients vollständig kompatibel. Das bedeutet: Ein Client, der expiresAt nicht sendet, wird vom Server akzeptiert, und ein Server, der das Feld nicht erwartet, ignoriert es einfach.
Diese lose Kopplung zwischen Sender und Empfänger ist ein zentrales Entwurfsziel des Projekts. Sie ermöglicht schrittweise Erweiterungen, ohne dass alle Komponenten gleichzeitig aktualisiert werden müssen.
Die Überarbeitung der JSON-Verarbeitung stärkt die Robustheit und die Zukunftssicherheit der Anwendung. Durch die gezielte Erweiterung von JsonUtils um das Feld expiresAt, die Bereinigung eingehender Daten und die Vereinfachung der Ausgabe ist die Serialisierung nun sowohl technisch stabil als auch semantisch präzise. Damit erfüllt sie die Anforderungen einer modernen, evolvierbaren Schnittstelle, die sowohl für automatisierte Prozesse als auch für menschliche Leser klar nachvollziehbar bleibt.
Sicherheit und Robustheit im UI-Fluss
Mit der wachsenden Komplexität der Benutzeroberfläche steigt auch die Verantwortung, sicherzustellen, dass alle Interaktionen vorhersehbar, valide und stabil bleiben. In diesem Kapitel wird erläutert, wie Sicherheitsaspekte und Robustheit direkt in den UI-Fluss integriert wurden – von der Validierung der Eingaben über defensive Navigationsentscheidungen bis hin zum Umgang mit externen Browser-APIs.
Eingabevalidierungen im Erzeugungsdialog
Der wichtigste Sicherheitsaspekt in der UI betrifft die Validierung der Benutzereingaben. Bereits beim Erstellen eines neuen Kurzlinks prüft die Anwendung, ob die eingegebene URL ein gültiges Schema enthält. Damit werden potenzielle Angriffe durch manipulierte oder nicht unterstützte Protokolle frühzeitig unterbunden.
binder.forField(urlField)
.asRequired("URL must not be empty")
.withValidator(url -> url.startsWith("http://") || url.startsWith("https://"),
"Only HTTP(S) URLs allowed")
.bind(ShortenRequest::getUrl, ShortenRequest::setUrl);
Durch diese einfache Validierung werden nur URLs akzeptiert, die über sichere oder klar definierte Transportprotokolle erreichbar sind. Das schützt sowohl den Benutzer als auch die Anwendung vor unerwünschten Interaktionen mit unsicheren Zielen. Allerdings ist es noch keine vollständige Input-Validierung.
Ein weiterer Teil der Validierung betrifft das Ablaufdatum. Hier wird sichergestellt, dass ein gewählter Zeitpunkt stets in der Zukunft liegt:
if (exp.isPresent() && exp.get().isBefore(Instant.now())) {
Notification.show("Expiry must be in the future");
return false;
}
Dieser Mechanismus schützt vor Fehleingaben und inkonsistenten Datenzuständen, insbesondere wenn Benutzer das Formular mehrfach bearbeiten oder bereits abgelaufene Zeiten auswählen.
Defensive Navigation im Detaildialog
Auch der Detaildialog (DetailsDialog) folgt dem Prinzip der sicheren Interaktion. Wird eine gespeicherte URL über den Button „Open“ geöffnet, erfolgt dies ausschließlich durch einen Aufruf der Methode UI.getCurrent().getPage().open(), jedoch nur dann, wenn das Ziel eindeutig als HTTP- oder HTTPS-Link erkannt wird. Dies verhindert, dass über die UI interne oder lokale Ressourcen versehentlich oder absichtlich aufgerufen werden.
openBtn.addClickListener(_ -> {
if (originalUrl.startsWith("http://") || originalUrl.startsWith("https://")) {
fireEvent(new OpenEvent(this, shortCode, originalUrl));
getUI().ifPresent(ui -> ui.getPage().open(originalUrl, "_blank"));
} else {
Notification.show("Invalid URL scheme");
}
});
Diese Entscheidung stärkt die Trennung zwischen internen und externen Ressourcen. Benutzeraktionen werden so gesteuert, dass sie keine unerwünschten Seiteneffekte außerhalb der Anwendung verursachen.
Umgang mit der Clipboard-API
Ein weiteres sicherheitsrelevantes Thema ist der Umgang mit der systemeigenen Clipboard-API. Diese steht aus Datenschutzgründen nur in sicheren Browserkontexten zur Verfügung, also über HTTPS oder localhost. Die Anwendung nutzt diese API, um Shortcodes und URLs bequem in die Zwischenablage zu kopieren. Sollte der Zugriff im aktuellen Kontext nicht erlaubt sein, führt der Aufruf zu keinem Fehler, sondern wird still verworfen – ein Beispiel für defensives Programmierverhalten im UI.
UI.getCurrent().getPage().executeJs("navigator.clipboard.writeText($0)", SHORTCODE_BASE_URL + m.shortCode());
Dieser nicht blockierende Aufruf vermeidet JavaScript-Fehler und hält die Benutzeroberfläche stabil, selbst wenn der Browser die Aktion ablehnt. Die Anwendung reagiert immer kontrolliert und bleibt in einem gültigen Zustand.
Konsistentes Feedback und Fehlertoleranz
Ein Kernelement der Robustheit ist das Feedback-System. Jeder Benutzerbefehl – ob erfolgreich oder fehlerhaft – löst eine visuelle Rückmeldung aus. Die Anwendung verwendet hierfür konsequent die Vaadin-Notification-Komponente. Sie informiert über Validierungsfehler, erfolgreiche Kopiervorgänge sowie Systemmeldungen, ohne den Arbeitsfluss zu unterbrechen. Dieses asynchrone Meldesystem unterstützt die Idee, dass ein Benutzer jederzeit fortfahren kann, selbst wenn ein einzelner Vorgang fehlschlägt.
Notification.show(“Alias already assigned or error saving”, 3000, Notification.Position.MIDDLE);
Diese Art der Fehlerkommunikation vermeidet Frustration und trägt zur wahrgenommenen Stabilität bei. Der Benutzer bleibt informiert, aber nie blockiert.
Die in diesem Kapitel beschriebenen Maßnahmen – Eingabevalidierungen, defensive Navigation und sichere Clipboard-Nutzung – verfolgen ein gemeinsames Ziel: Robustheit durch Vorsicht. Jede Aktion in der Benutzeroberfläche wird geprüft, jede externe Schnittstelle abgesichert und jede Benutzerinteraktion klar rückgemeldet. Damit erreicht das UI eine hohe Fehlertoleranz, ohne dabei an Bedienkomfort einzubüßen. Diese Balance aus Sicherheit und Nutzerfreundlichkeit bildet den Abschluss der technischen Umsetzung des Detaildialogs und legt die Grundlage für die kommenden Erweiterungen, bei denen Sicherheit und Benutzererlebnis weiterhin Hand in Hand gehen werden.
Fazit
Mit dem heutigen Tag des Adventskalenders erreicht die Anwendung einen entscheidenden Reifegrad. Während die ersten Teile vor allem die strukturelle Grundlage und den funktionalen Rahmen geschaffen haben, steht heute ganz im Zeichen der Detailtiefe und der Interaktionsqualität. Der Fokus lag darauf, den Übergang von einer technisch funktionsfähigen Oberfläche zu einer durchdachten, benutzerzentrierten Anwendung zu vollziehen.
Der neu eingeführte Detaildialog markiert dabei einen zentralen Wendepunkt: Er ermöglicht, einzelne Einträge im Kontext zu betrachten, ohne die Übersicht zu verlassen. Dieses Konzept verbindet technische Klarheit mit Benutzerfreundlichkeit und schafft eine modulare Struktur, die zukünftige Erweiterungen problemlos aufnehmen kann. Durch die Verwendung von Events zur Entkopplung der Komponenten bleibt die Architektur sauber, nachvollziehbar und testbar.
Auch die Integration des Ablaufdatums erweist sich als Meilenstein in der funktionalen Entwicklung des Systems. Von der Eingabe im UI über die Client-Schicht bis hin zur Persistenz und zur JSON-Verarbeitung wurde der neue Parameter konsequent in alle Schichten integriert. Dabei blieb der ursprüngliche Entwurf bewusst einfach – jede Schicht kennt nur ihre eigene Verantwortung, und keine enthält Logik, die einer anderen Schicht vorgreift. Das Ergebnis ist ein durchgängiger, klarer Datenfluss, der technische Präzision mit semantischer Transparenz verbindet.
Ein weiterer wichtiger Fortschritt besteht in der Verbesserung der Benutzererfahrung (UX). Durch konsistente Interaktionsmuster, präzises Feedback und sicherheitsbewusstes Verhalten wird die Anwendung zu einem Werkzeug, das sowohl intuitiv als auch vertrauenswürdig ist. Die Kombination aus unmittelbarer Rückmeldung, nicht blockierender Fehlerbehandlung und einem harmonischen visuell-funktionalen Design zeigt, wie technologische Strenge und Benutzerorientierung Hand in Hand gehen können.
Aus architektonischer Sicht verdeutlicht dieser Teil des Adventskalenders, dass selbst in kleinen Projekten Sauberkeit, Kohärenz und Erweiterbarkeit die maßgeblichen Erfolgsfaktoren sind. Die klare Trennung zwischen UI, Client, Server und Persistenz ermöglicht nicht nur eine effiziente Wartung, sondern eröffnet auch den Weg für zukünftige Module – etwa administrative Ansichten, Bulk-Operationen oder Sicherheitsrichtlinien für Benutzergruppen.
Cheers Sven