Wicket 6.2, WebSockets und JQuery-Visualize – Die richtige Atmosphäre schaffen

Im letzten Artikel zum Thema Wicket 6 ging es um JQuery und Ajax. Heute geht es um die Native WebSockets Integration – Rock´n´Roll!

Zunächst erzeugen wir uns wieder einen Wicket Maven Archetype.

Das Projekt erzeugen

[code]

<code>mvn archetype:generate -DarchetypeGroupId=org.apache.wicket -DarchetypeArtifactId=wicket-archetype-quickstart -DarchetypeVersion=6.2.0 -DgroupId=de.effectivetrainings -DartifactId=wicket-6-websockets -DarchetypeRepository=<a href="https://repository.apache.org/">https://repository.apache.org/</a> -DinteractiveMode=false</code>

[/code]

Wieso braucht man überhaupt so etwas wie WebSockets und warum sind sie hier?

WebSockets setzen direkt auf TCP/IP auf und ermöglichen eine bidirektionale Kommunikation zwischen Client und Server – toll… und?

Auf gut deutsch gesagt, wir haben die Möglichkeit, Nachrichten vom Server zum Client zu schicken und zwar ohne, dass der Client (Browser) ständig Pollen (Beim Server anfragen) muss.

Was wäre ein einfacher Use-Case?

Beispielsweise kann man sich vorstellen, wir haben eine Webanwendung, die von unserem Kunden verwendet wird, um Daten zu visualisieren, beispielsweise eingegangene Bestellungen in unserem Online-Shop.

Ohne WebSockets müsste man entweder jedesmal die Seite neu laden, um an die aktuellen Daten zu kommen oder via Ajax (Magie..) ständig beim Server anfragen ob denn nicht zufällig neue Daten vorhanden sind. Beides funktioniert, hat sich etabliert, wurde schon millionenfach implementiert und funktioniert – schick ist eben aber was anderes.

Man kann sich eine WebSocket-Implementierung einfach so vorstellen, dass ein Client eine Anfrage an den Server schickt, diese Anfrage wird aber nicht sofort beantwortet, sondern der Server wartet damit, bis tatsächlich eine Antwort Sinn macht (neue Daten vorhanden sind, die den Client interessieren). Sobald die Sinnhaftigkeit geklärt ist schickt der Server eine Nachricht an alle interessierten Clients und diese können die Visualisierung aktualisieren. Macht Sinn? Definitiv.

Wicket und WebSockets

Wir sind uns denke ich alle einig, dass Wicket das beste Web-Framework ist, das derzeit am Markt verfügbar ist (keine Diskussion!). Wicket 6 hat hier nochmal einen richtigen Schub an schönen Features gebracht – u.a. eben die Native-WebSocket-Integration.

Beispiel

Wir haben uns ja bereits einen wunderschönen Archetype generiert. Wir haben uns auch schon einen brauchbaren Use-Case ausgedacht – Visualisierung von Bestellungen am Client. Implementieren wir das Ganze.

WebSockets aktivieren

Um WebSockets in einer Wicket-Anwendung zu aktivieren verwendet Wicket selbstverständlich ein Behavior..

Bauen wir uns zunächst eine sehr einfaches Formular, mit dem wir Bestellungen simulieren können. Im Archetype gibt es ja bereits die Klasse HomePage. Diese passen wir einfach folgendermaßen an (da es sich hierbei um Wicket Standard Bordmittel handelt, gehe ich darauf nicht genauer ein, es soll ja schließlich um WebSockets gehen).

Wir brauchen ausserdem einige einfache Domainklassen –  Food und Order (wir bauen einen Lieferservice für Essen – nein nicht Pizza – dafür gibts schon genug Beispiele).

[code language=”java”]

public enum Food {
PIZZA, LEBERKAS, BURGER, SALAT, SPIEGELEI
}

public class Order implements Serializable {

//some meaningless random order id
private String orderId = String.valueOf(Math.random());
private String name;
private String street;
private String zip;
private String city;
private Food food;

public Order(){}

public String getOrderId() {
return orderId;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getStreet() {
return street;
}

public void setStreet(String street) {
this.street = street;
}

public String getZip() {
return zip;
}

public void setZip(String zip) {
this.zip = zip;
}

public String getCity() {
return city;
}

public void setCity(String city) {
this.city = city;
}

public Food getFood() {
return food;
}

public void setFood(Food food) {
this.food = food;
}

@Override
public String toString() {
return "Order{" +
"name=’" + name + ”’ +
", street=’" + street + ”’ +
", zip=’" + zip + ”’ +
", city=’" + city + ”’ +
", food=" + food +
‘}';
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;

Order order = (Order) o;

if (city != null ? !city.equals(order.city) : order.city != null) return false;
if (food != order.food) return false;
if (name != null ? !name.equals(order.name) : order.name != null) return false;
if (orderId != null ? !orderId.equals(order.orderId) : order.orderId != null) return false;
if (street != null ? !street.equals(order.street) : order.street != null) return false;
if (zip != null ? !zip.equals(order.zip) : order.zip != null) return false;

return true;
}

@Override
public int hashCode() {
int result = orderId != null ? orderId.hashCode() : 0;
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + (street != null ? street.hashCode() : 0);
result = 31 * result + (zip != null ? zip.hashCode() : 0);
result = 31 * result + (city != null ? city.hashCode() : 0);
result = 31 * result + (food != null ? food.hashCode() : 0);
return result;
}
}

[/code]

Über die Sinnhaftigkeit dieser Domainklassen würde sich vortrefflich streiten lassen, für das Beispiel ist das aber ausreichend.

Hier jetzt die einfache Wicket Implementierung des Formulars.

[code language=”java”]

public class HomePage extends WebPage {
private static final long serialVersionUID = 1L;

public HomePage(final PageParameters parameters) {
super(parameters);
Form orderForm = new Form("form", new             CompoundPropertyModel(new Order())){
@Override
protected void onSubmit() {
super.onSubmit();
System.out.println(getModelObject());
}
};
orderForm.add(new TextField("name"));
orderForm.add(new TextField("street"));
orderForm.add(new TextField("zip"));
orderForm.add(new TextField("city"));
orderForm.add(new DropDownChoice("food", Arrays.asList(Food.values())));
add(orderForm);
}
}

[/code]

und das ensprechende Markup dazu.

[code language=”html”]
<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org">
<head>
<meta charset="utf-8"/>
<title>Apache Wicket Quickstart</title>
<link href=’http://fonts.googleapis.com/css?family=Yanone+Kaffeesatz:regular,bold’ rel=’stylesheet’
type=’text/css’/>
<link rel="stylesheet" href="style.css" type="text/css" media="screen" title="Stylesheet"/>
<style>
fieldset {
width: 350px;
float: left;
text-align: right;
}
</style>
</head>
<body>
<form style="margin: 25px" wicket:id="form">
<h2>Wo gibts das beste Mittagessen?</h2>
<fieldset>
<label>Name:</label> <input type="text" wicket:id="name"/><br/>
<label>Strasse:</label> <input type="text" wicket:id="street"/><br/>
<label>PLZ: </label><input type="text" wicket:id="zip"/><br/>
<label>Stadt:</label> <input type="text" wicket:id="city"/><br/>
<label>Was möchtest Du essen?</label> <select wicket:id="food"/><br/>
<input type="submit"/>
</fieldset>
</form>
</body>
</html>

[/code]

Das Ganze ergibt ausgeführt das, schick nich?

Ziel ist es, unsere eingegangenen Bestellungen zu visualisieren. Das halten wir möglichst einfach (schön wäre es jetzt, wenn wir eine lokale DB hochziehen würden, hier schön die Bestellungen via Hibernate persistieren etc..). Das sparen wir uns, wir nehmen die einfachste Datenbank, die man sich vorstellen kann – ein gutes altes Singleton mit einer gekapselten HashMap (ist es hier gerade jemandem kalt den Rücken hinunter gelaufen?).

[code language=”java”]

public class DB {

private static DB instance = new DB();

private Map<String, List<Order>> orders;

private DB() {
this.orders = Collections.synchronizedMap(new HashMap<String, List<Order>>());
}

public void store(Order order) {
List<Order> orders = this.orders.get(order.getFood().name());
if(orders == null){
this.orders.put(order.getFood().name(),new ArrayList<Order>());
}
this.orders.get(order.getFood().name()).add(order);

}

/*
* do not copy that….
* */
public Map<String, Object> countOrdersByFood() {
Map<String, Object> foodCount = new HashMap<String, Object>();
for(Map.Entry<String, List<Order>> order : this.orders.entrySet()){
foodCount.put(order.getKey(),order.getValue().size());
}
return foodCount;
}

public static final DB get() {
return instance;
}
}

[/code]

Bitte nicht kopieren, hässlicher gehts kaum… Das interessante an unserer “DB” ist die Methode “countOrdersByFood” die eine Map zurückgibt, die die Anzahl von Bestellungen für bestimmte Gerichte beinhaltet. Genau diese Methode verwenden wir später für unsere Visualisierung. Dass der Rückgabewert eine Map vom Typ <String,Object> ist, hat mit der Convenience-Klasse JsonUtils zu tun, die standardmässig mit Wicket ausgeliefert wird.

Das Einzige was wir jetzt noch machen müssen ist in der onSubmit des Formulars folgender Call.

[code language=”java”]

protected void onSubmit() {
super.onSubmit();
DB.get().store(getModelObject());
}

[/code]

Damit ist unsere Infrastruktur aufgesetzt. Jetzt gehts an die Visualisierung. Hierfür verwenden wir das hervorragende JQuery-Visualize-Plugin (bietet sich ja an, da JQuery nativ schon über Wicket verfügbar ist)

Integration des Visualize Plugins

Wir brauchen das entsprechende Plugin und natürlich die native JQuery Bibliothek.
Hierfür bauen wir uns ein kleines HeaderItem, das beides rendert und dafür sorgt, dass die Bibliotheken zur Laufzeit verfügbar sind.

[code language=”java”]

public class JQueryVisualizePlugin extends JavaScriptUrlReferenceHeaderItem {

private static final String VISUALIZE_PLUGIN_URL = "https://raw.github.com/filamentgroup/jQuery-Visualize/master/js/visualize.jQuery.js";

public JQueryVisualizePlugin() {
super(VISUALIZE_PLUGIN_URL, "jquery-visualize", true, "utf-8","");
}

@Override
public Iterable<?> getRenderTokens() {
return Arrays.asList("jquery-visualize");
}

@Override
public Iterable<? extends HeaderItem> getDependencies() {
List<HeaderItem> deps = new ArrayList<HeaderItem>();
deps.add(JavaScriptHeaderItem.forReference(WicketEventJQueryResourceReference.get()));
return deps;
}
}

[/code]

Eine Erklärung für diese Implementierung findet sich im letzten Artikel.

Jetzt brauchen wir nur noch eine Page, um unseren OrderReport zu rendern.

[code language=”java”]

public class OrderReportPage extends WebPage {

@Override
public void renderHead(HtmlHeaderContainer container) {
super.renderHead(container);
container.getHeaderResponse().render(new JQueryVisualizePlugin());
}
}

[/code]

und das entsprechende Markup dazu.

[code language=”html”]

<html xmlns:wicket="http://wicket.apache.org">
<head>
<meta charset="utf-8"/>
<title>OrderReport</title>
</head>
<body>
<table>
<caption>Essensbestellungen nach Art</caption>
<thead>
<tr>
<td></td>
<th scope="col">Pizza</th>
<th scope="col">Leberkas</th>
<th scope="col">Burger</th>
<th scope="col">Salat</th>
<th scope="col">Spiegelei</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>2</td>
<td>3</td>
<td>4</td>
<td>5</td>
<td>6</td>
</tr>

</tbody>
</table>
</body>
</html>

[/code]

Das JQuery-Visualize Plugin erwartet eine ganz  bestimmte Table-Struktur, um damit zu arbeiten. Wir möchten die Anzahl Bestellungen visualisieren, also definieren wir pro Menüart eine Spalte die initial mit 1-6 befüllt sind.

Beispiele für das JQuery Visualize Plugin finden sich hier.

Fangen wir an. Um die Visualisierung zu testen brauchen wir folgendes JavaScript, das wir initial einfach in der Page rendern.

[code language=”html”]

$(‘table’).visualize();

[/code]

Das machen wir einfach genauso direkt in der Page.

[code language=”java”]

@Override
public void renderHead(HtmlHeaderContainer container) {
super.renderHead(container);
container.getHeaderResponse().render(new JQueryVisualizePlugin());
container.getHeaderResponse().render(new OnDomReadyHeaderItem("$(‘table’).visualize()"));
}

[/code]

Das Skript soll erst dann ausgeführt werden, wenn der DOM komplett aufgebaut ist, deswegen verwenden wir ein OnDomReadyHeaderItem. Was hier gerendert wird ist das.

[code language=”javascript”]

<script language="javascript">
<pre id="line1">/*<![CDATA[*/
Wicket.Event.add(window, "domready", function(event) {
$(‘table’).visualize();
;});</pre>
</script>

[/code]

Schauen wir uns das Ganze im Browser an, sehen wir folgendes:

Na wenn das mal nicht einfach war.

Aktuell sind unsere Werte hardkodiert, das ist natürlich eher uninteressant. Was wir möchten ist, jedesmal, wenn auf dem Server eine Bestellung eingeht, soll ein Client notifiziert werden, so dass der Chart aktualisiert werden kann – LiveCharting also!

Client Notifications mit WebSockets

Endlich wird es Zeit, WebSockets zu aktivieren. Das geht ganz einfach. Der Ort, auf dem wir an Server-Events interessiert sind ist die OrderReportPage.

Um WebSockets jetzt endlich verwenden zu können, müssen wir dafür sorgen, dass die Bibliothek aus den Wicket-Extensions mitgeladen wird (kommt natürlich nicht mit dem Core mit). Hierfür deklarieren wir folgende Abhängigkeit in der Projekt-Pom.

[code language=”xml”]

<dependency>
<groupId>org.apache.wicket</groupId>
<artifactId>wicket-native-websocket-jetty</artifactId>
<version>0.3</version>
</dependency>

[/code]

Es gibt verschiedene Implementierungen für WebSockets (verschiedene Jetty-Versionen und Tomcat). Wir verwenden hier die Jetty-Version in der Version 0.3 (derzeit die aktuellste). Das allein reicht aber leider noch nicht.

Zusätzlich dürfen wir nicht den Standard-Wicket-Filter verwenden, sondern eine spezielle Implementierung für WebSockets. Also auf in die web.xml und folgende Filter-Deklaration verwenden.

[code language=”xml”]

<filter>
<filter-name>wicket.wicket-6-websockets</filter-name>
<filter-class>org.apache.wicket.protocol.http.Jetty7WebSocketFilter</filter-class>
<init-param>
<param-name>applicationClassName</param-name>
<param-value>de.effectivetrainings.WicketApplication</param-value>
</init-param>
</filter>

[/code]

Das Ganze funktioniert natürlich nur, wenn wir auch wirklich mit dem Jetty7 arbeiten. Startet man jetzt, bekommt man die vielsagende Fehlermeldung:

java.lang.IllegalStateException: Websockets not supported on blocking connectors

Warum nur wird es Einem hier so schwer gemacht?

Was fehlt ist eine Anpassung in der Start-Klasse.
Das Problem ist diese Zeile in der Jetty-Konfiguration.

[code language=”java”]

SocketConnector connector = new SocketConnector();

[/code]

Für WebSockets machen aber Blocked-Sockets keinen Sinn, also nehmen wir einfach einen Nicht-Blockierenden?

[code language=”java”]

SelectChannelConnector connector = new SelectChannelConnector();

[/code]

Um WebSockets auf der OrderReportPage zu aktivieren verwenden wir das WebSocketBehavior mit den zugehörigen Methoden onConnect, onClose, onMessage für Textnachrichten und onMessage für Binaries.

[code language=”java”]

add(new WebSocketBehavior(){
@Override
protected void onConnect(ConnectedMessage message) {
super.onConnect(message);
}

@Override
protected void onClose(ClosedMessage message) {
super.onClose(message);
}

@Override
protected void onMessage(WebSocketRequestHandler handler, TextMessage message) {
super.onMessage(handler, message);
handler.push("Hallo Client!!");
}

@Override
protected void onMessage(WebSocketRequestHandler handler, BinaryMessage binaryMessage) {
super.onMessage(handler, binaryMessage);
}
});

[/code]

Sobald sich ein Client per WebSocket verbindet liefert er unserer Applikation bestimmte Daten, die wir benötigen, um eine Nachricht an den Client zu schicken, hierfür ist die onConnect-Methode im Behavior zuständig.

Was der Client uns liefert ist

  • applicationName
  • sessionId
  • pageId

Diese Informationen bekommen wir über die ConnectedMessage, die als Parameter übergeben wird.

Warum brauchen wir genau diese Parameter? Die Applikation identifiziert die Applikation, weiter nicht interessant. Die SessionId identifiziert den Client eindeutig, das brauchen wir, da ja beliebig viele Clients verbunden sein können. Die PageId ist interessant, hierzu muss man wissen, was intern passiert.

Kommt ein WebSocketRequest vom Client beim Server an, wird (analag eines AjaxRequests) die Seite aus dem DiskPageStore anhand der übergebenen PageId geladen und einmal komplett durch den Komponentenbaum dieser Seite iteriert (mittels dem Standard Event Mechanismus) und ein
WebSocketConnectedPayload, WebSocketClosedPayload oder ein WebSocketPayload übergeben. Theoretisch kann also jede Komponente einzeln auf WebSocketEvents reagieren, indem die onEvent(..) Methode überschrieben wid und auf das ComponentEvent reagiert wird.
Um jetzt aber tatsächlich effektiv mit WebSockets arbeiten zu können müssen wir ein bisschen tricksen.

Eine Connection wird eindeutig über Applicationname, SessionId und PageId identifizeirt. Wir brauchen eine Art Registry um die Connections zu verwalten.

Hier ein sehr einfacher und simpler Ansatz, der für unsere Zwecke aber völlig ausreichend ist.
Wir nehmen einfach an, die Session-ID identifiziert eindeutig unsere Connections, das funktioniert wunderbar, so lange wir nur eine Seite in der Anwendung haben, die Server-Nachrichten empfängt, bei mehreren Seiten müssen wir uns was besseres ausdenken.

[code language=”java”]

public class ConnectionRegistry {

private Map<String, List<ClientConnection>> connections;

public ConnectionRegistry(){
this.connections = Collections.synchronizedMap(new HashMap<String, List<ClientConnection>>());
}

public void clientConnects(String applicationName, String sessionId, Integer pageId){
List<ClientConnection> clientConnections = this.connections.get(sessionId);
if(clientConnections == null){
connections.put(sessionId, new ArrayList<ClientConnection>());
}
this.connections.get(sessionId).add(new ClientConnection(applicationName,sessionId,pageId));
}

public void clientDisconnects(String applicationName, String sessionId, Integer pageId){
List<ClientConnection> connections = this.connections.get(sessionId);
if(connections != null){
connections.remove(new ClientConnection(applicationName,sessionId,pageId));
}
}

public List<ClientConnection> getConnectionsBySessionId(String sessionId){
List<ClientConnection> connections =  this.connections.get(sessionId);
return connections != null ? connections : new ArrayList<ClientConnection>();
}

}

public class ClientConnection implements Serializable {

private String applicationName;
private String sessionId;
private Integer pageId;

public ClientConnection(String applicationName, String sessionId, Integer pageId) {
this.applicationName = applicationName;
this.sessionId = sessionId;
this.pageId = pageId;
}

public String getApplicationName() {
return applicationName;
}

public String getSessionId() {
return sessionId;
}

public Integer getPageId() {
return pageId;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;

ClientConnection that = (ClientConnection) o;

if (applicationName != null ? !applicationName.equals(that.applicationName) : that.applicationName != null)
return false;
if (pageId != null ? !pageId.equals(that.pageId) : that.pageId != null) return false;
if (sessionId != null ? !sessionId.equals(that.sessionId) : that.sessionId != null) return false;

return true;
}

@Override
public int hashCode() {
int result = applicationName != null ? applicationName.hashCode() : 0;
result = 31 * result + (sessionId != null ? sessionId.hashCode() : 0);
result = 31 * result + (pageId != null ? pageId.hashCode() : 0);
return result;
}
}

[/code]

Die Registry stellen wir über die Applikation bereit.

[code language=”java”]

public class WicketApplication extends WebApplication
{
private ConnectionRegistry registry;

/**
* @see org.apache.wicket.Application#getHomePage()
*/
@Override
public Class<? extends WebPage> getHomePage()
{
return HomePage.class;
}

/**
* @see org.apache.wicket.Application#init()
*/
@Override
public void init()
{
super.init();
registry = new ConnectionRegistry();
mountPage("/report", OrderReportPage.class);
}

public ConnectionRegistry getRegistry(){
return registry;
}

public static WicketApplication get(){
return (WicketApplication) Application.get();
}
}

[/code]

Sobald ein Client eine WebSocket-Verbindung öffnet oder schliesst, reagieren wir im WebSocketBehavior.

[code language=”java”]

@Override
protected void onConnect(ConnectedMessage message) {
super.onConnect(message);
WicketApplication.get().getRegistry().
clientConnects(message.getApplication().getName(),
message.getSessionId(), message.getPageId());
}

@Override
protected void onClose(ClosedMessage message) {
super.onClose(message);
WicketApplication.get().getRegistry().
clientDisconnects(message.getApplication().getName(),
message.getSessionId(), message.getPageId());
}

[/code]

Das Schöne ist, wir bekommen jetzt überall Zugriff auf die offenen Connections per Client.

[code language=”java”]

WicketApplication.get().getRegistry().getConnectionsBySessionId(Session.get().getId())

[/code]

Jetzt wird es Zeit, die WebSockets einzusetzen, hierzu spendieren wir unserem Formular noch einen AjaxSubmitButton und folgenden Code in der onSubmit() – Methode.

[code language=”java”]

orderForm.add(new AjaxSubmitLink("submit") {
@Override
protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
SimpleWebSocketConnectionRegistry registry = new SimpleWebSocketConnectionRegistry() ;
for(ClientConnection clientConnection :
WicketApplication.get().getRegistry().getConnectionsBySessionId(Session.get().getId())) {
IWebSocketConnection connection = registry.getConnection(Application.get(), clientConnection.getSessionId(), clientConnection.getPageId());
if (connection != null) {
WebSocketRequestHandler webSocketHandler = new WebSocketRequestHandler(this, connection);
webSocketHandler.push("My WebSocket message");
}
}

}
});

[/code]

Über die SimpleWebSocketConnectionRegistry bekommt man Zugriff auf die derzeit offenen Connections. Wir erzeugen einen WebSocketRequestHandler, und dieser bietet funktioniert ziemlich analog dem AjaxRequestTarget. Ich habe also Methode wie appendJavaScript, prependJavaScript, add etc.

Über die Methode push(…) können wir eine einfache TextMessage an den Client schicken. Dieser muss nur noch auf Messages reagieren.

ClientSide Code

Folgnder Code verwendet die definieren Wicket-Callbacks für den Client-Code.

[code language=”javascript”]

$(document).ready(function() {

Wicket.Event.subscribe("/websocket/open", function(jqEvent) {
alert("connection opened");
});

Wicket.Event.subscribe("/websocket/message", function(jqEvent, message) {
alert("message received " + message);
});

});

[/code]

Mit Wicket.Event.subscribe könnten wir uns für die WebSocket-Messages registrieren. Messages werden standardmässig über den Kanal “”/websocket/message” verschickt. Diesen Client rendern wir mit der OrderReportPage, die ja standardmässig auf die Nachrichten von der Bestellseite reagieren soll.

Hierfür definieren wir uns eine neue Resource.

[code language=”java”]

public class WebSocketClientResourceReference extends PackageResourceReference {
public WebSocketClientResourceReference() {
super(HomePage.class, "orderreport-client.js");
}

@Override
public Iterable<? extends HeaderItem> getDependencies() {
List<HeaderItem> headerItems = new ArrayList<HeaderItem>();
headerItems.add(JavaScriptHeaderItem.forReference(WicketWebSocketJQueryResourceReference.get()));
return headerItems;
}
}

[/code]

Diese Referenz rendern wir einfach mit der OrderReportPage.

[code language=”java”]

@Override
public void renderHead(HtmlHeaderContainer container) {
super.renderHead(container);
container.getHeaderResponse().render(new JQueryVisualizePlugin());
container.getHeaderResponse().render(new OnDomReadyHeaderItem("$(‘table’).visualize();"));
container.getHeaderResponse().render(JavaScriptHeaderItem.forReference(new WebSocketClientResourceReference()));
}

[/code]

Senden wir die Form jetzt testweise ab, öffnet sich automatisch die OrderReportPage im Browser und wir sehen folgendes:

Schon mal gar nicht schlecht. Damit sind wir fast am Ziel. Zuletzt müssen wir die OrderReportPage noch ein wenig tunen. Bisher ist unsere Tabelle mit der Bestellübersicht nur hardkodiert.

Wir haben jetzt zwei Möglichkeit, entweder wir machen aus der Tabelle eine WicketKomponente und fügen diese nach jedem Request mit der add(..)-Methode hinzu, damit diese neu gezeichnet wird, oder wir bauen das ganze manuell mit JQuery.

Problem beim ersten Ansatz ist, dass wir über den WebSocketRequestHandler zwar die PageInstanz bekommen, aber nicht direkt Zugriff auf die Wicket-Komponenten auf der Seite. Man müsste also über Getter- oder ähnliches die interne Struktur der Seite nach aussen exposen. Nicht schön, bauen wir das Ganze also einfacher mit JQuery.

In der onSubmit-Methode schicken wir keine einfache Textnachricht, sondern wir verschicken einfach JSON.

[code language=”java”]

WebSocketRequestHandler webSocketHandler = new WebSocketRequestHandler(this, connection);
try {
webSocketHandler.push(JsonUtils.asArray(DB.get().countOrdersByFood()).toString());
} catch (JSONException e) {
e.printStackTrace();
}

[/code]

Hierfür definieren wir folgende Html Struktur in der OrderReport Page.

[code language=”html”]

<table style="visibility:hidden">
<caption>Essensbestellungen nach Art</caption>
<thead>
<tr>
<th scope="col">Pizza</th>
<th scope="col">Leberkas</th>
<th scope="col">Burger</th>
<th scope="col">Salat</th>
<th scope="col">Spiegelei</th>
</tr>
</thead>
<tbody>
<tr>
<td id="PIZZA">0</td>
<td id="LEBERKAS">0</td>
<td id="BURGER">0</td>
<td id="SALAT">0</td>
<td id="SPIEGELEI">0</td>
</tr>

</tbody>
</table>

[/code]

und folgendes Skript im JavaScript-Client.

[code language=”javascript”]

Wicket.Event.subscribe("/websocket/message", function(jqEvent, message) {
var json = JSON.parse(message);
for(i in json){
$(‘#’+json[i].name).html(json[i].value);
}
$(‘.visualize’).trigger(‘visualizeRefresh’);
})

[/code]

Und ab jetzt triggert jede Bestellung ein Refresh der Ansicht auf allen aktuell geöffneten OrderReport Pages.

Fazit

Insgesamt muss ich sagen, es ist schön, dass WebSockets mittlerweile funktionieren, aber die Integration scheint mir noch ein wenig holprig. Gerade wenn es um die Integration mit verschiedenen Seiten geht (meiner Ansicht nach DER Use-Case ist das ganze ein wenig kompliziert).

Oder habe ich etwas übersehen? Geht es evtl einfacher als in diesem Artikel vorgestellt? Über Hinweise wäre ich dankbar, ansonsten wünsche ich viel Spaß mit Wicket und WebSockets.

Der Source-Code für dieses Beispiel befindet sich hier im Unterordner /wicket-6-websockets.

War dieser Blogeintrag für Sie interessant? Evtl. kann ich noch mehr für Sie tun.

Trainings & Know-How aus der Praxis zu

  • Apache Wicket 1.4.x, 1.5.x, 1.6.x
  • GIT – Best Practices, Einsatz, Methoden
  • Spring
  • Java
  • Scrum & Kanban
  • Agiles Arbeiten
Consulting & Softwareentwicklung

  • Wicket Professional
  • Requirements Engineering
  • Qualitätssicherung
  • Software-Entwicklung
  • Architektur
  • Scrum & Kanban

Links

Die Ankündigung von WebSockets

WIKI

Demo Applikation von Martin Grigorov mit Scala und Actors

Effective Trainings & Consulting - Martin Dilger



Hat Ihnen dieser Blog-Eintrag gefallen? Ich stelle in diesem Blog Informationen über Tools, Frameworks und Werkzeuge zur Verfügung, die mich produktiver machen. Vielleicht kann ich auch Ihnen helfen, produktiver zu werden.


Ich unterstütze Sie als freier Mitarbeiter bei der Entwicklung von Software-Projekten, Agiler Arbeit sowie Schulungen / Fortbildungen.


Jeden Tag ein bisschen produktiver - ab heute