19
.
1
.
2011

Apache Wicket – Best practices

Deutsch

Dieser Artikel bietet einige Kochrezepte zum richtigen, effizienten und nachhaltigen Umgang mit Apache Wicket.

Dieser Artikel richtet sich an Entwickler, die bereits erste Erfahrungen mit Apache Wicket gesammelt haben. Entwickler, die in Wicket-Welt einsteigen tun sich oftmals schwer, weil sie Entwicklungsmethoden aus der JSF- oder Struts-Welt adaptieren. Diese Frameworks setzen vorrangig auf prozedurale Programmierung. Wicket hingegen setzt massiv auf Objektorientierung. Also vergessen Sie die Struts und JSF-Patterns, sonst werden Sie nicht lange Freude an Wicket haben.

Kapseln Sie die Komponenten richtig

Eine Komponente sollte in sich geschlossen sein. Ein Nutzer der Komponente sollte nichts über deren internen Aufbau wissen müssen, um sie zu verwenden. Interessant sind für ihn nur die externen Schnittstellen. Erbt eine Komponente von der Klasse Panel, so muss sie ihr eigenes HTML-Template mitbringen. Erbt hingegen eine Komponente von den KlassenWebMarkupContainer oder Form, so bringen diese kein eigenes Markup mit. Dies hat zur Folge, dass diese keine vererbten Komponenten besitzen sollten.

github:edc3aac9c80aebe5a05a

Listing 1 ist ein Beispiel für eine schlechte Komponente. Hier muss der Nutzer diesesRegistrationForm’s genau wissen wie sich das Markup aufbaut.

github:f192c8dc58b5186cf105
github:5ab1e7f30c7891a36284

Listing 2 zeigt die Verwendung der schlechten Komponente im Java-Code und das entsprechende Markup, welches für die RegistrationPage benötigt wird. Hier erkennt man, dass die Input-Felder firstnamelastname und username verwendet werden obwohl diese nicht explizit in der RegistrationPage hinzugefügt wurden. Dies ist schlecht, da aus der RegistrationPage Klasse nicht ersichtlich ist, dass diese Komponenten vorhanden sind.

github:8938d68d49d305dc664e
github:4ca2bf639d37f404e24d

Listing 3 zeigt nun eine sauber geschnittene Eingabe-Komponente, die Ihr eigenes Markup mitliefert. Außerdem sehen wir hier die richtige Verwendung des Forms. Es werden nur Komponenten von außen über form.add(Component) hinzugefügt. Behaviors und Validatoren hingegen können auch über Vererbung hinzukommen.

github:da01f9e8a7af7561c406
github:6642de5af6f2af3bf903

Listing 4 zeigt die Verwendung des RegistrationInputPanel’s. Es ist kein Markup mehr aus einer anderen Komponente vorhanden, sondern nur noch Markup von Komponenten die direkt hinzugefügt werden. Die RegistrationPage bringt ihr eigenes Formular mit, welches beim Submit an alle unteren Wicket-Formulare den Submit weiter delegiert.

Speichern Sie Models und Seiteninformationen in Member-Variablen

Im Gegensatz zu Struts sind in Wicket die Instanzen von Pages und Komponenten keine Singletons, sondern sessionbezogene Instanzen. Dadurch bietet sich die Möglichkeit userspezische und flowgebundene Informationen innerhalb dieser Komponenten und Pages abzulegen. Die Informationen sollten in Member-Variablen abgelegt werden. So kann innerhalb der gleichen Klasse auf die Informationen zugegriffen werden und man vermeidet lange Methoden-Signaturen an die man die Informationen weiterreichen muss. Wicket-Pages sind somit stateful. Trotzdem können sie über mehrere Requests bestehen. Beispielsweise eine Seite mit einem Formular das beim Abschicken einen Validierungsfehler produziert. Hier wird die selbe Page-Instanz verwendet. Eine weitere Möglichkeit, wo die selbe Page-Instanz verwendet wird, ist wenn ein User den Back-Button drückt und auf die vorherige Seite gelangt und das Formular noch einmal abschickt. Informationen die von außen über den Konstruktur hinein gegeben werden, sollten einer Member-Variable zugewiesen werden (in der Regel sind dies Models). Bei der Ablage von Informationen in Member-Variablen ist jedoch darauf zu achten, dass diese serialisierbar sind, denn in Wicket werden die erstellten Pages in der Pagemap abgelegt. Die Pagemap speichert standardmäßig die Pages auf der Festplatte. Sind die Informationen nicht serialisierbar führt dies zwangsläufig zu NullPointerExceptions undNonSeriablizableExceptions. Weiterhin sollten große Informationsblöcke – wie Binärdaten – auch nicht direkt in einer Member-Variable abgelegt werden, da dies zu Performance-Einbußen beim Serialisieren führen kann. Hierfür sollte zum Beispiel ein LoadableDetachableModel verwendet werden, welches in einer Member-Variable abgelegt werden darf, da sich dieses um das Laden und Speicherfreigeben kümmert.

Wicket IDs richtig benennen

Benennung spielt für viele Entwickler eine nebensächliche Rolle ist aber eine der grundlegensten Themen in der Softwareentwicklung. Anhand der richtigen Benennung von Variablen und Methoden identifiziert man schnell die fachlichen Aspekte eines Softwarebestandteils. Gute Benennung vermeidet zudem überflüssige Kommentare.

Schlechte Benennungen für Wicket-IDs sind zum Beispiel birthdateTextFieldfirstnameField oderaddressPanel. Warum? In der Benennung sind zwei Aspekte vorhanden: Der technische Aspekt “TextField” und der fachliche Aspekt “birthdate”. Interessant ist aber nur der fachliche Aspekt, da im HTML Template durch und im Java-Code durch new TextField die technischen Aspekte schon beschrieben sind. Zudem erhöht diese falsche Benennung den Aufwand bei technischen Umbauten. Sollte zum Beispiel das TextField durch einen DatePicker ausgetauscht werden, so müssten zusätzlich alle IDs in birthdateDatePicker mit umbenannt werden. Ein weiterer Grund den technischen Aspekt nicht in die Wicket-ID Benennung mit aufzunehmen ist dasCompoundPropertyModel. Dabei wird das Model in einem Formular auf die Kind-Formelemente delegiert (siehe Listing 3). So wird beim TextField username automatisch setUsername() odergetUsername() auf dem Registration-Objekt aufgerufen. Hier wäre eine BenennungsetUsernameTextfield() äußerst unpraktisch.

Vermeiden Sie Veränderungen am Komponentenbaum

Den Wicket-Komponentenbaum sollte man sich als ein starres Gerüst vorstellen, welches erst zum Leben erweckt wird, wenn es mit einem Model gefüllt ist, ähnlich einem Roboter ohne Gehirn. Ohne Gehirn kann er nichts und ist nur eine starre Hülle. Füllt man ihn jedoch mit Informationen, so wird er zum Leben erweckt und führt Handlungen aus. Einzelne Komponenten können selbst über Ihren Zustand entscheiden, z.B. über die Sichtbarkeit.In Wicket sollte der Komponentenbaum so wenig wie möglich manipuliert werden, das heißt die Nutzung von Methoden wie Component.replace(Component) und Component.remove(Component)sollte vermieden werden. Die Verwendung dieser Methoden deutet auf die Nicht- oder Falsch-Verwendung von Models hin. Weiterhin sollten Komponentenbäume nicht konditional aufgebaut werden (siehe Listing 5).

github:8cfccb8f637b6ba812d4

Statt das LoginBoxPanel konditional aufzubauen, empfiehlt sich das Panel immer hinzuzufügen und die Sichtbarkeiten über eine Implementierung von isVisible() zu steuern. So kann innerhalb des LoginBoxPanel entschieden werden, ob es angezeigt wird oder nicht. Wir verschieben die Verantwortlichkeit für die Sichtbarkeit des Logins direkt in die Komponente, die den Login ausführen soll. Genial. Fachlichkeit wird sauber gekapselt. Es erfolgt keine Entscheidung mehr von Außen. In “Sichtbarkeiten von Komponenten” folgt hierzu ein Beispiel.

Sichtbarkeiten von Komponenten richtig implementieren

Sichtbarkeiten von Seitenbestandteilen sind ein wichtiges Thema. In Wicket wird die Sichtbarkeit über die Methoden isVisible() und setVisible() gesteuert. Diese Methoden befinden sich innerhalb der Wicket-Basis-Klasse Component und betreffen somit ausnahmslos jede Komponente und Page. Kommen wir zu einem konkreten Beispiel, dem LoginBoxPanel. Das Panel wird nur dann angezeigt, wenn ein Benutzer nicht eingeloggt ist.

github:7ce37086b78abb57dc04

Listing 6 zeigt wiederum eine schlechte Implementierung, denn es wird bereits beim Instanziieren der Seite eine Entscheidung über die Sichtbarkeit der Komponente getroffen. In Wicket werden Instanzen von Pages und Komponenten über mehrere Requests hin verwendet. Um die gleiche Instanz der Seite nach einem Login weiter verwenden zu können, müsste manloginBox.setVisible(false) aufrufen. Äußerst unpraktisch, wir müssen uns jedesmal explizit um das Setzen der Sichtbarkeit kümmern. Zudem werden Informationen dupliziert, denn visible entspricht “nicht eingeloggt”. So haben wir zwei gespeicherte States, einmal für den fachlichen Aspekt “nicht eingeloggt” und einmal für den technischen Aspekt visible. Beides ist aber immer gleich. Dieses Vorgehen ist äußerst fehleranfällig und fragil, da wir uns immer darum kümmern müssen, dass die richtige Information zu jedem Zeitpunkt gesetzt wird. Genau dies wird oft vergessen, da sich die Logik über mehrere Stellen verteilt. Die Lösung heißt Hollywood-Prinzip: “Don’t call us, we’ll call you” – Rufen Sie nicht uns an, sondern wir rufen Sie an. Im Diagramm sehen Sie den Ablauf der Aktionen und die dazugehörigen Aufgaben. Durch das Hollywood-Prinzip fallen allein drei Aufgaben weg und es muss nur noch das LoginBoxPanel instanziiert werden.

github:5f95159802abd2486300

Bei Listing 7 wird die Steuerung über die Sichtbarkeit umgekehrt, jetzt entscheidet das LoginBoxPanel über seine Sichtbarkeit selbstständig. Jedes mal, wenn isVisible() aufgerufen wird, wird eine neue Auswertung über den Login-Zustand erfragt, so werden keine alten Informationen über die Sichtbarkeit gehalten. Die Logik ist genau auf einer Zeile Code zentralisiert und nicht mehr breit im Code gestreut. Weiterhin lässt sich der technische AspektisVisible() für die fachliche Anforderung “nicht eingeloggt” herauslesen. Äquivalent gilt die Beschreibung für die Methode isEnabled(). Bei isEnabled() werden die Komponenten ausgegraut dargestellt. Formulare die sich innerhalb einer deaktivierten oder unsichtbaren Komponente befinden werden nicht ausgeführt. Es gibt Fälle in denen man um setVisible() oder setEnabled()nicht herumkommt. Beispiel: Der User klickt einen Button, um das Registrierungsformular aufzuklappen. Generell gilt, wenn Komponenten datengetrieben sind, so wird isVisible()überschrieben und auf das Datenmodel gezeigt. Usergesteuerte Aktionen rufensetVisible(boolean) auf. Die Methoden können auch mit einer Inline-Implementierung überschrieben werden (siehe Listing 8).

github:da4775f1f4bb31559c56

Verwenden Sie ausschließlich Models

Verwenden Sie ausschließlich Models! Geben Sie keine rohen Objekte direkt an Komponenten weiter. Pages und Komponenten können über mehrere Request-Zyklen bestehen. Wenn rohe Objekte verwendet werden, so können diese nicht nachträglich austauscht werden. Ein Beispiel hierfür könnte zum Beispiel eine Entity sein, die in einem LoadableDetachableModel mit jedem Request neugeladen wird. Über den EntityManager würde jedesmal ein neues Objekt erzeugt werden und die Seite würde noch die alte Instanz halten. Übergeben Sie im Konstruktor immerIModel (siehe Listing 9).

github:bc0bd17c5bb91469b078

Mit der Lösung in Listing 9 kann sich hinter dem Model jede Implementierung verbergen. Angefangen von der Klasse Model über das PropertyModel bis hin zur eigenen Implementierung von LoadableDetachableModel, welche die Werte automatisch lädt und persistiert. Die Model-Implementierungen werden so leicht austauschbar. Sie – als Nutzer – interessiert nur Folgendes: wenn IModel.getObject() aufgerufen wird, erhält man ein Objekt vom Typ Registration. Woher dieses Objekt kommt ist Aufgabe der Model-Implementierung und der aufrufenden Komponente. Das Model können Sie beispielsweise beim Instanziieren der Komponenten übergeben und weiterreichen. Sollten sie keine Models verwenden, werden Sie früher oder später in die Verlegenheit kommen, den Komponentenbaum zu manipulieren. Womit Sie wiederum Informationen über States duplizieren und somit produzieren Sie schlecht wartbaren Code. Ein weiterer Punkt warum Sie Models verwenden sollten ist die Serialisierung. Objekte die direkt ohne Models in den Komponenten und Pages in Member-Variablen gespeichert werden, werden mit jedem Request serialisiert und deserialisiert. Dies kann unter Umständen unperformant sein.

Entpacken Sie keine Models innerhalb der Konstruktor-Hierarchie

Vermeiden Sie es, Wicket-Models innerhalb der Konstruktor-Hierarchie zu entpacken, d.h. rufen Sie innerhalb der Konstruktor-Hierarchie IModel.getObject() nicht auf. Wie bereits erwähnt kann eine Page-Instanz mehrere Request-Zyklen überleben, so halten Sie dann veraltete und redundante Informationen. Wicket-Models dürfen bei Events entpackt werden (User-Aktionen), also Methoden wie onUpdate()onClick() oder onSubmit() (siehe Listing 10).

github:dd0ee8643bd319962b2b

Eine weitere Möglichkeit das Model zu entpacken ist durch das Implementieren der MethodenisVisible()isEnabled() oder onBeforeRender().

Reichen Sie Models immer an die Komponenten weiter

Reichen Sie Models immer an die Komponente weiter von der Sie erben. So wird sicher gestellt, dass am Ende jedes Requests die Methode IModel.detach() aufgerufen wird. Diese sorgt dafür, dass Informationen aufgeräumt werden. Beispielsweise haben Sie ein eigenes Model implementiert, welches in der detach()-Methode die Daten persistiert. Ist dieses Model an keiner Komponente weitergegeben worden, so wird die Methode detach() niemals aufgerufen und die Daten somit nicht persistiert. Ein musterhafte Übergabe an den super-Konstruktor sehen Sie in Listing 11.

github:6d878004d829bdd3e052

Validatoren dürfen keine Models und Daten verändern

Validatoren sollen nur validieren. Beispielsweise ein Formular, welches die Kontodaten eines Kunden erfasst. An dem Formular hängt ein BankFormValidator, welcher die Bankdaten über einen Webservice prüft und den Banknamen korrigiert. Niemand rechnet damit, dass ein Validator Informationen verändert. Solche Logik gehört nach Form.onSubmit() oder in die Event-Logik eines Buttons.

Komponenten sollten nicht an Konstruktoren übergeben werden

Übergeben Sie Komponenten oder Pages nicht über den Konstruktor an andere Komponenten weiter.

github:104c219ffab2a45dcee0

Schauen Sie sich Listing 12 an: Die SettingsPage erwartet im Konstruktor die Seite zu der sie nach einem erfolgreichen Submit zurückspringen soll. Diese Variante funktioniert, ist aber äußerst unflexibel und unschön. Man muss bereits zum Zeitpunkt des Instanziierens der SettingsPagewissen, wohin man den Benutzer leitet. Dies setzt eine Instanzierungsreihenfolge voraus. Viel besser wäre es die Anwendungen nach der fachlichen Reihenfolge zu strukturieren. Die Lösung ist wiederum das Hollywood-Prinzip. Hierfür erzeugen wir eine abstrakte Methode oder einen Hook (Listing 13).

github:f7e4c5dc8b2bc1e7e322

Die Variante aus Listing 13 hat ersteinmal mehr Code ist aber deutlich flexibler und aussagekräftiger. Wir wissen, dass es ein Event onSettingsChanged() gibt und dieses wird nach dem Ändern aufgerufen. Zudem ist es möglich weitaus mehr Code auszuführen als nur die Back-Seite zu setzen, beispielsweise kann man zusätzliche Informationen ausgeben oder andere Informationen persistieren.

Die Wicket-Session nur für globale Informationen nutzen

Die Wicket-Session ist ein typisiertes Objekt. Es werden keine Informationen mehr über eine Map-Struktur gespeichert. Benutzen Sie die Wicket-Session nur für globale Informationen. Authentifizierung ist das Parade-Beispiel für globale Informationen. Die Login- und User-Informationen werden im Normalfall auf fast jeder Seite benötigt. Im Beispiel von einem Blog ist es sinnvoll zu wissen, ob es sich um einen Author handelt der Beiträge verfassen darf. So wird ein Link zum Bearbeiten ein- oder ausgeblendet. Generell gehört sämtliche Logik zur Authentifizierung in die Wicket-Session, da sie normalerweise applikationsweit benötigt wird. Und wenn nicht, dann ist es trotzdem gut, dass die Logik in der Session liegt, da man sie dort erwarten würde.Daten von Formularen, die sich beispielsweise über mehrere Seiten erstrecken, haben in der Session nichts verloren. Diese Daten können beim Instanziieren der Seiten über Models von einer Seite auf die Nächste weitergereicht werden (Listing 14). So haben die Models und Daten einen festen Lebenszyklus über den Seitenverlauf.

github:65249219cc0fbb2b56ad

So wie im Listing 14 werden konkrete Informationen über die Pages direkt weitergereicht. Sämtliche Models können bedenkenlos in Member-Variablen gespeichert werden. Wicket-Pages sind im Gegensatz zu Struts-Actions keine Singletons, sondern userspezifische Instanzen. Der große Vorteil dieses Vorgehens ist, dass die Daten automatisch aufgeräumt werden, wenn der Benutzer den Page-Flow beendet hat oder aus ihm vorzeitig aussteigt. Nie wieder manuelles aufräumen! Dies ist quasi ein Garbage-Collector für Ihre Session.

Verwenden Sie keine Factories für Komponenten

Das Factory-Pattern ist ein nützliches Pattern, jedoch für Wicket-Komponenten ungeeignet.

github:fc968e57cad62f62f9ef

Die Lösung in Listing 15 für das Hinzufügen des Labels aus der CmsFactory sieht zunächst nicht schlecht aus, bringt aber Nachteile mit sich. Es ist nun nicht mehr möglich mit Vererbung zu arbeiten, es ist nicht mehr möglich isVisible() oder isEnabled() zu überschreiben. Die Factory könnte auch ein Spring-Service sein, der eine Komponente instanziiert. Die richtige Variante wäre hier ein CmsLabel zu erstellen (Listing 16).

github:fc3222e9165633db19d2

In Listing 16 ist das Label sauber in der Komponente gekapselt und das ohne Factory. Es ist problemlos möglich eine Inline-Implementierung zu erstellen und so isVisible() oder Sonstiges zu überschreiben. Jetzt folgt das Argument: “Ich benötige eine Factory für das Initialisieren von diversen Werten in der Komponente.”. Hierfür gibt es in Wicket dieIComponentInstantiationListener, diese werden direkt im super-Konstruktor von der Component-Klasse aufgerufen. Das bekannteste Beispiel ist der SpringComponentInjector, welcher dafür sorgt, dass Spring-Beans bei der Annotation @SpringBean injected werden. Hier können problemlos weitere IComponentInstantiationListener geschrieben und hinzugefügt werden. Somit gibt es kein Argument mehr, welches für eine Factory spricht. Weitere Informationen zumIComponentInstantiationListener finden Sie in der JavaDoc.

Jede Seite und Komponente erwartet einen Test

Jede Seite und Komponente sollte einen entsprechenden Test besitzen. Der Basis-Test rendert einfach die Komponente und prüft, ob diese technisch korrekt ist. Beispielsweise sei genannt, ob für jede Kind-Komponente eine entsprechende Wicket-ID im Markup vergeben ist. Ist eine Wicket-ID nicht korrekt gebunden so schlägt der Test fehl. Ein weiterführender Test könnte zum Beispiel ein Formular sein bei dem ein entsprechender Backend-Call stattfindet und man über einen Mock diesen Call validiert. Auf jeden Fall können so technische und fachliche Fehler bereits im Build-Prozess erkannt und behoben werden. Auch für Test-Driven Development ist Wicket bestens geeignet. Stellen Sie sich vor, Sie erstellen eine Seite und bevor der Server hochfahren wird, lassen Sie den Unit-Test laufen, der Ihnen sagt, dass Sie eine Wicket-ID nicht gebunden haben. Nachdem der Fehler behoben und der Unit-Test erfolgreich durchgelaufen ist, fahren Sie den Server hoch. Einen Server hochzufahren dauert länger als einen Unit-Test auszuführen. Dies verkürzt den Entwicklungs-Turnaround deutlich. Einziger Nachteil beimWicketTester ist, dass sich AJAX-Komponenten schwer testen lassen. Jedoch sind die Test-Möglichkeiten, die Wicket bereits bietet deutlich größer als bei jedem anderen Web-Framework.

Interaktion mit anderen Servlet-Filtern vermeiden

Bleiben Sie so lang sie können innerhalb der Wicket-Welt. Vermeiden Sie die Verwendung von Servlet-Filtern, hierfür kann man den WebRequestCycle verwenden, wo die MethodenonBeginRequest() und onEndRequest() existieren. Die HttpSession ist genauso tabu. Das Äquivalent hierfür ist die WebSession von Wicket, einfach von WebSession erben und in der Application-Klasse, durch das Überschreiben der newSession()-Methode, registrieren. Es gibt wenige Ausnahme-Fälle, um auf die Servlet-Schnittstellen zuzugreifen. Ein Beispiel ist, wenn ein externes Cookie auswertet werden muss, um den User zu authentifizieren. Diese Schnittpunkte sollten möglichst gekapselt und minimiert werden. Für dieses Beispiel könnte man die Auswertung des Cookies in der Wicket-Session erledigen, da dies eine Authentifizierung ist.

Schneiden Sie kleine Klassen und Methoden

Vermeiden Sie monolithische Klassen. Oft kommt es vor, dass Entwickler alles in den Konstruktor packen. Diese Klassen werden sehr schnell unübersichtlich, da in Wicket oft Inline-Implementierungen über mehrere Ebenen gemacht werden. Gruppieren sie logische Einheiten und extrahieren Sie hierfür eigene Methoden mit einer fachlich korrekten Bezeichnung. Dies steigert die Übersicht und das Verständnis für den fachlichen Hintergrund der Komponenten. Navigiert ein Entwickler in die Komponente so interessiert ihm Anfangs nicht die technische Ausprägung, sondern die Fachliche. Um detaillierte technische Informationen zu erhalten navigiert man schließlich in die Methoden. Im Zweifel sollten Sie auch in Betracht ziehen eigene Komponenten herauszuschneiden. Kleinere Komponenten erhöhen die Wahrscheinlichkeit der Wiederverwendung und lassen sich deutlich einfacher testen. Listing 17 zeigt ein Beispiel für eine mögliche Strukturierung Ihrer Komponenten.

github:81679348d8158604af0e

Das Argument “Schlechte Dokumentation”

Des öfteren höre ich, dass Wicket über eine schlechte Dokumentation verfügt. Dieses Argument stimmt nur zum Teil. Es gibt aber eine Menge an Wicket-Code Beispielen, welche man als Vorlagen verwenden kann. Zudem gibt es eine große Community die innerhalb kürzester Zeit auch komplexeste Fragen klärt. In Wicket ist es recht schwierig alles zu dokumentieren, da fast alles austauschbar und erweiterbar ist. Reicht einem eine Methode oder Implementierung nicht aus, so wird diese erweitert oder überschrieben. Die Arbeit mit Wicket gleicht einem ständigem Navigieren durch den Code. Als Beispiel seien die Validatoren genannt. Wie finde ich alle Validatoren heraus die es gibt? Man öffnet das Interface IValidator (Eclipse Strg + Shift + T) und anschließend die Type-Hierachy (Strg + T) und schon haben wir alle Validatoren im Überblick.

Fazit

Die Ratschläge sollten Ihnen helfen, besseren und wartbaren Code in Wicket zu schreiben.Alle beschriebenden Methodiken wurden bereits erfolgreich in mehreren Wicket-Projekten erprobt. Wenn Sie diese Ratschläge befolgen sind Ihre Wicket-Projekte gut für die Zukunft gerüstet und werden mit Sicherheit ein Erfolg.

Links:1. http://wicket.apache.org/ (Apache Wicket)2. http://wicketstuff.org/wicket14/ (Wicket Examples)3. http://en.wikipedia.org/wiki/Hollywood_Principle

Vielen Dank an Daniel Bartl für das Korrektur lesen.

No items found.