Wäre es nicht praktisch, wenn man eine E-Mail verschicken könnte, in dem man die Komponenten, die man für die Darstellung auf der Webseite verwendet, auch für das Erstellen der E-Mail heranziehen könnte. Das Unterfangen ist leider nicht trivial, weil man nicht ohne Weiteres um die "Magie", die Wicket an vielen Stellen benutzt, herum kommt. Nun bietet Wicket die Möglichkeit, Komponenten und Seiten, Formulare und vieles mehr in Unit-Test zu testen. Was liegt also näher, als diese Funktionalität für unsere Zwecke zu missbrauchen.
Die E-Mail
Als erstes erstellen wir eine Seite, die den Inhalt der E-Mail erzeugen soll:
package de.wicketpraxis.web.blog.pages.questions.email;
import java.util.List;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.model.IModel;
public class EmailContentPage extends WebPage
{
public EmailContentPage(IModel<List<? extends String>> list)
{
add(new ListView<String>("list",list)
{
@Override
protected void populateItem(ListItem<String> item)
{
item.add(new Label("name",item.getModel()));
}
});
}
}
Der Code soll zeigen, dass Komponenten ganz normal Funktionieren. Deshalb bekommt die Seite von außen eine Liste mit Werten übergeben, die dann durch eine ListView dargestellt wird.
<h1>TestEmail</h1>
<ul>
<li wicket:id="list"><span wicket:id="name"></span></li>
</ul>
Im Markup habe ich absichtlich den "Rahmen" der Seite (html > body) weggelassen. Die Klasse und das Markup dienen in diesem Beispiel nur als Platzhalter.
Darstellen und Abschicken
In Wicket sind sehr viele Objekte an den aktuellen Thread gebunden. Das ist notwendig, damit man nicht eine ganze Liste von Objekten als Übergabeparameter durch Methodenaufrufe schleifen muss. Das ist allerdings genau dann ein Problem, wenn wir innerhalb einer Wicketanwendung eine andere Seite darstellen möchten. Außerdem ist es ein Problem, wenn es wie in einem im Hintergrund laufenden Prozess überhaupt keine WicketApplication gibt. Daher erstellen wir uns ein paar Hilfsklassen, die sich um dieses Problem kümmern.
package de.wicketpraxis.web.blog.pages.questions.email;
public interface WicketCallback<I,O>
{
public O getResult(I input);
}
package de.wicketpraxis.web.blog.pages.questions.email;
import java.util.logging.Logger;
public class WicketThreadAdapter<I,O> extends Thread
{
private static final Logger _logger = Logger.getLogger(WicketThreadAdapter.class.getName());
Object _lock=new Object();
WicketCallback<I, O> _callback;
I _input;
O _output;
boolean _done=false;
protected WicketThreadAdapter(WicketCallback<I, O> callback, I input)
{
_callback=callback;
_input=input;
}
@Override
public void run()
{
synchronized (_lock)
{
_output=_callback.getResult(_input);
_done=true;
_lock.notify();
}
}
protected O getResult() throws InterruptedException
{
synchronized (_lock)
{
while (!_done)
{
_lock.wait();
}
return _output;
}
}
public static <I,O> O getResult(WicketCallback<I, O> callback, I input) throws InterruptedException
{
WicketThreadAdapter<I,O> threadAdapter = new WicketThreadAdapter<I,O>(callback,input);
threadAdapter.start();
return threadAdapter.getResult();
}
}
Die WicketThreadAdapter-Klasse wird über die statische Methode getResult(...) angesprochen und macht folgendes: Es wird ein neuer Thread erzeugt. Im laufenden Thread wird der Callback aufgerufen und der Thread beendet. Das Ergebnis, das im Thread ermittelt wurde, wird als Ergebnis herausgereicht. Kurz: Das Ergebnis wird in einem anderen Thread als dem aktuellen ermittelt und führt so nicht zu Kollisionen mit der laufenden Anwendung.
Der Trick
Das alles führt noch nicht zum Ziel. Wir müssen uns jetzt noch darum kümmern, dass wir die Seite dargestellt bekommen. Dazu benutzten wir die Klasse BaseWicketTester, die sich um alles weitere kümmert. Dort übergeben wir die Seite, die wir darstellen wollen. Dann können wir auf den Inhalt der Seite zugreifen und als Ergebnis zurück liefern.
package de.wicketpraxis.web.blog.pages.questions.email;
import java.util.Arrays;
import java.util.List;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.util.tester.BaseWicketTester;
public class EmailFromComponentPage extends WebPage
{
public EmailFromComponentPage()
{
WicketCallback<List<String>, String> callback = new WicketCallback<List<String>, String>()
{
public String getResult(List<String> input)
{
final IModel<List<? extends String>> listModel = Model.ofList(input);
BaseWicketTester tester=new BaseWicketTester();
tester.startPage(new EmailContentPage(listModel));
return tester.getServletResponse().getDocument();
}
};
String result;
try
{
result = WicketThreadAdapter.getResult(callback, Arrays.asList("Klaus","Susi","Bert"));
}
catch (InterruptedException e)
{
e.printStackTrace();
result=e.getLocalizedMessage();
}
add(new Label("email",result).setEscapeModelStrings(false));
}
}
Damit das Ergebnis richtig angezeigt wird, teilen wir dem Label "email" mit, dass es den Text nicht umschreiben soll.
<html>
<head>
<title>Email Form Component</title>
</head>
<body>
Email <span wicket:id="email"></span>
</body>
</html>
Wenn man die Seite aufruft, erhält man dann folgende Darstellung:
Wenn man die E-Mail durch Hintergrundprozesse versenden möchte, ist ein eigener Thread unnötig, weil keine WicketApplication-Klasse an den Thread gebunden ist. Dann sollte der Einsatz der BaseWicketTester-Klasse reichen. Es ist allerdings darauf zu achten, dass alles, was in der eigenen Application-Klasse definiert wurde, an dieser Stelle nicht ohne weiteres zur Verfügung steht (z.B. wird der OpenSessionInViewFilter für Spring nicht aufgerufen, die SpringBean-Annotationen sind ebenfalls funktionslos).
Fazit
Auch wenn sicher noch die eine oder andere Anpassung notwendig ist, damit man Komponenten sowohl für die Anwendung als auch für den E-Mail-Versand benutzten kann, lohnt sich der Aufwand, weil man a) auf die vielfältigen Möglichkeiten, die Wicket als "Template-Engine" bietet, zurückgreifen kann, b) man sich im besten Fall doppelten Code ersparen kann und c) man keine zweite Lösung einbinden muss. Und wieder hat mich Wicket ein wenig überrascht. Weil es dann doch so einfach ging:)