Die serverseitige Erweiterung für die dynamische Spaltensichtbarkeit folgt derselben gestalterischen Logik wie die UI: Einfachheit, klare Zuständigkeit und ein präziser Datenfluss. Während die OverviewView und der ColumnVisibilityDialog die Oberfläche für die Interaktion bilden, übernehmen mehrere spezialisierte REST-Handler die Verarbeitung und Persistenz der Benutzereinstellungen. Ihre Aufgabe besteht darin, eingehende JSON-Anfragen zu verarbeiten, zu validieren, in Domänenoperationen zu übersetzen und den aktuellen Zustand zurückzugeben oder zu speichern.
Der
Quelltext für diese Version befindet sich auf GitHub unter https://github.com/svenruppert/url-shortener/tree/feature/advent-2025-day-04
Hier ist der Screenshot der Version, die wir nun implementieren.


Das zentrale Bindeglied ist die Schnittstelle PreferencesStore, die sowohl Lese- als auch Schreiboperationen für Spaltenpräferenzen definiert. Sie wird in zwei funktionale Aspekte unterteilt – das Laden und das Aktualisieren:
public interface PreferencesStore extends PreferencesLookup, PreferencesUpdater, HasLogger {
}
public interface PreferencesLookup {
Map<String, Boolean> load(String userId, String viewId);
}
public interface PreferencesUpdater {
void saveColumnVisibilities(String userId, String viewId, Map<String, Boolean> visibility);
void delete(String userId, String viewId, String columnKey);
void delete(String userId, String viewId);
void delete(String userId);
}
Diese Struktur bildet das Fundament für die nachfolgenden Handlerklassen. Jeder REST-Endpunkt umfasst eine klar abgegrenzte Operation – das Laden, Bearbeiten oder Löschen von Spaltenpräferenzen. So entsteht eine granulare API, die einerseits idempotent ist und andererseits eine präzise Fehlerbehandlung ermöglicht.
Die erste Anlaufstelle ist der ColumnVisibilityHandler, der POST- und DELETE-Anfragen entgegennimmt. Er ist zuständig für das Laden aller Spalteneinstellungen eines Benutzers innerhalb einer View. Der Handler prüft zunächst die Vollständigkeit der Anfrage und gibt anschließend eine flache JSON-Map zurück, die Spaltennamen und Sichtbarkeitszustände abbildet:
@Override
public void handle(HttpExchange ex) throws IOException {
switch (ex.getRequestMethod()) {
case "POST" -> handleLoad(ex);
case "DELETE" -> handleDeleteAll(ex);
case "OPTIONS" -> allow(ex, "POST, DELETE, OPTIONS");
default -> methodNotAllowed(ex, "POST, DELETE, OPTIONS");
}
}
private void handleLoad(HttpExchange ex) throws IOException {
var req = fromJson(readBody(ex.getRequestBody()), ColumnInfoRequest.class);
var vis = store.load(req.userId(), req.viewId());
writeJson(ex, OK, toJson(vis == null ? Map.of() : vis));
}
Darüber hinaus existieren zwei spezialisierte Varianten, die die Editieroperationen voneinander trennen. Der ColumnVisibilitySingleHandler verarbeitet PUT- und DELETE-Anfragen für einzelne Spalten. Er nimmt ein JSON-Objekt mit Benutzer-ID, View-ID und Spaltennamen entgegen und aktualisiert die Persistenz entsprechend:
private void handleSingleEdit(HttpExchange ex) throws IOException {
var req = fromJson(readBody(ex.getRequestBody()), ColumnSingleEditRequest.class);
var visibility = Map.of(req.columnKey(), req.visible());
store.saveColumnVisibilities(req.userId(), req.viewId(), visibility);
writeJson(ex, OK, toJson(Map.of("status", "ok")));
}
Für Massenuploads dient der ColumnVisibilityBulkHandler. Diese Variante ermöglicht das gleichzeitige Aktualisieren mehrerer Spaltenzustände in einem einzigen Request. Das Konzept der Bulk-Verarbeitung reduziert die Latenz und minimiert serverseitige Schreiboperationen, wenn der Benutzer beispielsweise viele Checkboxen im Dialog verändert:
private void handleBulkEdit(HttpExchange ex) throws IOException {
var req = fromJson(readBody(ex.getRequestBody()), ColumnEditRequest.class);
store.saveColumnVisibilities(req.userId(), req.viewId(), req.changes());
writeJson(ex, OK, toJson(Map.of("status", "ok")));
}
Alle drei Handler folgen demselben Designmuster: Sie validieren den Request, rufen die passenden Methoden des PreferencesStores auf und senden den korrekten HTTP-Statuscode zurück. Bei Erfolg antwortet der Server mit 200 OK oder 204 No Content, bei fehlerhaften Eingaben mit 400 Bad Request. Diese Konvention macht die Schnittstelle robust und leicht zu testen.
Die neue API bildet damit das Rückgrat der Personalisierungslogik. Sie ist zugleich ein Beispiel für die lose Kopplung zwischen UI und Persistenz: Der Client weiß nichts über die Speicherstrategie, der Server nichts über die Darstellung. Beide kommunizieren über wohldefinierte JSON-Strukturen. Dieses Prinzip hält die Anwendung erweiterbar und unabhängig von konkreten Implementierungsdetails – sei es ein In-Memory-Store oder die langfristige Ablage in EclipseStore.
Der PreferencesClient: Roundtrip zwischen UI und Server
Die Verbindung zwischen der Benutzeroberfläche und der Persistenz wird durch den PreferencesClient hergestellt. Diese Komponente übernimmt die Aufgabe, die REST-Endpunkte der Serverseite über HTTP anzusprechen, Daten zu serialisieren und die Ergebnisse in Strukturen zu überführen, die innerhalb der Vaadin-UI genutzt werden können. Der Client fungiert damit als Vermittler zwischen der Interaktionslogik der Oberfläche und der Datenspeicherung des Servers – ein klassisches Bindeglied zwischen Darstellung und Zustand.
Der PreferencesClient folgt in seinem Aufbau dem etablierten Muster der anderen Service-Clients des Projekts. Er verwendet die Java-Standardbibliothek mit HttpClient, HttpRequest und HttpResponse und verzichtet vollständig auf externe Abhängigkeiten. Die Kommunikation erfolgt über klar definierte Endpunkte, die alle in der Klasse dokumentiert sind:
/**
* Client for server-side column visibility preferences.
*
* Endpoints:
* - POST /admin/preferences/columns -> load
* - DELETE /admin/preferences/columns -> delete all (for a view)
* - PUT /admin/preferences/columns/edit -> bulk edit
* - PUT /admin/preferences/columns/single -> single edit
*/
public final class ColumnVisibilityClient implements HasLogger {
Der Client stellt eine Reihe von Methoden bereit, die den REST-Endpunkten der Serverseite direkt entsprechen. Der typische Ablauf eines Roundtrips beginnt mit dem Laden der gespeicherten Sichtbarkeitsinformationen:
public Map<String, Boolean> load(String userId, String viewId)
throws IOException, InterruptedException {
var reqDto = new ColumnInfoRequest(userId, viewId);
var req = requestBuilder(PATH_ADMIN_PREFERENCES_COLUMNS)
.POST(jsonBody(reqDto))
.build();
var resp = http.send(req, HttpResponse.BodyHandlers.ofString(UTF_8));
if (resp.statusCode() == 200) {
var body = resp.body();
if (body == null || body.isBlank()) return Collections.emptyMap();
var parsed = parseJson(body);
return parsed.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey,
e -> Boolean.parseBoolean(e.getValue())));
}
if (resp.statusCode() == 204) return Collections.emptyMap();
throw new IOException("Unexpected HTTP " + resp.statusCode() + " while loading column visibilities: " + resp.body());
}
Die Methode erzeugt aus den Benutzer- und View-IDs ein einfaches JSON-Objekt und sendet es als POST-Anfrage an den Server. Die Antwort enthält eine Map von Spaltennamen zu Wahrheitswerten, die direkt in der UI verwendet werden kann. Fehlende Werte werden dabei als true interpretiert, was dem Prinzip der vollständigen Sichtbarkeit entspricht.
Für Änderungen bietet der Client zwei Varianten: die Bearbeitung einzelner Spalten und das Bulk-Update. Während die Methode editSingle gezielt einen Spaltenzustand anpasst, erlaubt editBulk die Übertragung mehrerer Änderungen in einem einzigen Request. Diese Trennung entspricht der semantischen Struktur der REST-Handler:
public void editSingle(String userId, String viewId, String columnKey, boolean visible)
throws IOException, InterruptedException {
var reqDto = new ColumnSingleEditRequest(userId, viewId, columnKey, visible);
var req = requestBuilder(PATH_ADMIN_PREFERENCES_COLUMNS_SINGLE)
.PUT(jsonBody(reqDto))
.build();
var resp = http.send(req, HttpResponse.BodyHandlers.ofString(UTF_8));
if (resp.statusCode() != 200) {
throw new IOException("Unexpected HTTP " + resp.statusCode() + " on single edit: " + resp.body());
}
}
Diese Methode veranschaulicht das Prinzip der Idempotenz: Mehrfache Aufrufe mit denselben Daten führen zu demselben Zustand, ohne Seiteneffekte zu erzeugen. Das ist besonders wichtig für reaktive Benutzeroberflächen, die Zustände asynchron aktualisieren.
Im Zusammenspiel mit dem ColumnVisibilityService, der als Wrapper fungiert, entsteht so ein klarer Kommunikationsfluss:
- Die UI interagiert über den ColumnVisibilityDialog mit dem Service.
- Der Service ruft über den PreferencesClient die passenden Endpunkte auf.
- Die Serverseite validiert, speichert und liefert aktuelle Zustände zurück.
- Der Service spiegelt die neuen Werte im Grid wider.
Das gesamte System ist dadurch deterministisch, nachvollziehbar und fehlertolerant. Sollte eine Übertragung fehlschlagen, bleibt der vorherige Zustand erhalten, und der Benutzer kann den Vorgang erneut anstoßen. Der PreferencesClient bildet somit die technische Grundlage für die stabile Persistenz visueller Präferenzen – eine einfache, aber äußerst robuste Brücke zwischen Interaktion und Speicherung.
Persistenz und EclipseStore-Integration
Die Speicherung der Benutzervorlieben für die Spaltensichtbarkeit erfolgt im selben Persistenzsystem, das bereits in den vorangegangenen Teilen des Projekts eingeführt wurde: EclipseStore. Diese Entscheidung folgt nicht nur der Konsistenz des Architekturdesigns, sondern unterstreicht auch das Ziel, alle Systemzustände – ob fachlich oder visuell – in einem kohärenten Datenmodell zusammenzuführen. Die Persistenz von Benutzerpräferenzen wird so zu einem gleichberechtigten Bestandteil der Anwendung.
Die zentrale Rolle spielt dabei die Implementierung der Schnittstelle PreferencesStore. In der Architektur gibt es mehrere konkrete Varianten: eine In-Memory-Version für Tests und volatile Szenarien sowie eine EclipseStore-gestützte Version für den produktiven Betrieb. Beide folgen dem gleichen Vertragsmodell, unterscheiden sich jedoch im zugrunde liegenden Speichermedium.
Der folgende Auszug zeigt die Schnittstellenstruktur, auf der die Implementierungen basieren:
// com.svenruppert.urlshortener.api.store.preferences.PreferencesStore
public interface PreferencesStore extends PreferencesLookup, PreferencesUpdater, HasLogger {
}
// com.svenruppert.urlshortener.api.store.preferences.PreferencesLookup
public interface PreferencesLookup {
Map<String, Boolean> load(String userId, String viewId);
}
// com.svenruppert.urlshortener.api.store.preferences.PreferencesUpdater
public interface PreferencesUpdater {
void saveColumnVisibilities(String userId, String viewId, Map<String, Boolean> visibility);
void delete(String userId, String viewId, String columnKey);
void delete(String userId, String viewId);
void delete(String userId);
}
Diese klaren Verträge bilden die Basis für verschiedene Speicherstrategien. Besonders die EclipseStore-Variante (EclipsePreferencesStore) integriert sich dabei nahtlos in den bestehenden Objektgraphen. Beim Speichern prüft sie, ob ein Eintrag für die Kombination aus Benutzer- und View-ID bereits existiert, und aktualisiert die Sichtbarkeitsinformationen entsprechend. Ein neuer Eintrag wird nur angelegt, wenn kein vorheriger Zustand existiert. Diese Logik gewährleistet Idempotenz und verhindert unnötige Duplikate.
Das folgende Fragment aus der EclipseStore-Implementierung verdeutlicht das Prinzip:
// com.svenruppert.urlshortener.api.store.provider.eclipsestore.patitions.EclipsePreferencesStore (vereinfachter Auszug)
@Override
public void saveColumnVisibilities(String userId, String viewId, Map<String, Boolean> visibility) {
var userPrefs = dataRoot.preferences().computeIfAbsent(userId, _ -> new HashMap<>());
var viewPrefs = userPrefs.computeIfAbsent(viewId, _ -> new HashMap<>());
viewPrefs.putAll(visibility);
storage.storeRoot(dataRoot);
}
Diese Methode illustriert die Einfachheit und Direktheit der Persistenz: Alle Präferenzen werden im Root-Objekt gespeichert, sodass sie automatisch Teil der transaktionalen Speicherung im EclipseStore werden. Das gesamte System profitiert dabei von der Objektpersistenz ohne klassische Datenbanktabellen oder ORM-Schichten.
Ein entscheidendes Merkmal dieser Lösung ist ihre Resilienz. Da EclipseStore Änderungen automatisch synchronisiert, werden Benutzerpräferenzen selbst dann zuverlässig erhalten, wenn das System unerwartet beendet wird. Zudem profitiert die Anwendung von der direkten Objektadressierung, die EclipseStore bietet: Es sind keine komplizierten ORM-Mappings notwendig, und Änderungen an der Datenstruktur werden automatisch übernommen. Die Speicherung bleibt damit so natürlich wie die Modellierung selbst.
Darüber hinaus bleibt die Persistenzschicht erweiterbar. Durch die Trennung zwischen Interface und Implementierung können künftig auch alternative Speichermechanismen verwendet werden – etwa eine verschlüsselte Dateivariante für besonders sensible Umgebungen oder eine netzwerkbasierte Lösung für Mehrbenutzersysteme. Der bestehende Code der UI und des Server-Handlers müsste hierfür nicht angepasst werden, da alle Zugriffe über die PreferencesStore-Schnittstelle erfolgen.
Mit dieser Integration ist der Persistenzpfad vollständig: Vom Benutzer über den Dialog, den Service, den Client, die REST-Schnittstellen bis schließlich in den EclipseStore verläuft der Datenfluss durchgehend typisiert und konsistent. Damit wird die Spaltensichtbarkeit endgültig zu einem Teil des dauerhaften Systemzustands – ein sichtbares Zeichen dafür, dass Benutzerinteraktion und Datenhaltung in dieser Architektur keine getrennten Welten mehr sind, sondern zwei Perspektiven auf denselben, persistenten Kontext.
Architektur und Ereignisfluss
Die Einführung der dynamischen Spaltensichtbarkeit hat nicht nur neue UI- und Serverkomponenten hervorgebracht, sondern auch die Gesamtheit der Anwendungsarchitektur verfeinert. Mit ihr ist ein konsistenter Ereignisfluss entstanden, der alle Ebenen – vom Benutzerinterface bis hinunter zur Persistenz – aufeinander abstimmt. Diese Durchgängigkeit macht die Funktion nicht nur robust, sondern auch erweiterbar und testbar.
Im Zentrum dieses Flusses steht die OverviewView als Auslöser der Benutzerinteraktion. Wenn der Benutzer das Zahnrad-Symbol betätigt, öffnet sich der ColumnVisibilityDialog, der wiederum über den ColumnVisibilityService kommuniziert. Dieser Service ruft den ColumnVisibilityClient auf, der wiederum die REST-Endpunkte des Servers aufruft. Der Server verarbeitet die Anfrage über die entsprechenden Handler – etwa ColumnVisibilityHandler, ColumnVisibilitySingleHandler oder ColumnVisibilityBulkHandler – und schreibt die Änderungen über den PreferencesStore in den EclipseStore. Schließlich sorgt der Rückkanal dafür, dass die gespeicherten Sichtbarkeiten bei der nächsten Initialisierung automatisch wiederhergestellt werden.
Der Ablauf lässt sich schematisch wie folgt darstellen:
Benutzer → OverviewView → ColumnVisibilityDialog → ColumnVisibilityService
→ ColumnVisibilityClient → REST-API → PreferencesHandler
→ PreferencesStore → EclipseStore → Persistente Speicherung
Jede dieser Komponenten erfüllt eine klar umrissene Rolle und kommuniziert ausschließlich über wohl definierte Schnittstellen. Damit folgt die Architektur streng dem Prinzip der Separation of Concerns. Der UI-Teil bleibt vollständig entkoppelt von der Persistenz, während die Serverlogik keinerlei Kenntnis vom Frontend benötigt. Änderungen an einer Ebene wirken sich somit nicht unmittelbar auf die anderen Ebenen aus.
Ein besonders eleganter Aspekt zeigt sich in der Interaktion zwischen der Benutzeroberfläche und dem Ereignissystem. Die OverviewView ist an das Event-System StoreEvents angebunden. Sobald auf der Serverseite Änderungen am Datenbestand erkannt werden, wird über ein Publish/Subscribe-Muster ein Signal an die UI gesendet. Die Anwendung reagiert darauf mit einem synchronisierten Refresh:
subscription = StoreEvents.subscribe(_ -> getUI().ifPresent(ui -> ui.access(this::refresh)));
Diese Mechanik, die bereits aus den vorherigen Tagen des Adventskalenders bekannt ist, gewährleistet, dass Benutzeroberfläche und gespeicherter Zustand stets übereinstimmen. Sie bildet das Fundament der Reaktivität im gesamten Projekt.
Der Ereignisfluss innerhalb der Anwendung ist somit bidirektional: Einerseits stößt der Benutzer Interaktionen an, die über den Roundtrip bis zur Persistenz führen. Andererseits können persistierte Änderungen wiederum auf die UI zurückwirken. Dieses Wechselspiel zwischen Handlung und Reaktion erzeugt eine dynamische Kohärenz, die das System spürbar lebendig macht.
Im Vergleich zu klassischen Webanwendungen, in denen der Zustand zwischen Client und Server oft fragmentiert oder nur vorübergehend besteht, etabliert diese Architektur einen durchgängigen Datenzyklus. Jeder Schritt ist nachvollziehbar und typisiert – von der Checkbox im Dialog bis zur gespeicherten Boolean-Map im EclipseStore. Fehlerquellen werden dadurch minimiert, und die Wartbarkeit der Anwendung steigt erheblich.
In der Summe zeigt sich, dass die Erweiterung um die dynamische Spaltensichtbarkeit nicht einfach nur eine neue Funktion ist, sondern eine Reifung der gesamten Systemarchitektur. Sie verbindet UI, Ereignissteuerung und Persistenz zu einem kohärenten Ganzen und legt damit den Grundstein für weitere Personalisierungsmechanismen, die auf denselben Prinzipien aufbauen.
UX und Ergonomie: Selbstbestimmtes Arbeiten
Mit der Einführung der dynamischen Spaltensichtbarkeit verändert sich die Beziehung zwischen Benutzer und Anwendung grundlegend. Während die bisherigen Funktionen vor allem auf Konsistenz, Stabilität und Übersichtlichkeit abzielten, tritt nun ein Aspekt in den Vordergrund, der bislang vor allem implizit angelegt war: die Selbstbestimmung des Benutzers. Die Oberfläche der OverviewView ist nicht mehr ein starres Abbild der Entwicklerentscheidungen, sondern wird zu einem anpassbaren Werkzeug, das sich den individuellen Arbeitsweisen seiner Benutzer unterordnet.
Dieser Wandel zeigt sich bereits in der Art, wie der Dialog gestaltet wurde. Der ColumnVisibilityDialog folgt dem Prinzip der minimalen Reibung: Der Benutzer soll seine Ansicht anpassen können, ohne Kontext zu verlieren oder kognitive Last zu empfinden. Deshalb öffnet sich der Dialog modal, fokussiert den Blick auf die aktuelle Aufgabe und bietet nur genau jene Bedienelemente, die für die Entscheidung relevant sind. Jede Checkbox repräsentiert eine Spalte – nicht mehr und nicht weniger. Die unmittelbare Rückmeldung im Grid nach jeder Änderung sorgt dafür, dass der Benutzer die Auswirkungen seiner Entscheidung direkt erlebt.
Die Kombination aus unmittelbarem Feedback und persistenter Speicherung hat einen psychologisch wichtigen Effekt: Sie erzeugt Vertrauen. Wenn ein System sichtbar auf die Präferenzen des Benutzers reagiert und diese beim nächsten Öffnen unverändert wiederherstellt, entsteht das Gefühl von Stabilität und Kontrolle. Dies ist einer der Grundpfeiler guter Interaktionsgestaltung – das System soll nicht nur funktionieren, sondern auch verlässlich wirken.
Auch die Platzierung der Funktion auf der Oberfläche folgt ergonomischen Überlegungen. Das Zahnrad-Symbol, das den Dialog öffnet, ist bewusst unaufdringlich gestaltet. Es ist jederzeit erreichbar, aber nicht dominant. Dadurch bleibt der Fokus auf den Inhalten, während die Kontrolle über die Darstellung stets in Reichweite bleibt. Der Benutzer entscheidet selbst, wann und in welchem Umfang er Anpassungen vornimmt. Das System drängt sich nicht auf, sondern reagiert.
Im Zusammenspiel mit der automatischen Wiederherstellung der Präferenzen beim Start entsteht eine sanfte Personalisierung, die sich organisch anfühlt. Die Anwendung merkt sich nicht nur Zustände – sie erinnert sich auch an Gewohnheiten. Wer bestimmte Spalten ausgeblendet hat, signalisiert damit ein persönliches Nutzungsmuster, das beim nächsten Besuch still respektiert wird. Diese Form der impliziten Personalisierung ist effizient, weil sie ohne zusätzliche Dialoge, Profileinstellungen oder Benutzerkonten auskommt und dennoch ein individuelles Erlebnis erzeugt.
Die Entwicklerperspektive bleibt dabei nicht außen vor. Die Architektur wurde so gestaltet, dass die Erweiterung weiterer persönlicher Einstellungen – etwa Sortierreihenfolgen, Spaltenbreiten oder Layoutpräferenzen – nahtlos möglich wäre. Das bestehende Konzept des PreferencesStore erlaubt die Aufnahme zusätzlicher Parameter ohne strukturelle Änderungen. Damit entsteht ein erweiterbares System, das zwar in der Benutzererfahrung schlicht wirkt, im Hintergrund jedoch hochgradig modular ist.
So wird die Funktion zur dynamischen Spaltensichtbarkeit mehr als nur ein Komfortmerkmal. Sie ist Ausdruck einer Philosophie: Der Benutzer soll sich in der Anwendung wiederfinden können. Eine Software, die sich anpasst, statt Anpassung zu verlangen, schafft Akzeptanz und Produktivität zugleich. Der Benutzer bleibt der Mittelpunkt, die Technik tritt in den Hintergrund – genau dort, wo sie ihre größte Wirkung entfaltet.
Technische Reflexion und Sicherheitsaspekte
Die Einführung der dynamischen Spaltensichtbarkeit ist nicht nur eine funktionale Erweiterung, sondern auch ein technisches Statement. Sie zeigt, dass Personalisierung in einer modernen Webanwendung nicht im Widerspruch zu Robustheit, Sicherheit und Wartbarkeit stehen muss. Vielmehr lässt sich Individualisierung als kontrollierte und nachvollziehbare Erweiterung des bestehenden Sicherheitsmodells integrieren.
Aus technischer Sicht berührt die neue Funktion mehrere sicherheitsrelevante Ebenen. Zunächst ist der Umgang mit Benutzer-IDs zu erwähnen. In der aktuellen Implementierung dient der Benutzername „admin“ als Platzhalter, doch in einer produktiven Umgebung stammen diese Informationen aus einem authentifizierten Kontext. Wichtig ist dabei, dass die Präferenzen strikt an den authentifizierten Benutzer gebunden werden, um eine klare Abgrenzung der Sichtbarkeitsbereiche zu gewährleisten. Ein Benutzer darf ausschließlich auf seine eigenen gespeicherten Präferenzen zugreifen – niemals auf die eines anderen.
Die Schnittstellen selbst sind so gestaltet, dass sie diese Trennung bereits implizit voraussetzen. Jeder Request enthält sowohl eine userId als auch eine viewId. Die REST-Handler prüfen, ob beide Werte vorhanden und gültig sind. Fehlende oder leere Felder führen zu einer kontrollierten Ablehnung der Anfrage:
if (isBlank(req.userId()) || isBlank(req.viewId())) {
writeJson(ex, BAD_REQUEST, "userId and viewId required");
return;
}
Diese einfache, aber effektive Validierung schützt vor unspezifischen Aufrufen und verhindert ungewollte Manipulationen des Datenbestands. In Kombination mit der REST-Semantik, die ausschließlich idempotente Operationen erlaubt (POST, PUT, DELETE), wird sichergestellt, dass die Anwendung auch unter hoher Last oder bei wiederholten Requests deterministisch verhält.
Ein weiterer wichtiger Aspekt betrifft die Datenintegrität innerhalb des PreferencesStore. Da die gespeicherten Werte in einer Map<String, Boolean> organisiert sind, stellt sich die Frage, wie mit ungültigen oder manipulierten Daten umgegangen wird. Die Implementierung nutzt hierfür Typprüfung und defensive Programmierung: Nur bekannte Spaltennamen werden übernommen, unbekannte oder fehlerhafte Schlüssel werden verworfen. Dadurch bleibt der persistierte Zustand konsistent, selbst wenn ein fehlerhafter Client Daten sendet.
var knownKeys = grid.getColumns()
.stream()
.map(Grid.Column::getKey)
.filter(Objects::nonNull)
.toList();
Map<String, Boolean> state = service.mergeWithDefaults(knownKeys);
Diese Logik in der UI verhindert, dass unautorisierte oder unpassende Werte überhaupt gespeichert werden. Der Dialog arbeitet ausschließlich mit jenen Spalten, die tatsächlich im Grid vorhanden sind, und ist damit von Natur aus resistent gegen Manipulationsversuche auf Client-Seite.
Neben der funktionalen Sicherheit spielt auch die Resilienz gegen Fehler eine wesentliche Rolle. Alle Kommunikationspfade zwischen Client und Server sind in try-catch-Blöcke eingebettet. Falls die Verbindung unterbrochen wird oder der Server nicht antwortet, bleibt die Anwendung weiterhin benutzbar. Der Service meldet Fehler über das Logging, ohne die Benutzerinteraktion zu blockieren:
try {
client.editBulk(userId, viewId, changes);
} catch (IOException | InterruptedException e) {
logger().warn("Persist bulk failed {}: {}", changes.keySet(), e.toString());
}
Diese Herangehensweise gewährleistet, dass Ausfälle der Serverkommunikation keine negativen Auswirkungen auf die Benutzererfahrung haben. Der Benutzer kann weiterarbeiten; seine Änderungen werden bei der nächsten erfolgreichen Verbindung automatisch synchronisiert.
Insgesamt zeigt sich, dass das Sicherheitskonzept der Anwendung durch die Erweiterung nicht aufgeweicht, sondern gestärkt wurde. Durch klare Schnittstellen, typisierte Datenstrukturen, defensive Programmierung und strikte Trennung von Benutzerkontexten bleibt die Integrität des Systems gewahrt. Die dynamische Spaltensichtbarkeit beweist, dass Benutzerfreiheit und Systemsicherheit keine Gegensätze sind, sondern sich – bei sorgfältiger Umsetzung – gegenseitig verstärken.
Fazit
Mit der Einführung der dynamischen Spaltensichtbarkeit erreicht die Entwicklung der Administrationsoberfläche einen entscheidenden Meilenstein. Was in den ersten Tagen des Adventskalenders als statische, systemzentrierte Übersicht begann, hat sich nun zu einer interaktiven, benutzerzentrierten Plattform entwickelt. Die Anwendung lernt, reagiert und erinnert sich – sie wird zum Werkzeug, das sich dem Benutzer anpasst, statt ihn zu zwingen, sich an ihre Strukturen zu gewöhnen.
Anstatt bestehende Komponenten zu ersetzen, wurden sie erweitert und präzise miteinander verknüpft. Der neue ColumnVisibilityDialog fügt sich nahtlos in das bestehende UI-Konzept ein, der PreferencesClient nutzt dieselben Mechanismen wie die vorherigen REST-Clients, und die Persistenz über den EclipseStore schließt den Kreis der Datenflüsse. Alles greift ineinander, ohne Brüche oder Sonderpfade. Diese Homogenität ist kein Zufall, sondern das Ergebnis einer Architektur, die von Beginn an Offenheit und Kohärenz als Grundprinzipien verankert hat.
Aus der Perspektive der Benutzererfahrung ist die Erweiterung ein Schritt in Richtung Selbstbestimmung. Benutzer können ihre Arbeitsumgebung nun personalisieren, ohne sich durch Menüs und Optionen herumzuquälen. Die Oberfläche bleibt leicht, das Verhalten ist nachvollziehbar und die gespeicherten Präferenzen schaffen Vertrauen in die Stabilität der Anwendung. Das System reagiert auf Benutzerentscheidungen, statt sie zu erzwingen – ein Paradigmenwechsel, der die Qualität der Interaktion spürbar erhöht.
Die klare Trennung zwischen Benutzerkontext, Sichtbarkeitslogik und Datenhaltung verhindert Manipulationen und gewährleistet die Datenintegrität. Die Verwendung von typisierten Maps und wohldefinierten REST-Endpunkten minimiert Fehlerquellen. Die Persistenz über EclipseStore bietet zusätzlich den Vorteil, dass die Anwendung jederzeit konsistent bleibt, selbst bei abrupten Unterbrechungen oder Teilausfällen.
Der Blick nach vorn eröffnet neue Möglichkeiten. Auf Grundlage des nun etablierten Präferenzsystems lassen sich weitere Personalisierungsdimensionen umsetzen: Spaltenreihenfolgen, Sortierpräferenzen, Farbschemata oder Layoutoptionen. Durch die lose Kopplung von UI, Service und Persistenz ist das System bereit für diese Erweiterungen, ohne dass bestehende Strukturen verändert werden müssen. Auch die Integration mit Benutzerprofilen oder rollenbasierten Zugriffsmodellen könnte darauf aufbauen. Cheers Sven