<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>rattlab.net &#187; Coding</title>
	<atom:link href="http://rattlab.net/topics/coding/feed/" rel="self" type="application/rss+xml" />
	<link>http://rattlab.net</link>
	<description>Web, Programmierung und anderes Zeuch</description>
	<lastBuildDate>Fri, 12 Aug 2011 17:41:03 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.0.4</generator>
		<item>
		<title>Persistenz für den Feedreader</title>
		<link>http://rattlab.net/2008/10/persistenz-fur-den-feedreader/</link>
		<comments>http://rattlab.net/2008/10/persistenz-fur-den-feedreader/#comments</comments>
		<pubDate>Tue, 28 Oct 2008 20:01:33 +0000</pubDate>
		<dc:creator>Peter</dc:creator>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Hibernate]]></category>
		<category><![CDATA[JPA]]></category>
		<category><![CDATA[Wicket]]></category>

		<guid isPermaLink="false">http://www.rattlab.net/?p=38</guid>
		<description><![CDATA[Heute möchte ich mich mit Persistenz beschäftigen. Als Ausgangspunkt dient mir der Feedreader, der im letzten Teil etwas dynamischer wurde. Ich werde JPA mit Hibernate einsetzen, um die Daten, die aus den Newsfeeds ausgelesen werden, in einer Datenbank zu speichern und von dort auch wieder auszulesen. Außerdem werden wir auch hier wieder mit Wicket in Berührung kommen, denn der Code kann und sollte etwas verbessert werden, wenn wir unsere Daten aus einer Datenbank holen.]]></description>
			<content:encoded><![CDATA[<p>Heute möchte ich mich mit Persistenz beschäftigen. Als Ausgangspunkt dient mir der Feedreader, der im <a href="http://www.rattlab.net/2008/09/dynamik-fur-den-feedreader/">letzten Teil</a> etwas dynamischer wurde. Ich werde <a href="http://java.sun.com/developer/technicalArticles/J2EE/jpa/">JPA</a> mit <a href="http://www.hibernate.org/">Hibernate</a> einsetzen, um die Daten, die aus den Newsfeeds ausgelesen werden, in einer Datenbank zu speichern und von dort auch wieder auszulesen. Außerdem werden wir auch hier wieder mit Wicket in Berührung kommen, denn der Code kann und sollte etwas verbessert werden, wenn wir unsere Daten aus einer Datenbank holen.<span id="more-38"></span></p>
<h3>Vorarbeit</h3>
<p>Bevor wir loslegen, müssen wir erstmal die benötigten Bibliotheken herunterladen. Das machen wir natürlich nicht per Hand, sondern wie bisher auch mit Maven. In unserer <code>pom.xml</code> ergänzen wir folgende Abhängigkeiten:</p>
<pre>		&lt;dependency&gt;
			&lt;groupId&gt;org.hibernate&lt;/groupId&gt;
			&lt;artifactId&gt;hibernate-annotations&lt;/artifactId&gt;
			&lt;version&gt;3.3.1.GA&lt;/version&gt;
		&lt;/dependency&gt;
		&lt;dependency&gt;
			&lt;groupId&gt;org.hibernate&lt;/groupId&gt;
			&lt;artifactId&gt;hibernate-commons-annotations&lt;/artifactId&gt;
			&lt;version&gt;3.3.0.ga&lt;/version&gt;
		&lt;/dependency&gt;
		&lt;dependency&gt;
			&lt;groupId&gt;org.hibernate&lt;/groupId&gt;
			&lt;artifactId&gt;hibernate-entitymanager&lt;/artifactId&gt;
			&lt;version&gt;3.3.2.GA&lt;/version&gt;
		&lt;/dependency&gt;
		&lt;dependency&gt;
			&lt;groupId&gt;org.hibernate&lt;/groupId&gt;
			&lt;artifactId&gt;hibernate&lt;/artifactId&gt;
			&lt;version&gt;3.2.6.ga&lt;/version&gt;
			&lt;exclusions&gt;
				&lt;exclusion&gt;
					&lt;groupId&gt;javax.transaction&lt;/groupId&gt;
					&lt;artifactId&gt;jta&lt;/artifactId&gt;
				&lt;/exclusion&gt;
			&lt;/exclusions&gt;
		&lt;/dependency&gt;
		&lt;dependency&gt;
			&lt;groupId&gt;org.apache.geronimo.specs&lt;/groupId&gt;
			&lt;artifactId&gt;geronimo-jta_1.1_spec&lt;/artifactId&gt;
			&lt;version&gt;1.1&lt;/version&gt;
		&lt;/dependency&gt;
		&lt;dependency&gt;
			&lt;groupId&gt;hsqldb&lt;/groupId&gt;
			&lt;artifactId&gt;hsqldb&lt;/artifactId&gt;
			&lt;version&gt;1.8.0.7&lt;/version&gt;
		&lt;/dependency&gt;</pre>
<p>Die ersten beiden Abhängigkeiten brauchen wir für die (JPA-)Annotations, die wir verwenden wollen. Mit dem Entity Manager hatte ich meine Probleme, da ich erst nicht wusste, dass ich ihn brauch und dann gab es auch noch ein Problem mit der Version der Annotation-Library, denn mit der 3.3.0 will der Manager mit Version 3.3.2 offenbar nicht zusammenarbeiten. Dann folgt Hibernate selbst, aber eine der Abhängigkeiten von Hibernate klammern wir aus, da sie von Sun nicht freigegeben ist und Maven uns nur den Hinweis bringen würden, dass wir sie manuell runterladen müssten. Dazu sind wir aber zu faul (also zumindest ich bin es) und deswegen holen wir das Ding aus dem <a href="http://geronimo.apache.org/">Geronimo-Projekt</a>. In diesem stehen viele Enterprise APIs unter der <a href="http://de.wikipedia.org/wiki/Apache-Lizenz">Apache License</a> zur Verfügung, was uns etwas die Arbeit erleichtert. Schließlich besorgen wir uns noch die HSQLDB.</p>
<pre class="shell">$ mvn install
[...]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 4 minutes 54 seconds
[INFO] Finished at: Fri Oct 03 12:04:18 CEST 2008
[INFO] Final Memory: 10M/18M
[INFO] ------------------------------------------------------------------------
$ mvn eclipse:eclipse
[...]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1 minute 42 seconds
[INFO] Finished at: Fri Oct 03 12:30:47 CEST 2008
[INFO] Final Memory: 6M/12M
[INFO] ------------------------------------------------------------------------</pre>
<p>Nun wollen wir erstmal die Datenbank zum Laufen bekommen. <a href="http://hsqldb.org/">HSQLDB</a> ist eine kleine leichte Datenbank, die komplett in Java geschrieben wurde und die Daten (zumeist) komplett im Speicher behält. Für geringe Datenmengen oder Tests eignet sie sich damit sehr gut. Sie kann einerseits wie ein Datenbank-Server betrieben werden, der an einem Port lauscht, oder direkt in dem Programm angesprochen werden, von dem die Datenbank verwendet werden soll.</p>
<p>In meinem Projekt-Verzeichnis erstelle ich einen Unterordner für die HSQLDB und kopiere anschließend das Jar dort hinein. Der Pfad zum lokalen Maven-Repository wird bei euch sicher anders sein. Dann starte ich erstmals den Server:</p>
<pre class="shell">$ cd /cygdrive/c/myprojects
$ mkdir hsqldb
$ cp /cygdrive/c/Users/ex-ratt/.m2/repository/hsqldb/hsqldb/1.8.0.7/hsqldb-1.8.0.7.jar hsqldb
$ cd hsqldb
$ java -cp hsqldb-1.8.0.7.jar org.hsqldb.Server
...</pre>
<p>Nun läuft der Datenbank-Server. Da wir keine weiteren Parameter übergeben haben, wurde die Datenbank &#8220;test&#8221; genannt, zu sehen an den drei Dateien, die im <code>hsqldb</code>-Verzeichnis erstellt wurden (<code>test.lck</code>, <code>test.log</code> und <code>test.properties</code>). Im Log finden wir eine SQL-Anweisung, die einen User mit Namen &#8220;sa&#8221; und ohne Passwort anlegt und ihm Administrator-Rechte gibt.</p>
<h3>Konfiguration</h3>
<p>Damit Hibernate weiß, wie es auf die Datenbank zugreifen soll, müssen wir noch eine Konfiguration erstellen. Wir halten uns dabei an den JPA-Standard. Eine Datei namens <code>persistence.xml</code> beinhaltet diese Konfiguration und muss im <code>META-INF</code>-Verzeichnis liegen, das wiederum im Classpath zu finden sein muss. Damit hatte ich meine lieben Probleme. Damit das Starten aus Eclipse heraus funktioniert, legt man im Verzeichnis <code>WEB-INF</code> ein Verzeichnis <code>classes</code> an und da hinein kommt <code>META-INF</code> mitsamt der <code>persistence.xml</code>. In meinem Fall sähe der komplette Pfad so aus: <code>C:/myprojects/wicket/src/main/webapp/WEB-INF/classes/META-INF/persistence.xml</code>. Will man Jetty über Maven starten, so kann man <code>META-INF</code> unter <code>target/classes</code> ablegen.</p>
<pre><code>&lt;persistence xmlns="http://java.sun.com/xml/ns/persistence"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
	version="1.0"&gt;
	&lt;persistence-unit name="feed"&gt;
		&lt;provider&gt;org.hibernate.ejb.HibernatePersistence&lt;/provider&gt;
		&lt;class&gt;net.rattlab.model.Game&lt;/class&gt;
		&lt;class&gt;net.rattlab.model.News&lt;/class&gt;
		&lt;properties&gt;
			&lt;property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" /&gt;
			&lt;property name="hibernate.archive.autodetection" value="class" /&gt;
			&lt;property name="hibernate.hbm2ddl.auto" value="create-drop" /&gt;

			&lt;property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver" /&gt;
			&lt;property name="hibernate.connection.url" value="jdbc:hsqldb:hsql://localhost" /&gt;
			&lt;property name="hibernate.connection.username" value="sa" /&gt;
			&lt;property name="hibernate.connection.passwords" value="" /&gt;

			&lt;property name="hibernate.cache.provider_class" value="org.hibernate.cache.EhCacheProvider" /&gt;
		&lt;/properties&gt;
	&lt;/persistence-unit&gt;
&lt;/persistence&gt;</code></pre>
<p>Die erste wichtige Information ist der Provider. Normalerweise findet man JPA im Application-Server-Umfeld und dieser stellt eine Implementierung bereit. Wir aber haben keinen solchen Server, sondern nutzen direkt Hibernate und müssen das angeben. Dann folgen die nötigen Parameter wie z.B. Nutzername, Passwort und URL. Eigentlich sollte man die persistenten Klassen nicht extra angeben müssen, aber das funktionierte bei mir leider nicht wie gewünscht.</p>
<p>Die Klassen selber werden mit Annotationen versehen, die beschreiben, wie die Variablen der Objekte in der Datenbank abgebildet werden sollen. Ursprünglich hat man das bei Hibernate mit XML-Dateien gemacht, JPA bietet nun aber die Annotationen an (die Hibernate aber auch selber anbietet bzw. erweitert). Werfen wir also einen Blick auf unsere beiden persistenten Klassen:</p>
<pre>package net.rattlab.model;

import java.io.Serializable;
import java.util.Date;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

@Entity
@NamedQueries({
	@NamedQuery(name = "findAllNews", query = "from News order by date desc"),
	@NamedQuery(name = "countNews", query = "select count(*) from News")
})
public class News implements Serializable {

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private long id;

	@ManyToOne(cascade = CascadeType.ALL)
	private Game game;

	private String page;

	private String url;

	private String title;

	@Lob
	private String description;

	@Temporal(value = TemporalType.TIMESTAMP)
	private Date date;

	public News() {}

	public News(Game game, String page, String url, String title, String description, Date date) {
		this.game = game;
		this.page = page;
		this.url = url;
		this.title = title;
		this.description = description;
		this.date = date;
	}

	public long getId() {
		return id;
	}

	public Game getGame() {
		return game;
	}

	public String getPage() {
		return page;
	}

	public String getUrl() {
		return url;
	}

	public String getTitle() {
		return title;
	}

	public String getDescription() {
		return description;
	}

	public Date getDate() {
		return date;
	}

	public void setId(long id) {
		this.id = id;
	}

	public void setGame(Game game) {
		this.game = game;
	}

	public void setPage(String page) {
		this.page = page;
	}

	public void setUrl(String url) {
		this.url = url;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public void setDescription(String description) {
		this.description = description;
	}

	public void setDate(Date date) {
		this.date = date;
	}
}</pre>
<p>Mit <code>@Entity</code> wird die Klasse markiert, damit ihre Objekte persistent gemacht werden können. Standardmäßig geht die JPA-Implementierung davon aus, dass jedes Attribut in die Datenbank abgebildet werden soll. Da außerdem je nach Java-Datentyp bestimmte Datenbank-Datentypen automatisch zugeordnet werden, können wir uns an vielen Variablen die Annotationen sparen. Damit ein Datenbankeintrag eindeutig referenziert werden kann, braucht jede Zeile eine ID &#8211; diese müssen wir mit in unsere Klasse einbauen und mit <code>@Id</code> markieren, <code>@GeneratedValue(strategy = GenerationType.AUTO)</code> zeigt dabei an, dass der Wert generiert werden soll. Zu <code>Game</code> gibt es eine Beziehung, so kann jedes Spiel mehrere News haben, aber jede News ist genau einem Spiel zugeordnet. Eine Fremdschlüsselspalte wird automatisch angelegt.</p>
<pre>package net.rattlab.model;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.NamedQuery;

import org.hibernate.annotations.CollectionOfElements;
import org.hibernate.annotations.MapKey;

@Entity
@NamedQuery(name = "findAllGames", query = "from Game")
public class Game implements Serializable {

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private long id;

	private String name;

	@CollectionOfElements
	@JoinTable(name = "feed", joinColumns = @JoinColumn(name = "game"))
	@Column(name = "url")
	@MapKey(columns = @Column(name = "site"))
	private Map&lt;String, String&gt; feeds;

	public Game() {}

	public Game(String name) {
		setName(name);
		feeds = new HashMap&lt;String, String&gt;();
	}

	public void addFeed(String page, String url) {
		feeds.put(page, url);
	}

	public Map&lt;String, String&gt; getFeeds() {
		return feeds;
	}

	public long getId() {
		return id;
	}

	public String getName() {
		return name;
	}

	public void setId(long id) {
		this.id = id;
	}

	public void setName(String name) {
		this.name = name;
	}
}</pre>
<p>Hier benötigt die <code>Map</code>, die die Feed-URLs mit ihren Namen speichert, besondere Aufmerksamkeit. Mit JPA allein ist es nicht möglich, primitive Datentypen oder Strings in einer <code>Collection</code> oder <code>Map</code> zu halten, weshalb wir hier direkt auf Hibernate-Annotations zurückgreifen müssen. Für die <code>Map</code> muss eine eigene Tabelle angelegt werden, deren Spaltennamen wir vorgeben, sonst würden diese <code>KEY</code> und <code>VALUE</code> heißen.</p>
<p>Da wir für Tabellen- und Spaltennamen kaum Vorgaben gemacht haben, werden sie einfach aus den Namen der Attribute abgeleitet. Die Tabellen heißen also <code>Game</code> und <code>News</code>, die Spalten <code>name</code>, <code>page</code> usw. Bei Galileo Computing findet ihr eine gute <a href="http://www.galileocomputing.de/artikel/gp/artikelID-328">Übersicht über alle Entity-Annotationen</a>.</p>
<h3>Lesen und Schreiben auf der Datenbank</h3>
<p>Jetzt ersetzen wir erstmal ganz plump die beiden Listen in der <code>WicketApplication</code> und fügen Datenbankzugriffe hinzu. Hier werden <code>NamedQuery</code>s erstellt, die wir vorher mit <code>@NamedQuery</code> in den Entities angegeben haben. Auf die manuelle Sortierung der News können wir jetzt verzichten, da diese beim Datenbankzugriff schon sortiert ausgelesen werden (siehe entsprechenden benannten Query).</p>
<pre>package net.rattlab.wicket;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Map;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

import net.rattlab.model.Game;
import net.rattlab.model.News;

import org.apache.wicket.Application;
import org.apache.wicket.protocol.http.WebApplication;

import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.io.FeedException;
import com.sun.syndication.io.SyndFeedInput;
import com.sun.syndication.io.XmlReader;

<span class="javadoc comment">/**
 * Application object for your web application. If you want to run this application without deploying, run the Start class.
 *
 * @see net.rattlab.wicket.Start#main(String[])
 */</span>
public class WicketApplication extends WebApplication {

	private EntityManagerFactory entityManagerFactory;

	<span class="javadoc comment">/**
	 * Constructor
	 */</span>
	public WicketApplication() {
		entityManagerFactory = Persistence.createEntityManagerFactory("feed");

		EntityManager entityManager = entityManagerFactory.createEntityManager();
		createGames(entityManager);
		createNews(entityManager);
		entityManager.close();
	}

	<span class="javadoc comment">/**
	 * Creates the games and news feeds.
	 */</span>
	protected void createGames(EntityManager entityManager) {
		EntityTransaction transaction = entityManager.getTransaction();
		transaction.begin();

		Game starcraft2 = new Game("StarCraft 2");
		Game diablo3 = new Game("Diablo 3");

		starcraft2.addFeed("inStarCraft", "http://starcraft2.ingame.de/feed.php?type=RSS1.0&#038;section=345");
		starcraft2.addFeed("StarCraft 2 Mecca", "http://forum.gamersunity.de/external.php?type=rss2&#038;forumids=195");
		diablo3.addFeed("inDiablo", "http://diablo3.ingame.de/feed.php?type=RSS1.0");
		diablo3.addFeed("Diablo 3 Source", "http://diablo3.4players.de/rss_feed.xml");

		entityManager.persist(starcraft2);
		entityManager.persist(diablo3);

		transaction.commit();
	}

	<span class="javadoc comment">/**
	 * Creates the news from the news feeds.
	 */</span>
	protected void createNews(EntityManager entityManager) {
		EntityTransaction transaction = entityManager.getTransaction();
		transaction.begin();
		SyndFeedInput input = new SyndFeedInput();
		for (Game game : getGames(entityManager)) {
			Map&lt;String, String&gt; feeds = game.getFeeds();
			for (String page : feeds.keySet()) {
				String feedUrl = feeds.get(page);
				try {
					URL url = new URL(feedUrl);
					SyndFeed feed = input.build(new XmlReader(url));
					for (Object o : feed.getEntries()) {
						<span class="comment">// rome 0.9 does not support generics</span>
						SyndEntry entry = (SyndEntry) o;
						entityManager.persist(new News(game, page, entry.getLink(), entry.getTitle(),
								entry.getDescription().getValue(), entry.getPublishedDate()));
					}
				}
				catch (MalformedURLException exc) {}
				catch (IllegalArgumentException e) {}
				catch (FeedException e) {}
				catch (IOException e) {}
			}
		}
		transaction.commit();
	}

	@Override
	protected void init() {
		super.init();
		getMarkupSettings().setStripWicketTags(true);
	}

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

	<span class="javadoc comment">/**
	 * @see org.apache.wicket.Application#getHomePage()
	 */</span>
	public Class&lt;HomePage&gt; getHomePage() {
		return HomePage.class;
	}

	@SuppressWarnings("unchecked")
	public List&lt;Game&gt; getGames() {
		EntityManager entityManager = entityManagerFactory.createEntityManager();
		List&lt;Game&gt; games = entityManager.createNamedQuery("findAllGames").getResultList();
		entityManager.close();
		return games;
	}

	@SuppressWarnings("unchecked")
	public List&lt;Game&gt; getGames(EntityManager entityManager) {
		return entityManager.createNamedQuery("findAllGames").getResultList();
	}

	@SuppressWarnings("unchecked")
	public List&lt;News&gt; getNews() {
		EntityManager entityManager = entityManagerFactory.createEntityManager();
		List&lt;News&gt; news = entityManager.createNamedQuery("findAllNews").getResultList();
		entityManager.close();
		return news;
	}
}</pre>
<p>Das ist jetzt zugegebenermaßen nicht die schönste Lösung, aber so können wir erstmal testen, ob der Kram überhaupt funktioniert. Die nötigen Tabellen in der Datenbank werden übrigens automatisch erzeugt. In der <code>persistence.xml</code> gibt es eine Zeile <code>&lt;property name="hibernate.hbm2ddl.auto" value="create-drop" /&gt;</code>, die beim Starten unserer Anwendung aus den bestehenden Annotationen ein Datenbankschema erstellt und beim Beenden alles wieder löscht. Ideal ist sowas für automatisierte Tests, da so immer wieder auf einer frischen Datenbank gearbeitet wird, womit die Ergebnisse reproduzierbar bleiben.</p>
<p>Das scheinbar willkürliche Erstellen von <code>EntityManager</code> und Starten von <code>EntityTransaction</code> wollen wir nun, nachdem wir wissen, dass unsere Applikation funktioniert, etwas schicker gestalten. Optimal wäre es, wenn wir uns nicht darum kümmern müssten, wann ein neuer Manager benötigt oder eine neue Transaktion gestartet werden muss. Die Hibernate-Dokumentation beschreibt ein <a href="http://www.hibernate.org/43.html">Open Session in View</a> Pattern, bei dem für jeden Request ein neuer Manager samt Transaktion gestartet wird, die am Ende des Requests wieder geschlossen werden. Das bietet sich für Webanwendungen natürlich an.</p>
<p>Allerdings können wir das Beispiel, wie es in der Dokumentation angegeben ist, nicht eins zu eins übernehmen, da wir mit JPA arbeiten und nicht direkt mit <code>Session</code> oder <code>SessionFactory</code> von Hibernate. Außerdem wollen wir auch keinen <code>ServletFilter</code> nutzen, sondern direkt in dem von uns benutzten Web-Framework ansetzen. Bei Wicket ist die ideale Stelle dafür der <code>WebRequestCycle</code>. Für jede Anfrage wird ein neues solches Objekt erzeugt und es hat Methoden, die angesprochen werden, wenn die Anfrage beginnt (<code>onBeginRequest</code>) und wenn sie aufhört (<code>onEndRequest</code>).</p>
<pre><code>package net.rattlab.wicket;

import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;

import org.apache.wicket.Page;
import org.apache.wicket.Response;
import org.apache.wicket.protocol.http.WebApplication;
import org.apache.wicket.protocol.http.WebRequest;
import org.apache.wicket.protocol.http.WebRequestCycle;

import persistence.JpaUtil;

public class JpaRequestCycle extends WebRequestCycle {

	public JpaRequestCycle(WebApplication application, WebRequest request, Response response) {
		super(application, request, response);
	}

	@Override
	protected void onBeginRequest() {
		super.onBeginRequest();
		EntityManager manager = JpaUtil.getEntityManagerFactory().createEntityManager();
		EntityTransaction transaction = manager.getTransaction();
		transaction.begin();
		JpaUtil.setCurrentEntityManager(manager);
	}

	@Override
	protected void onEndRequest() {
		super.onEndRequest();
		EntityManager manager = JpaUtil.getCurrentEntityManager();
		EntityTransaction transaction = manager.getTransaction();
		if (transaction.isActive()) {
			transaction.commit();
		}
		if (manager.isOpen()) {
			manager.close();
		}
	}

	@Override
	public Page onRuntimeException(Page page, RuntimeException e) {
		EntityManager manager = JpaUtil.getCurrentEntityManager();
		EntityTransaction transaction = manager.getTransaction();
		if (transaction.isActive()) {
			transaction.rollback();
		}
		if (manager.isOpen()) {
			manager.close();
		}
		return super.onRuntimeException(page, e);
	}
}</code></pre>
<p>Wir nutzen hierbei eine von uns geschriebene Klasse <code>JpaUtil</code>, über die wir später auf den <code>EntityManager</code> zugreifen. Dieser muss für die Dauer des Requests irgendwo gespeichert werden und dabei müssen wir noch eines beachten: Parallelität. <code>EntityManager</code> ist nicht threadsicher, darf also nur von einem Thread zur gleichen Zeit benutzt werden, was auch überhaupt der Grund ist, wieso wir das Open Session in View Pattern einsetzen wollen. Dafür nutzen wir im <code>JpaUtil</code> eine Variable, die lokal für jeden Thread engelegt wird, <code>ThreadLocal</code>. Darin speichern wir unseren Manager und nun hat jeder Thread seinen eigenen und somit auch jeder Request, denn ein Request wird von genau einem Thread ausgeführt (und jeder Thread hat zu einem Zeitpunkt maximal einen Request).</p>
<pre><code>package net.rattlab.persistence;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public class JpaUtil {

	private static final EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("feed");
	private static final ThreadLocal&lt;EntityManager&gt; entityManager = new ThreadLocal&lt;EntityManager&gt;();

	public static EntityManagerFactory getEntityManagerFactory() {
		return entityManagerFactory;
	}

	public static EntityManager getCurrentEntityManager() {
		return entityManager.get();
	}

	public static void setCurrentEntityManager(EntityManager manager) {
		entityManager.set(manager);
	}
}</code></pre>
<p>Schließlich müssen wir noch unsere <code>WicketApplication</code> dahingehend umbauen, dass unser neuer RequestCycle auch tatsächlich benutzt wird und dass wir uns den <code>EntityManager</code> vom <code>JpaUtil</code> holen. Außerdem müssen wir uns einmal außerhalb des Anfragezyklus einen Manager samt Transaktion besorgen, da das Füllen der Daten aus den Newsfeeds nicht während eines Requests passiert, sondern beim Starten des Servers.</p>
<pre><code>package net.rattlab.wicket;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Map;

import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;

import net.rattlab.model.Game;
import net.rattlab.model.News;

import org.apache.wicket.Application;
<span class="changed">import org.apache.wicket.Request;
import org.apache.wicket.RequestCycle;
import org.apache.wicket.Response;</span>
import org.apache.wicket.protocol.http.WebApplication;
<span class="changed">import org.apache.wicket.protocol.http.WebRequest;</span>

<span class="changed">import persistence.JpaUtil;</span>

import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.io.FeedException;
import com.sun.syndication.io.SyndFeedInput;
import com.sun.syndication.io.XmlReader;

<span class="javadoc comment">/**
 * Application object for your web application. If you want to run this application without deploying, run the Start class.
 *
 * @see net.rattlab.wicket.Start#main(String[])
 */</span>
public class WicketApplication extends WebApplication {

	<span class="javadoc comment">/**
	 * Constructor
	 */</span>
	public WicketApplication() {
		<span class="changed">try {
			EntityManager manager = JpaUtil.getEntityManagerFactory().createEntityManager();
			EntityTransaction transaction = manager.getTransaction();
			transaction.begin();
			JpaUtil.setCurrentEntityManager(manager);

			createGames();
			createNews();

			transaction.commit();
		} catch (RuntimeException exc) {
			JpaUtil.getCurrentEntityManager().getTransaction().rollback();
			throw exc;
		} finally {
			JpaUtil.getCurrentEntityManager().close();
		}</span>
	}

	<span class="javadoc comment">/**
	 * Creates the games and news feeds.
	 */</span>
	protected void createGames() {
		<span class="changed">EntityManager entityManager = JpaUtil.getCurrentEntityManager();</span>

		Game starcraft2 = new Game("StarCraft 2");
		Game diablo3 = new Game("Diablo 3");

		starcraft2.addFeed("inStarCraft", "http://starcraft2.ingame.de/feed.php?type=RSS1.0&#038;section=345");
		starcraft2.addFeed("StarCraft 2 Mecca", "http://forum.gamersunity.de/external.php?type=rss2&#038;forumids=195");
		diablo3.addFeed("inDiablo", "http://diablo3.ingame.de/feed.php?type=RSS1.0");
		diablo3.addFeed("Diablo 3 Source", "http://diablo3.4players.de/rss_feed.xml");

		entityManager.persist(starcraft2);
		entityManager.persist(diablo3);
	}

	<span class="javadoc comment">/**
	 * Creates the news from the news feeds.
	 */</span>
	protected void createNews() {
		<span class="changed">EntityManager entityManager = JpaUtil.getCurrentEntityManager();</span>
		SyndFeedInput input = new SyndFeedInput();
		for (Game game : <span class="changed">getGames()</span>) {
			Map&lt;String, String&gt; feeds = game.getFeeds();
			for (String page : feeds.keySet()) {
				String feedUrl = feeds.get(page);
				try {
					URL url = new URL(feedUrl);
					SyndFeed feed = input.build(new XmlReader(url));
					for (Object o : feed.getEntries()) {
						<span class="comment">// rome 0.9 does not support generics</span></code>
						SyndEntry entry = (SyndEntry) o;
						entityManager.persist(new News(game, page, entry.getLink(), entry.getTitle(),
								entry.getDescription().getValue(), entry.getPublishedDate()));
					}
				}
				catch (MalformedURLException exc) {}
				catch (IllegalArgumentException e) {}
				catch (FeedException e) {}
				catch (IOException e) {}
			}
		}
	}

	@Override
	protected void init() {
		super.init();
		getMarkupSettings().setStripWicketTags(true);
	}

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

	<span class="javadoc comment">/**
	 * @see org.apache.wicket.Application#getHomePage()
	 */</span>
	public Class&lt;HomePage&gt; getHomePage() {
		return HomePage.class;
	}

	<span class="changed">@Override
	public RequestCycle newRequestCycle(Request request, Response response) {
		return new JpaRequestCycle(this, (WebRequest) request, response);
	}</span>

	@SuppressWarnings("unchecked")
	public List&lt;Game&gt; getGames() {
		<span class="changed">EntityManager entityManager = JpaUtil.getCurrentEntityManager();
		return entityManager.createNamedQuery("findAllGames").getResultList();</span>
	}

	@SuppressWarnings("unchecked")
	public List&lt;News&gt; getNews() {
		<span class="changed">EntityManager entityManager = JpaUtil.getCurrentEntityManager();
		return entityManager.createNamedQuery("findAllNews").getResultList();</span>
	}
}</pre>
<h3>Speicher schonen</h3>
<p>Noch sind wir aber nicht an unserem Ziel angelangt. Aktuell werden nämlich alle News zusammen mit der (letzten Version der) Seite in der Session gespeichert &#8211; das hängt damit zusammen, dass Wicket, da unsere Applikation zustandsorientiert ist, den aktuellen Status speichert. Das wollen wir aber nicht, da das in diesem Fall einen recht sinnfreien Ressourcenverbrauch darstellt. Dafür kann man ein <code>LoadableDetachableModel</code> nutzen, das am Ende eines Requests sein Objekt abkoppelt und es bei der nächsten Anfrage wieder lädt, so dass die Session nicht damit belastet wird.</p>
<p>Die simple Nutzung eines <code>LoadableDetachableModel</code>s allein würde uns bei einem anderen Problem aber nicht helfen. Es würden nämlich alle News auf einmal geladen, obwohl nur eine bestimmte Anzahl pro Seite angezeigt werden &#8211; sinnvoller wäre es, wenn nur genau die News aus der Datenbank geholt würden, die auch angezeigt werden sollen. Dafür gibt es eine Komponente <code>DataView</code>, der ein <code>IDataProvider</code> statt eines Models übergeben wird. Dieser holt bestimmte Datensätze und verpackt sie in Models. Da die Daten dann bereits im Speicher vorhanden sind, bringt uns auch ein <code>LoadableDetachableModel</code> nur bedingt etwas, denn das würde das Objekt anhand der ID nochmal laden, weshalb wir uns ein eigenes Model bauen.</p>
<p>Werfen wir zuerst einen Blick auf den <code>NewsDataProvider</code>. An der Methode <code>iterator(int first, int count)</code> ist zu erkennen, dass tatsächlich nur eine bestimmte Anzahl von News angefordert werden. Sie gibt einen <code>Iterator</code> zurück, den die <code>DataView</code> nutzt, um die einzelnen News zu holen. Damit wird dann die Methode <code>model(News news)</code> aufgerufen, die jede News in ein eigenes Model steckt.</p>
<pre><code>public class NewsDataProvider implements IDataProvider&lt;News&gt; {

	@Override
	@SuppressWarnings("unchecked")
	public Iterator&lt;? extends News&gt; iterator(int first, int count) {
		EntityManager entityManager = JpaUtil.getCurrentEntityManager();
		return entityManager.createNamedQuery("findAllNews")
				.setFirstResult(first)
				.setMaxResults(count)
				.getResultList()
				.iterator();
	}

	@Override
	public IModel<news> model(News news) {
		return new NewsModel(news);
	}

	@Override
	public int size() {
		EntityManager entityManager = JpaUtil.getCurrentEntityManager();
		return ((Long) entityManager.createNamedQuery("countNews").getSingleResult()).intValue();
	}

	@Override
	public void detach() {}
}</news></code></pre>
<p>Das <code>NewsModel</code> ist nicht von <code>LoadableDetachableModel</code> abgeleitet, da wir keinen schreibenden Zugriff auf das zu speichernde Objekt hätten, sondern dieses immer über <code>load()</code> geladen werden müsste. Dennoch ist die Model-Klasse recht einfach und übersichtlich. Es speichert die ID und die News selbst, beim Speichern in der Session wird die News aber nicht serialisiert. Werden die Daten wieder aus der Session geholt (z.B. durch ein Neu Laden der Seite), so wird die News anhand der ID erneut aus der Datenbank geladen. Eine allgemein gehaltenere Version eines solchen Models beschreibt Igor Vaynberg, einer der Wicket-Mitentwickler, im Blogeintrag <a href="http://wicketinaction.com/2008/09/building-a-smart-entitymodel/">&#8220;Building a smart EntityModel&#8221;</a>.</p>
<pre><code>public class NewsModel implements IModel&lt;News&gt; {

	private Long id;
	private transient News news;

	public NewsModel(News news) {
		this.id = news.getId();
		this.news = news;
	}

	@Override
	public News getObject() {
		if (null == news &#038;&#038; null != id) {
			EntityManager manager = JpaUtil.getCurrentEntityManager();
			news = manager.find(News.class, id);
		}
		return news;
	}

	@Override
	public void setObject(News object) {
		throw new UnsupportedOperationException("Model " + getClass() +
				" does not support setObject(Object)");
	}

	@Override
	public void detach() {
		news = null;
	}
}</code></pre>
<p>Im Konstruktor der <code>NewsList</code>-Komponente stehen auch noch einige Veränderungen aus. Zum einen soll eine <code>DataView</code> verwendet werden, zum anderen aber sind dort noch zwei Stolpersteine aus dem Weg zu räumen, die dafür sorgen, dass die News dennoch in der Session serialisiert abgespeichert werden. Das Problem war, dass die News als final deklariert war und von zwei Models referenziert wurde &#8211; das umgehen wir, indem wir über das Model auf die News zugreifen.</p>
<pre><code>	public NewsList(String id) {
		super(id);

		DataView&lt;News&gt; newsList = new DataView&lt;News&gt;("news", new NewsDataProvider(), 15) {
			@Override
			protected void populateItem(<span class="changed">final</span> Item&lt;News&gt; item) {
				News news = item.getModelObject();
				item.add(new AttributeModifier("class", true, new AbstractReadOnlyModel&lt;String&gt;() {
					@Override
					public String getObject() {
						return <span class="changed">item.getModelObject()</span>.getGame().getName().toLowerCase().replace(' ', '-');
					}
				}));
				item.add(new ExternalLink("link", news.getUrl(), news.getTitle()).setEscapeModelStrings(false));
				item.add(new Label("page", news.getPage()).setRenderBodyOnly(true));

				MultiLineLabel description = new MultiLineLabel("description", news.getDescription());
				description.setEscapeModelStrings(false);
				item.add(description);
				item.add(new ChangeVisibilityLink("expand", description));

				item.add(new DateTimeLabel("date", new PropertyModel&lt;Date&gt;(<span class="changed">item.getModel()</span>, "date")));
			}
		};
		<span class="changed">newsList.setItemReuseStrategy(new ReuseIfModelsEqualStrategy());</span>
		newsList.add(new ToggleTextBehavior("ul.newslist &gt; li a.expand", "[+]", "[-]"));
		add(newsList);
		add(new PagingNavigator("pagenavigation", newsList));
	}</code></pre>
<p>Wir ersetzen außerdem den Aufruf von <code>newsList.setReuseItems(true);</code>, da es diese Methode bei <code>DataView</code> nicht gibt, durch <code>newsList.setItemReuseStrategy(new ReuseIfModelsEqualStrategy());</code>, was uns im Grunde das selbe Verhalten gibt. Damit das funktioniert, musste unser <code>NewsModel</code> die <code>equals</code>-Methode überschreiben, damit ein sinnvoller Vergleich zustande kommt. Ist ein neues Model gleich dem bestehenden alten, so wird das alte <code>Item</code> wiederverwendet, was für die Funktionalität des Aufklappens der Beschreibung ohne JavaScript notwendig ist. Da wir direkt mit der ID vergleichen, muss die News des alten Models (die ja losgelöst wurde) auch nicht extra aus der Datenbank geladen werden.</p>
<p>Zu guter Letzt muss nun nur noch das Erstellen der <code>NewsList</code> in <code>HomePage</code> angepasst werden, denn dort ist es nicht mehr nötig, die Liste mit News zu übergeben. Nun wird auch eigentlich <code>getNews()</code> in <code>WicketApplication</code> nicht mehr benötigt. Wie üblich könnt ihr euch den kompletten <a href='http://www.rattlab.net/wp-content/uploads/feedreader-3.zip'>Sourcecode herunterladen</a>.</p>
<h3>Fazit</h3>
<p>Mit einer Menge Aufwand wurde die kleine Anwendung nun auf eine Datenbank aufgesetzt. Das hat mich weit mehr Zeit gekostet, als die vorangegangenen Tutorials, da ich mich recht frisch in die Thematik eingearbeitet habe und einige Stolpersteine aus dem Weg räumen musste. Dafür bin ich jetzt um einige Erfahrungen reicher. Perfekt ist das Projekt aber noch immer nicht, denn es wäre z.B. auch schön, wenn die News dauerhaft in der Datenbank bleiben und sie regelmäßig aktualisiert werden würden, z.B. mittels <code>Timer</code> und <code>TimerTask</code>. Das darf sich aber jeder selber überlegen, sonst kommt dieser Beitrag nie zu einem Ende.</p>
]]></content:encoded>
			<wfw:commentRss>http://rattlab.net/2008/10/persistenz-fur-den-feedreader/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Dynamik für den Feedreader</title>
		<link>http://rattlab.net/2008/09/dynamik-fur-den-feedreader/</link>
		<comments>http://rattlab.net/2008/09/dynamik-fur-den-feedreader/#comments</comments>
		<pubDate>Mon, 29 Sep 2008 19:24:59 +0000</pubDate>
		<dc:creator>Peter</dc:creator>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Wicket]]></category>

		<guid isPermaLink="false">http://www.rattlab.net/?p=35</guid>
		<description><![CDATA[Im letzten Teil haben wir eine Wicket-Komponente entwickelt, die Newsfeeds schick anzeigt. Die nötigen Daten haben wir mittels Rome aus echten Feeds erhalten. Heute wollen wir unsere Newsansicht optimieren, denn diese ist noch reichlich unübersichtlich. Dabei bringen wir auch Dynamik mittels JavaScript und Third-Party-Libraries, in unserem Fall jQuery, ins Spiel.]]></description>
			<content:encoded><![CDATA[<p>Im <a href="http://www.rattlab.net/2008/09/wicket-feedreader/">letzten Teil</a> haben wir eine Wicket-Komponente entwickelt, die Newsfeeds schick anzeigt. Die nötigen Daten haben wir mittels Rome aus echten Feeds erhalten. Heute wollen wir unsere Newsansicht optimieren, denn diese ist noch reichlich unübersichtlich. Dabei bringen wir auch Dynamik mittels JavaScript und Third-Party-Libraries, in unserem Fall <a href="http://jquery.com/">jQuery</a>, ins Spiel.<span id="more-35"></span></p>
<h3>Mehrseitigkeit</h3>
<p>Doch bevor es dynamisch wird, sorgen wir ersteinmal dafür, dass nicht alle News auf einer Seite stehen. Für solche Zwecke verwenden wir eine <code>PageableListView</code> in Verbindung mit einem <code>PageNavigator</code>. Damit nun also immer maximal 15 News auf einmal angezeigt werden, passen wir unsere Komponente entsprechend an:</p>
<pre>public class NewsList extends Panel {

	public NewsList(String id, List&lt;News&gt; news) {
		super(id);

		<span class="changed">PageableListView&lt;News&gt; newsList = new PageableListView&lt;News&gt;("news", news, 15) {</span>
			@Override
			protected void populateItem(ListItem&lt;News&gt; item) {
				final News news = item.getModelObject();
				item.add(new AttributeModifier("class", true, new AbstractReadOnlyModel&lt;String&gt;() {
					@Override
					public String getObject() {
						return news.getGame().getName().toLowerCase().replace(' ', '-');
					}
				}));
				item.add(new ExternalLink("link", news.getUrl(), news.getTitle()).setEscapeModelStrings(false));
				item.add(new Label("page", news.getPage()).setRenderBodyOnly(true));
				item.add(new MultiLineLabel("description", news.getDescription()).setEscapeModelStrings(false));
				item.add(new DateTimeLabel("date", new PropertyModel&lt;Date&gt;(news, "date")));
			}
		};
		<span class="changed">add(newsList);
		add(new PagingNavigator("pagenavigation", newsList));</span>
	}
}</pre>
<p>Die Änderung am Markup ist recht trivial:</p>
<pre>&lt;html&gt;
&lt;head&gt;
&lt;wicket:head&gt;&lt;wicket:link&gt;
	&lt;link rel="stylesheet" type="text/css" href="newslist.css" /&gt;
&lt;/wicket:link&gt;&lt;/wicket:head&gt;
&lt;/head&gt;

&lt;body&gt;
&lt;wicket:panel&gt;
	&lt;ul class="newslist"&gt;
		&lt;li wicket:id="news"&gt;
			&lt;span wicket:id="date" class="date"&gt;01.01.1970&lt;/span&gt;
			&lt;span wicket:id="page"&gt;Seite&lt;/span&gt;: &lt;a wicket:id="link"&gt;&lt;/a&gt;
			&lt;div wicket:id="description" class="description"&gt;Beschreibung&lt;/div&gt;
		&lt;/li&gt;
	&lt;/ul&gt;
	<span class="changed">&lt;div wicket:id="pagenavigation"&gt;&lt;/div&gt;</span>
&lt;/wicket:panel&gt;
&lt;/body&gt;
&lt;/html&gt;</pre>
<h3>Ajax</h3>
<p>Ein nächster störender Punkt sind die unterschiedlichen Beschreibungstexte. Während es in manchen Feeds nur kurze Beschreibungen gibt, findet sich in anderen der komplette Newstext mit Bildern und allem drum und dran wieder. Dadurch geht viel Übersicht verloren, da die Abstände zwischen den News sehr uneinheitlich sind und zumeist will man sowieso nur wissen, wo es etwas Neues gibt und dann dem Link folgen, um den Text auf der Ursprungsseite zu lesen.</p>
<p>Im einfachsten Fall also könnte man die Beschreibung einfach wegfallen lassen, aber der ein oder andere will sich ja vielleicht doch ein Bild machen, um festzustellen, ob er die News lesen will oder sie vielleicht sogar schon kennt. An dieser Stelle bietet es sich an, die Beschreibung optional einblenden zu lassen. Um mal ein Gefühl dafür zu kriegen, wie man Wicket und Ajax verbindet, werden wir erstmal Ajax verwenden. Klar, unsere News braucht also erstmal einen Link (ich habe die HTML-Datei mal auf das wesentliche gekürzt, da wir sie eben schon gesehen haben):</p>
<pre>		&lt;li wicket:id="news"&gt;
			&lt;span wicket:id="date" class="date"&gt;01.01.1970&lt;/span&gt;
			<span class="changed">&lt;a wicket:id="expand"&gt;[+]&lt;/a&gt;</span>
			&lt;span wicket:id="page"&gt;Seite&lt;/span&gt;: &lt;a wicket:id="link"&gt;&lt;/a&gt;
			&lt;div wicket:id="description" class="description"&gt;Beschreibung&lt;/div&gt;
		&lt;/li&gt;</pre>
<p>Im Java-Code gibt es nun einige Neuerungen, die ich gleich erklären werde.</p>
<pre>public class NewsList extends Panel {

	public NewsList(String id, List&lt;News&gt; news) {
		super(id);

		PageableListView&lt;News&gt; newsList = new PageableListView&lt;News&gt;("news", news, 15) {
			@Override
			protected void populateItem(ListItem&lt;News&gt; item) {
				final News news = item.getModelObject();
				item.add(new AttributeModifier("class", true, new AbstractReadOnlyModel&lt;String&gt;() {
					@Override
					public String getObject() {
						return news.getGame().getName().toLowerCase().replace(' ', '-');
					}
				}));
				item.add(new ExternalLink("link", news.getUrl(), news.getTitle()).setEscapeModelStrings(false));
				item.add(new Label("page", news.getPage()).setRenderBodyOnly(true));

				<span class="changed">MultiLineLabel description = new MultiLineLabel("description", news.getDescription());</span>
				description.setEscapeModelStrings(false);
				<span class="changed">description.setOutputMarkupPlaceholderTag(true);
				description.setVisible(false);
				item.add(description);
				item.add(new ChangeVisibilityLink("expand", description));</span>

				item.add(new DateTimeLabel("date", new PropertyModel&lt;Date&gt;(news, "date")));
			}
		};
		<span class="changed">newsList.setReuseItems(true);</span>
		add(newsList);
		add(new PagingNavigator("pagenavigation", newsList));
	}

	<span class="changed">class ChangeVisibilityLink extends AjaxFallbackLink&lt;String&gt; {

		private Component component;

		public ChangeVisibilityLink(String id, Component component) {
			super(id);
			this.component = component;
		}

		@Override
		public void onClick(AjaxRequestTarget target) {
			component.setVisible(!component.isVisible());
			if (null != target) {
				target.addComponent(component);
			}
		}
	}</span>
}</pre>
<p>Das <code>MultiLineLabel</code>, das die Beschreibung darstellt, erhält jetzt etwas mehr Aufmerksamkeit. Der erste neue Aufruf ist <code>setOutputMarkupPlaceholderTag(true)</code>, der dafür sorgt, dass ein Platzhalter im Code gelassen wird, wenn das Element nicht sichtbar ist. In der nächsten Zeile wird die Beschreibung unsichtbar gemacht, was normalerweise bedeutet, dass das gesamte HTML-Element in der gerenderten Seite gar nicht mehr vorkommt. Dann wüsste das JavaScript aber nicht, wo es den Beschreibungstext hinschreiben sollte, wenn der Link angeklickt wird &#8211; deswegen brauchen wir den Platzhalter, der dann einfach ersetzt wird.</p>
<p>Schließlich wird noch der neue Link hinzugefügt, der fürs Ändern der Sichtbarkeit der Beschreibung zuständig ist. Diesen finden wir als innere Klasse in unserer Komponente und abgeleitet ist sie von <code>AjaxFallbackLink</code>, wodurch gewährleistet ist, dass die Funktionalität sowohl mit, als auch ohne JavaScript gegeben ist. Im Konstruktor speichern wir das Element, dessen Sichtbarkeit geändert werden soll. Wichtig ist die Methode <code>onClick()</code>, die wir überschreiben müssen. Dort notieren wir die Anweisungen, die beim Klicken des Links ausgeführt werden sollen, wir Ändern also die Sichtbarkeit. Außerdem wird ein <code>AjaxRequestTarget</code> übergeben, das <code>null</code> ist, wenn es sich um keinen Ajax-Request handelt. Diesem Target werden die Elemente übergeben, die neu geladen werden sollen, in unserem Fall ist das also einfach nur die Beschreibung.</p>
<p>Ich hatte anfangs das Problem, dass zwar die Ajax-Funktionalität wunderbar lief, aber wenn ich JavaScript deaktivierte, dann geschah nix weiter, als dass die Seite neu geladen wurde. Beschreibungen wurden nicht sichtbar. Eine ganze Weile musste ich herumrätseln, bis ich auf die Lösung stieß: Bei jedem Request erneuert sich die Liste von selbst, da sich ja Listenelemente geändert haben könnten. Dabei wird auch <code>populateItem()</code> von neuem aufgerufen und unter anderem ein völlig neues <code>MultiLineLabel</code> erstellt &#8211; die Sichtbarkeitsänderung bezieht sich also auf ein Element, dass es beim erneuten Rendern gar nicht mehr gibt. Deswegen findet sich noch der Aufruf von <code>newsList.setReuseItems(true);</code> in unserer Komponente, denn die verhindert, dass sich die <code>ListView</code> neu erstellt.</p>
<h3>Mehr Dynamik</h3>
<p>Eigentlich sieht jetzt alles toll aus. Die Beschreibungen sind eingeklappt und per Ajax können wir sie ausklappen. Allerdings dauert das unter Umständen eine Weile, wenn wir nicht lokal arbeiten, sondern übers Internet. Da wäre es doch sinnvoller, die Beschreibung gleich zu laden und dann nur mit normalem JavaScript die Sichtbarkeit zu ändern. Das ist auch mein Ziel für heute &#8211; der Ajax-Kram war eher mal Neugierde, weil ich das auch mal probieren wollte.</p>
<pre>public class NewsList extends Panel {

	public NewsList(String id, List&lt;News&gt; news) {
		super(id);

		PageableListView&lt;News&gt; newsList = new PageableListView&lt;News&gt;("news", news, 15) {
			@Override
			protected void populateItem(ListItem&lt;News&gt; item) {
				final News news = item.getModelObject();
				item.add(new AttributeModifier("class", true, new AbstractReadOnlyModel&lt;String&gt;() {
					@Override
					public String getObject() {
						return news.getGame().getName().toLowerCase().replace(' ', '-');
					}
				}));
				item.add(new ExternalLink("link", news.getUrl(), news.getTitle()).setEscapeModelStrings(false));
				item.add(new Label("page", news.getPage()).setRenderBodyOnly(true));

				MultiLineLabel description = new MultiLineLabel("description", news.getDescription());
				description.setEscapeModelStrings(false);
				item.add(description);
				item.add(new ChangeVisibilityLink("expand", description));

				item.add(new DateTimeLabel("date", new PropertyModel&lt;Date&gt;(news, "date")));
			}
		};
		newsList.setReuseItems(true);
		add(newsList);
		add(new PagingNavigator("pagenavigation", newsList));
	}

	class ChangeVisibilityLink extends Link&lt;String&gt; {

		private Component component;
		<span class="changed">private AttributeModifier visibilityModifier;
		private boolean visible;</span>

		public ChangeVisibilityLink(String id, Component component) {
			super(id);
			this.component = component;
			<span class="changed">visibilityModifier = new AttributeModifier("style", true, new Model&lt;String&gt;("display:none;"));
			visible = false;

			component.add(visibilityModifier);
			add(new ChangeVisibilityBehavior(component));</span>
		}

		@Override
		public void onClick() {
			<span class="changed">if (visible) {
				component.add(visibilityModifier);
				visible = false;
			} else {
				component.remove(visibilityModifier);
				visible = true;
			}</span>
		}
	}

	<span class="changed">class ChangeVisibilityBehavior extends AbstractBehavior {

		private Component component;

		public ChangeVisibilityBehavior(Component component) {
			this.component = component;
			component.setOutputMarkupId(true);
		}

		@Override
		public void onComponentTag(Component component, ComponentTag tag) {
			super.onComponentTag(component, tag);
			String action = "var n = document.getElementById('" + this.component.getMarkupId() + "'); ";
			action += "var d = n.style.display; n.style.display = d == 'block' ? 'none' : 'block'; return false;";
			tag.put("onclick", action);
		}
	}</span>
}</pre>
<p>Wieder wurden zahlreiche Änderungen durchgeführt. Zuersteinmal wird die Beschreibung nicht unsichtbar gemacht, denn schließlich soll das <code>MultiLineLabel</code> mitsamt des Beschreibungstextes gerendert werden. Den Aufruf von <code>setOutputMarkupPlaceholderTag(true)</code> können wir uns auch sparen, da das Element ja auf jeden Fall komplett angezeigt wird.</p>
<p>Der Link zum Ändern der Sichtbarkeit erbt nicht mehr von <code>AjaxFallbackLink</code>, sondern ist nur noch ein ganz normaler Link, der im Falle von deaktiviertem JavaScript immer noch seinen Dienst tun soll. Deshalb erhält die Komponente einen <code>AttributeModifier</code>, der das nötige <code>style</code>-Attribut an Ort und Stelle bringt. Wird der Link betätigt, so wird die <code>onClick()</code>-Methode ausgeführt (wie bisher auch) und dort wird je nach dem, ob die Komponente sichtbar ist, der Modifikator hinzugefügt oder entfernt. Mit <code>isVisible()</code> und <code>setVisible()</code> können wir hier ja nicht arbeiten, da wie bereits erwähnt das gesamte Element immer im HTML-Code erscheinen soll.</p>
<p>Doch das eigentliche Ziel war es ja, die Funktionalität ohne Request an den Server umzusetzen. Dafür machen wir uns eine <code>Behavior</code> zunutze, mit der man allerlei Dinge anstellen kann. Im einfachsten Fall wird damit ein Attribut zum HTML-Element hinzugefügt, ähnlich wie beim <code>AttributeModifier</code>, aber die Möglichkeiten sind vielfältig. So wäre es auch möglich, das Tag auf noch ganz andere Arten zu verändern oder Anweisungen in den Kopf der HTML-Datei zu schreiben. Wir aber beschränken uns darauf, etwas JavaScript ins onclick-Attribut zu geben. Das sieht auf den ersten Blick zugegebenermaßen nicht besonders schick aus, aber es erfüllt seinen Zweck. Die letzte JavaScript-Anweisung, <code>return false;</code>, sorgt dafür, dass das Klicken auf den Link keinen Request an den Server zur Folge hat &#8211; sonst würde die Seite anschließend neu geladen werden.</p>
<p>Die Funktionalität ist jetzt gegeben, aber ganz zufrieden bin ich noch nicht. Der selbstgeschriebene JavaScript-Code ist nicht besonders schick und das Auf- bzw. Zuklappen ist irgendwie langweilig. Eine JavaScript-Bibliothek soll dabei beide Fliegen mit einer Klappe schlagen. Ich habe mich für jQuery entschieden &#8211; einfach, weil es irgendwie einen sympathischen Eindruck auf mich gemacht hat. Auch gut finde ich die Möglichkeit, mittels CSS-Selektoren auf mehrere HTML-Elemente zugreifen zu können, auch wenn ich das heute nicht nutzen will.</p>
<p>Wir müssen nur unsere <code>Behavior</code> anpassen, um zum gewünschten Erfolg zu kommen:</p>
<pre>	class ChangeVisibilityBehavior extends AbstractBehavior {

		private Component component;

		public ChangeVisibilityBehavior(Component component) {
			this.component = component;
			component.setOutputMarkupId(true);
		}

		<span class="changed">@Override
		public void renderHead(IHeaderResponse response) {
			super.renderHead(response);
			response.renderJavascriptReference(new ResourceReference(NewsList.class, "jquery-1.2.6.min.js"));
		}</span>

		@Override
		public void onComponentTag(Component component, ComponentTag tag) {
			super.onComponentTag(component, tag);
			<span class="changed">tag.put("onclick", "$('#" + this.component.getMarkupId() + "').slideToggle(200);return false;");
		}
	}</span></pre>
<p>In der Methode <code>renderHead</code> sorgen wir dafür, dass die JavaScript-Bibliothek verwendet wird. Diese habe ich einfach in das selbe Verzeichnis kopiert, in dem auch die Klassen und HTML-Dateien liegen. Wir müssen uns keine Sorgen machen, dass sie nun 15 mal eingebunden wird &#8211; Wicket kümmert sich darum, dass es nur einmal gemacht wird. In <code>onComponentTag</code> wird nun mittels eines Befehls ein Slide-Effekt ausgelöst und der Code sieht schon viel aufgeräumter auf.</p>
<p>Mit jQuery kann man auch auf andere Art und Weise Events an Elemente koppeln und auf diese Elemente kann man per Selektoren, wie man sie aus CSS kennt, zugreifen. Das probiere ich aus, indem ich das Plus vom Aufklapp-Link durch ein Minus ersetze, wenn geklickt wird. Dazu schreibe ich eine neue Behavior:</p>
<pre>	class ToggleTextBehavior extends AbstractBehavior {

		private String cssSelector;
		private String text;
		private String otherText;

		public ToggleTextBehavior(String cssSelector, String text, String otherText) {
			this.cssSelector = cssSelector;
			this.text = text;
			this.otherText = otherText;
		}

		public void renderHead(IHeaderResponse response) {
			super.renderHead(response);
			response.renderJavascriptReference(new ResourceReference(NewsList.class, "jquery-1.2.6.min.js"));
			StringBuilder javascript = new StringBuilder();
			javascript.append("$(document).ready(function() {\n");
			javascript.append("  $('" + cssSelector + "').toggle(function() {\n");
			javascript.append("    $(this).empty().append('" + otherText + "');\n");
			javascript.append("  }, function() {\n");
			javascript.append("    $(this).empty().append('" + text + "');\n");
			javascript.append("  })\n");
			javascript.append("});");
			response.renderJavascript(javascript, null);
		}
	}</pre>
<p>An welches Element sie gehangen wird, ist unerheblich, da hier nur der Header verändert wird. Man könnte es an die <code>ListView</code> hängen, aber auch direkt an den <code>ChangeVisibilityLink</code> und natürlich muss man sich auch hier keine Sorgen machen, dass der Code evtl. mehrmals angezeigt wird.</p>
<pre>newsList.add(new ToggleTextBehavior("ul.newslist > li a.expand", "[+]", "[-]"));</pre>
<p>Damit dieser hier verwendete Selektor funktioniert, braucht der Aufklapp-Link noch eine CSS-Klasse namens &#8220;expand&#8221;. Abgesehen davon spart man sich viele verschiedene IDs an den Tags &#8211; auch den Aufklapp-Effekt der Beschreibung hätten wir auf diese Art umsetzen können. Wer nicht zig Behaviors selber schreiben will, der kann sich mal <a href="http://code.google.com/p/wickext/">Wickext</a> ansehen. Das ist ein Projekt, das versucht, Wicket und jQuery zusammenzuführen, so dass der Anwender dann keinen eigenen JavaScript-Code mehr schreiben muss.</p>
<h3>Fazit</h3>
<p>Damit haben wir heute schon etwas erreicht. Die News sind wesentlich übersichtlicher und werden aufs Nötigste reduziert &#8211; Titel, Link und Zeitpunkt. Optional kann man sich mit einem Klick die Beschreibung anzeigen lassen, die stylish aufklappt oder, falls JavaScript deaktiviert ist, über einen normalen Request abläuft, damit niemand außen vor gelassen wird. Außerdem haben wir gesehen, wie einfach es ist, ein wenig Ajax in die Anwendung zu bringen, auch wenn wir diesen Ansatz dann wieder verworfen haben. Das gesamte Projekt könnt ihr euch natürlich <a href="http://www.rattlab.net/wp-content/uploads/feedreader-2.zip">herunterladen</a>.</p>
<p><a href='http://www.rattlab.net/wp-content/uploads/feedreader-2.jpg'><img src="http://www.rattlab.net/wp-content/uploads/feedreader-2-300x272.jpg" alt="" title="Feedreader mit Dynamik" width="300" height="272" class="aligncenter size-medium wp-image-36" /></a></p>
<p>Was kommt als nächstes? Abgesehen davon, dass der Code nicht kommentiert ist und manche der inneren Klassen durchaus in eigene ausgelagert werden könnten, gibt es auch noch immer funktionale Schwachstellen. Der Feed wird immer beim Start des Servers geladen und dann nie wieder aktualisiert. Alle News werden zusammen mit der <code>WebPage</code> serialisiert und in der Session hinterlegt, was unnützer Verbrauch von Ressourcen ist. Es gibt also noch jede Menge zu tun.</p>
<p>Der dritte Teil geht das Problem mit JPA und Hibernate an: <a href="http://www.rattlab.net/2008/10/persistenz-fur-den-feedreader/">Persistenz für den Feedreader</a>.</p>
]]></content:encoded>
			<wfw:commentRss>http://rattlab.net/2008/09/dynamik-fur-den-feedreader/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Wicket Feedreader</title>
		<link>http://rattlab.net/2008/09/wicket-feedreader/</link>
		<comments>http://rattlab.net/2008/09/wicket-feedreader/#comments</comments>
		<pubDate>Sun, 21 Sep 2008 09:58:55 +0000</pubDate>
		<dc:creator>Peter</dc:creator>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Wicket]]></category>

		<guid isPermaLink="false">http://www.rattlab.net/?p=32</guid>
		<description><![CDATA[Nachdem ich mich vor einiger Zeit mit Maven beschäftigt und schließlich ein kleines Wicket-Projekt angelegt habe, möchte ich nun endlich etwas tiefer in die Materie eindringen. Heute entwickeln wir eine Wicket-Komponente, die News anzeigt. Die nötigen News bekommen wir aus Newsfeeds verschiedener Spiele-Fanseiten, so dass ich nicht erst zig Seiten nach Neuigkeiten abklappern muss. Ja, ich weiß, dass es Feedreader gibt, aber ich fand das eine gute Idee, um mal etwas zu basteln.]]></description>
			<content:encoded><![CDATA[<p>Nachdem ich mich <a href="http://www.rattlab.net/2008/08/wicket-projekt-mit-maven/">vor einiger Zeit</a> mit Maven beschäftigt und schließlich ein kleines <a href="http://wicket.apache.org/">Wicket</a>-Projekt angelegt habe, möchte ich nun endlich etwas tiefer in die Materie eindringen. Heute entwickeln wir eine Wicket-Komponente, die News anzeigt. Die nötigen News bekommen wir aus Newsfeeds verschiedener Spiele-Fanseiten, so dass ich nicht erst zig Seiten nach Neuigkeiten abklappern muss. Ja, ich weiß, dass es Feedreader gibt, aber ich fand das eine gute Idee, um mal etwas zu basteln.<span id="more-32"></span></p>
<p><a href='http://www.rattlab.net/wp-content/uploads/feedreader.jpg'><img src="http://www.rattlab.net/wp-content/uploads/feedreader-300x246.jpg" alt="Wicket Feedreader" title="Wicket Feedreader" width="300" height="246" class="aligncenter size-medium wp-image-33" /></a></p>
<h3>Den Grundstein legen</h3>
<p>Zuerst legen wir ein neues Wicket-Projekt an. Da erst die Version 1.4 von Wicket auch Generics unterstützt, möchte ich diese (noch nicht finale) Version nutzen. Im Formular der <a href="http://wicket.apache.org/quickstart.html">Quickstart-Anleitung</a> wählen wir uns deshalb die Version 1.4-m3 aus, das ist der dritte (und derzeit aktuellste) Milestone. Außerdem passen wir noch ArtifactID und GroupId an unsere Bedürfnisse an. Wir wechseln in unser Projekt-Verzeichnis, kopieren den Code aus dem Formular in die Shell und führen ihn aus. Ich probiere bei dieser Gelegenheit auch gleich <a href="http://www.rattlab.net/2008/09/cygwin/">Cygwin</a> aus &#8211; das Einfügen des vorher kopierten Codes geschieht hier mittels Rechtsklick.</p>
<pre class="shell">$ cd /cygdrive/c/myprojects
$ mvn archetype:create -DarchetypeGroupId=org.apache.wicket -DarchetypeArtifactId=wicket-archetype-quickstart -DarchetypeVersion=1.4-m3 -DgroupId=net.rattlab.wicket -DartifactId=wicket
[...]
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating OldArchetype: wicket-archetype-quickstart:1.4-m3
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: net.rattlab.wicket
[INFO] Parameter: packageName, Value: net.rattlab.wicket
[INFO] Parameter: package, Value: net.rattlab.wicket
[INFO] Parameter: artifactId, Value: wicket
[INFO] Parameter: basedir, Value: c:\myprojects
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] ********************* End of debug info from resources from generated POM ***********************
[INFO] OldArchetype created in dir: c:\myprojects\wicket
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 4 seconds
[INFO] Finished at: Thu Sep 18 19:21:18 CEST 2008
[INFO] Final Memory: 7M/14M
[INFO] ------------------------------------------------------------------------</pre>
<p>Da ich mit Eclipse arbeite, bereite ich nun den Import vor. Ich wechsle ins Projektverzeichnis und rufe das Eclipse-Plugin mit einem zusätzlichen Parametern auf, damit der Quellcode der Projekte heruntergeladen wird, die als Abhängigkeiten deklariert sind.</p>
<pre>$ cd wicket
$ mvn eclipse:eclipse -DdownloadSources=true</pre>
<p>Nach dem Starten von Eclipse importieren wir das Projekt über <code>File &gt; Import &gt; General &gt; Existing Projects into Workspace</code> und dort wählen wir das Projektverzeichnis aus. Ganz so, wie im <a href="http://herebebeasties.com/2007-10-07/wicket-quickstart/">Screencast von Alastair Maw</a>. Da das Projekt dann noch viele Fehler aufweist, gehen wir in <code>Window &gt; Preferences &gt; Java &gt; Build Path &gt; Classpath Variables</code>, legt dort per <code>New</code> eine neue Variable ein namens <code>M2_REPO</code> an und geben den Pfad auf das lokale Maven Repository ein, in meinem Fall wäre das <code>C:/Users/ex-ratt/.m2/repository</code>.</p>
<p>Nun gibt es in meinem Fall immer noch Fehler. Von Milestone zwei zu Milestone drei wurde die Verwendung von Generics geändert, aber scheinbar wurde der Quickstart-Archetype nicht daran angepasst. Alle Stellen, die noch als Fehler angezeigt werden, müssen also angepasst werden, indem die Typbeschreibung mit den spitzen Klammern entfernt wird. Das sind erstmal nur zwei in der Klasse <code>HomePage</code>, nämlich in Zeile 10 und in Zeile 25.</p>
<p>Um zu testen, ob das jetzt auch wirklich geht, führen wir die Klasse <code>Start</code> aus, die im Test-Verzeichnis zu finden ist. Sie startet eine Jetty-Instanz und über die URL <code>http://localhost:8080/</code> oder <code>http://127.0.0.1:8080/</code> sieht man die übliche Nachricht &#8220;If you see this message wicket is properly configured and running&#8221;. Nachdem das geklärt ist, kann es ja endlich losgehen.</p>
<p>Zuerst brauchen wir zwei Model-Klassen, die lediglich die Daten halten. Die eine wird die Daten einer News speichern und die andere stellt ein Spiel dar. Auf die Darstellung der Getter und Setter verzichte ich, die wird sich jeder dazudenken können, hoffe ich.</p>
<pre>public class Game {
	private String name;
}</pre>
<pre>public class News {
	private Game game;
	private String page;
	private String url;
	private String title;
	private String description;
	private Date date;
}</pre>
<p>Unsere Komponente nennt sich NewsList, da sie eine Liste von News darstellt, die aus verschiedenen Feeds kommt. Aus diesem Grund speichert jede News auch den Namen der Seite, von der sie kommt. Der HTML-Code unserer Komponente ist jedenfalls recht übersichtlich:</p>
<pre>&lt;html&gt;
&lt;body&gt;
&lt;wicket:panel&gt;
	&lt;ul&gt;
		&lt;li wicket:id="news"&gt;
			&lt;span wicket:id="date"&gt;01.01.1970&lt;/span&gt;
			&lt;span wicket:id="page"&gt;Seite&lt;/span&gt;: &lt;a wicket:id="link"&gt;&lt;/a&gt;
			&lt;div wicket:id="description"&gt;Beschreibung&lt;/div&gt;
		&lt;/li&gt;
	&lt;/ul&gt;
&lt;/wicket:panel&gt;
&lt;/body&gt;
&lt;/html&gt;</pre>
<p>Hier wird deutlich, dass die HTML-Dateien tatsächlich keinerlei Logik enthalten. Viele Template-Engines hätten an irgendeiner Stelle eine Schleife, die dafür sorgt, dass ein bestimmter Teil mehrmals wiederholt wird &#8211; nämlich so oft, wie es News gibt. Bei Wicket aber wird derartiges komplett im Java-Code gemacht. Das <code>wicket:panel</code>-Tag grenzt hier das Panel ein. Alles, was im inneren des Tags ist, gehört zum Panel und wird dort angezeigt, wo es eingebunden wird, alles andere fällt weg. So ist es möglich, zu Vorschauzwecken HTML-Konstrukte darum zu bauen, um zu sehen, wie die Komponente aussähe, ohne die ganze Applikation starten zu müssen. Die Texte innerhalb von Labels, wie z.B. die Beschreibung, werden im laufenden Betrieb durch den eigentlichen Inhalt ersetzt und dienen hier auch nur der Vorschau &#8211; man könnte sie also auch weglassen.</p>
<pre>public class NewsList extends Panel {

	public NewsList(String id, List&lt;News&gt; news) {
		super(id);

		add(new ListView&lt;News&gt;("news", news) {
			@Override
			protected void populateItem(ListItem&lt;News&gt; item) {
				News news = item.getModelObject();
				item.add(new ExternalLink("link", news.getUrl(), news.getTitle()));
				item.add(new Label("page", news.getPage()).setRenderBodyOnly(true));
				item.add(new Label("description", news.getDescription()));
				item.add(new Label("date", new PropertyModel&lt;Date&gt;(news, "date")));
			}
		});
	}
}</pre>
<p><code>ListView</code> ist genauso wie <code>Label</code> und <code>ExternalLink</code> eine Komponente, die Wicket von sich aus mitbringt. Dabei muss man immer eine ID angeben, die der im HTML-Code angegebenen <code>wicket:id</code> entsprechen muss. Die ListView ist dabei insofern besonders, als dass sie das entsprechende Tag mitsamt den Kindelementen wiederholt &#8211; hier taucht also die Schleife auf.</p>
<p>Die Methode <code>populateItem()</code> wird für jede News aufgerufen, die sich in der Liste befindet, die dem Konstruktor übergeben wurde. Jedem <code>ListItem</code> werden dann wiederum verschiedene Komponenten hinzugefügt. Wieder hat jede eine ID und außerdem wird ein Model benötigt, dass den darzustellenden Text enthält. In den meisten Fällen übergeben wir hier einfach einen <code>String</code> und die Komponente baut sich selber ein einfaches Model drumrum, aber im Falle des Datums machen wir das selber. So wird automatisch ein Converter aufgerufen, der das Datum so darstellt, wie es für unsere Region typisch ist. Würden wir das Datum pur angeben, würde es direkt per <code>toString</code> umgewandelt werden.</p>
<p>Weiterhin fällt der Aufruf von <code>setRenderBodyOnly()</code> bei dem Label, das den Namen der Seite enthält, auf. Das sorgt dafür, dass das <code>span</code>-Tag im HTML-Code nicht dargestellt wird, sondern nur der Name der Seite. Dazu können wir uns hinterher mal den erzeugten Quellcode ansehen.</p>
<p>Die Komponente sollte soweit funktionieren, jetzt müssen wir sie nur noch verwenden und mit Daten füttern. Dazu ändern wir die bereits bestehende <code>HomePage.java</code> an unsere Bedürfnisse an:</p>
<pre>public class HomePage extends WebPage {

	private static final long serialVersionUID = 1L;

	<span class="javadoc comment">/**
	 * Constructor that is invoked when page is invoked without a session.
	 *
	 * @param parameters
	 *            Page parameters
	 */</span>
	public HomePage(final PageParameters parameters) {

		Game game = new Game("Spiel 1");
		List&lt;News&gt; news = new LinkedList&lt;News&gt;();
		News news1 = new News();
		news1.setGame(game);
		news1.setPage("Seite1");
		news1.setTitle("Erste News");
		news1.setUrl("http://www.asdf.com/erste-news/");
		news1.setDescription("Lorem ipsum bla");
		news1.setDate(new Date());
		news.add(news1);
		News news2 = new News();
		news2.setGame(game);
		news2.setPage("Seite1");
		news2.setTitle("Zweite News");
		news2.setUrl("http://www.asdf.com/zweite-news/");
		news2.setDescription("Lorem ipsum bla");
		news2.setDate(new Date());
		news.add(news2);

		add(new NewsList("news", news).setRenderBodyOnly(true));
	}
}</pre>
<p>Hier basteln wir kurz ein paar Testdaten zusammen und fügen unsere <code>NewsList</code> einfach der Seite hinzu. Außerdem sorgen wir auch hier dafür, dass das Tag, das wir im Markup der Seite nutzen, nicht erscheint. Der zur Seite gehörende HTML-Code (<code>HomePage.html</code>) muss natürlich auch angepasst werden:</p>
<pre>&lt;html&gt;
    &lt;head&gt;
        &lt;title&gt;News Feed Reader&lt;/title&gt;
    &lt;/head&gt;
    &lt;body&gt;
        &lt;div wicket:id="news"&gt;&lt;/div&gt;
    &lt;/body&gt;
&lt;/html&gt;</pre>
<p>Das wars auch schon. Für diejenigen aber, die mit Hilfe von Maven kompilieren, bleibt noch etwas zu tun, denn ein Test schlägt fehl. Dazu müssen wir in der <code>TestHomePage.java</code>, diese liegt im Test-Verzeichnis, die Zeile 27 auskommentieren oder löschen, denn dort wird der Inhalt eines Labels geprüft, das es in unserer Version nicht mehr gibt. Eine Einführung zum Testen gebe ich evtl. ein anderes Mal.</p>
<p>Würden wir jetzt unsere Anwendung starten, die Seite aufrufen und den Quelltext betrachten, so würden wir feststellen, dass dort noch die <code>wicket:id</code>-Attribute, sowie die <code>&lt;wicket:panel&gt;</code>-Tags enthalten wären. Während der Entwicklung ist das noch hilfreich, in einer späteren Produktivumgebung aber will man das vielleicht nicht mehr drin haben. Um das zu erreichen, konfigurieren wir es in unserer <code>WicketApplication</code>. Der wesentliche Code ist rot dargestellt:</p>
<pre>public class WicketApplication extends WebApplication {

	<span class="javadoc comment">/**
	 * Constructor
	 */</span>
	public WicketApplication() {}

	<span class="changed">@Override
	protected void init() {
		super.init();
		getMarkupSettings().setStripWicketTags(true);
	}</span>

	<span class="javadoc comment">/**
	 * @see wicket.Application#getHomePage()
	 */</span>
	public Class&lt;HomePage&gt; getHomePage() {
		return HomePage.class;
	}

}</pre>
<p>Nachdem wir den Server gestartet haben und nun die Seite betrachtet, sehen wir folgenden erzeugten HTML-Code:</p>
<pre>&lt;html&gt;
    &lt;head&gt;
        &lt;title&gt;News Feed Reader&lt;/title&gt;
    &lt;/head&gt;
    &lt;body&gt;

	&lt;ul&gt;
		&lt;li&gt;
			&lt;span&gt;19.09.08&lt;/span&gt;
			Seite1: &lt;a href="http://www.asdf.com/erste-news/"&gt;Erste News&lt;/a&gt;
			&lt;div&gt;Lorem ipsum bla&lt;/div&gt;
		&lt;/li&gt;&lt;li&gt;
			&lt;span&gt;19.09.08&lt;/span&gt;
			Seite1: &lt;a href="http://www.asdf.com/zweite-news/"&gt;Zweite News&lt;/a&gt;
			&lt;div&gt;Lorem ipsum bla&lt;/div&gt;
		&lt;/li&gt;
	&lt;/ul&gt;

    &lt;/body&gt;
&lt;/html&gt;</pre>
<p>Das ist nun weder besonders schick, noch handelt es sich um einen richtigen Feedreader, also ist noch etwas Arbeit vonnöten. Schauen wir uns also erstmal an, wie wir echte Newsfeeds einlesen können.</p>
<h3>Echte Newsfeeds mit Rome</h3>
<p>Feeds sind Daten in XML-Form, die sich aber je nach Art des Feeds (Atom oder RSS) und der Version etwas voneinander unterscheiden. Aber zum Glück gibt es dafür bereits Bibliotheken, die uns die Arbeit des Parsens abnehmen, wie das <a href="https://rome.dev.java.net/">Projekt Rome</a>. Die Version 0.9 liegt im zentralen Maven-Repository, so dass wir uns um den Download keine Gedanken machen müssen. Wir tragen einfach eine weitere Dependency in die <code>pom.xml</code> ein und lassen Maven den Rest erledigen.</p>
<pre>&lt;dependency&gt;
	&lt;groupId&gt;rome&lt;/groupId&gt;
	&lt;artifactId&gt;rome&lt;/artifactId&gt;
	&lt;version&gt;0.9&lt;/version&gt;
&lt;/dependency&gt;</pre>
<p>Nur woher weiß man, welche Projekte im Maven-Repository liegen und wie kommt man an <code>groupId</code> und <code>artifactId</code>? Entweder, diese Informationen findet man auf der Seite des Projekts (wie es bei Wicket <a href="http://wicket.apache.org/getting-wicket.html">der Fall ist</a>) oder man sucht auf <a href="http://www.mvnrepository.com/">www.mvnrepository.com</a>, so habe ich es in diesem Fall gemacht. Jetzt wechseln wir wieder per Kommandozeile in das Verzeichnis unseres Projektes, wenn wir da nicht ohnehin sind und führen <code>mvn install</code> aus, damit die neuen Abhängigkeiten heruntergeladen werden.</p>
<pre class="shell">$ cd /cygdrive/c/myprojects/wicket
$ mvn install</pre>
<p>Spätestens jetzt wird uns Maven einen Fehler um die Ohren hauen, wenn wir vorher nicht die <code>TestHomePage.java</code> wie oben angegeben geändert haben. Das stört aber nicht weiter, da die neuen Abhängigkeiten vor dem Ausführen der Tests heruntergeladen wurden. Die Eclipse Settings müssen nun natürlich noch auf den aktuellen Stand gebracht werden:</p>
<pre class="shell">$ mvn eclipse:eclipse -DdownloadSources=true</pre>
<p>Anschließend reicht ein Neu Laden des Projektes, damit die neue Bibliothek auch in Eclipse erkannt wird. Die News aus den Feeds wollen wir nun nur einmal beim Starten des Servers laden, damit der Zugriff auf eine Seite weiterhin schnell bleibt. Dafür bietet sich unsere <code>WicketApplication</code> Klasse an, denn diese wird nur ein einziges Mal beim Starten des Servers instanziiert. Wir fügen eine ganze Menge neues Zeug hinzu. Diesmal der Vollständigkeit halber mit allen Imports:</p>
<pre>package net.rattlab.wicket;

<span class="changed">import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import net.rattlab.model.Game;
import net.rattlab.model.News;</span>

<span class="changed">import org.apache.wicket.Application;</span>
import org.apache.wicket.protocol.http.WebApplication;

<span class="changed">import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.io.FeedException;
import com.sun.syndication.io.SyndFeedInput;
import com.sun.syndication.io.XmlReader;</span>

<span class="javadoc comment">/**
 * Application object for your web application. If you want to run this application without deploying, run the Start class.
 *
 * @see net.rattlab.wicket.Start#main(String[])
 */</span>
public class WicketApplication extends WebApplication {

	<span class="changed">private List&lt;Game&gt; games;
	private List&lt;News&gt; news;</span>

	<span class="javadoc comment">/**
	 * Constructor
	 */</span>
	public WicketApplication() {
		<span class="changed">games = new LinkedList&lt;Game&gt;();
		news = new LinkedList&lt;News&gt;();

		createGames();
		createNews();</span>
	}

	<span class="changed"><span class="javadoc comment">/**
	 * Creates the games and news feeds.
	 */</span>
	protected void createGames() {
		Game starcraft2 = new Game("StarCraft 2");
		Game diablo3 = new Game("Diablo 3");

		starcraft2.addFeed("inStarCraft", "http://starcraft2.ingame.de/feed.php?type=RSS1.0&#038;section=345");
		starcraft2.addFeed("StarCraft 2 Mecca", "http://forum.gamersunity.de/external.php?type=rss2&#038;forumids=195");
		diablo3.addFeed("inDiablo", "http://diablo3.ingame.de/feed.php?type=RSS1.0");
		diablo3.addFeed("Diablo 3 Source", "http://diablo3.4players.de/rss_feed.xml");

		games.add(starcraft2);
		games.add(diablo3);
	}</span>

	<span class="changed"><span class="javadoc comment">/**
	 * Creates the news from the news feeds.
	 */</span>
	protected void createNews() {
		SyndFeedInput input = new SyndFeedInput();
		for (Game game : games) {
			Map&lt;String , String&gt; feeds = game.getFeeds();
			for (String page : feeds.keySet()) {
				String feedUrl = feeds.get(page);
				try {
					URL url = new URL(feedUrl);
					SyndFeed feed = input.build(new XmlReader(url));
					for (Object o : feed.getEntries()) {
						<span class="comment">// rome 0.9 does not support generics</span>
						SyndEntry entry = (SyndEntry) o;
						news.add(new News(game, page, entry.getLink(), entry.getTitle(),
								entry.getDescription().getValue(), entry.getPublishedDate()));
					}
				}
				catch (MalformedURLException exc) {}
				catch (IllegalArgumentException e) {}
				catch (FeedException e) {}
				catch (IOException e) {}
			}
		}
		<span class="comment">// sort news by date in descending order</span>
		Collections.sort(news, new Comparator&lt;News&gt;() {
			public int compare(News news, News otherNews) {
				if (null == news.getDate() &#038;&#038; null == otherNews.getDate()) {
					return 0;
				}
				if (null == news.getDate()) {
					return 1;
				}
				if (null == otherNews.getDate()) {
					return -1;
				}
				return -1 * news.getDate().compareTo(otherNews.getDate());
			}
		});
	}</span>

	@Override
	protected void init() {
		super.init();
		getMarkupSettings().setStripWicketTags(true);
	}

	<span class="changed">public static WicketApplication get() {
		return (WicketApplication) Application.get();
	}</span>

	<span class="javadoc comment">/**
	 * @see org.apache.wicket.Application#getHomePage()
	 */</span>
	public Class&lt;Homepage&gt; getHomePage() {
		return HomePage.class;
	}

	<span class="changed">public List&lt;News&gt; getNews() {
		return news;
	}</span>

}</pre>
<p>Neu sind zuerst einmal die zwei Listen. Eine davon speichert die Spiele, die andere die noch zu generierenden News. Im Konstruktor erzeugen wir die Spiele und fügen jedem beispielhaft zwei Newsfeeds hinzu mit dem Namen der Seite, von der sie kommen. Anschließend werden die News aus den Feeds geholt. Zuviel will ich dazu nicht erklären &#8211; wer mehr wissen will, kann sich die <a href="http://wiki.java.net/bin/view/Javawsxml/Rome05Tutorials">Rome Tutorials</a> anschauen. Dass ich die Exceptions einfach ignoriere, ist nicht besonders vorbildlich, aber darum soll es hier nicht primär gehen.</p>
<p>Schließlich wird die Liste mit den News noch sortiert, so dass die aktuellsten Nachrichten ganz oben stehen. Da es nicht garantiert ist, dass die Daten nicht <code>null</code> sind, muss hier diese doch recht hässliche Abfrage erfolgen.</p>
<p>Schließlich gibt es noch zwei weitere neue Methoden. Die eine davon gibt einfach nur die Liste der News zurück, die andere das <code>WicketApplication</code>-Objekt selber. Das brauchen wir, um von unserer Komponente aus auf die News zugreifen zu können.</p>
<p>Wie zu sehen ist, hat auch <code>Game</code> ein paar neue Methoden spendiert bekommen, <code>addFeed()</code> und <code>getFeeds</code>. Hier also die überarbeitete Klasse, diesmal mit Getter und Setter:</p>
<pre>public class Game {

	private String name;
	<span class="changed">private Map&lt;String , String&gt; feeds;</span>

	public Game(String name) {
		setName(name);
		<span class="changed">feeds = new HashMap&lt;String , String&gt;();</span>
	}

	<span class="changed">public void addFeed(String page, String url) {
		feeds.put(page, url);
	}</span>

	<span class="changed">public Map&lt;String , String&gt; getFeeds() {
		return feeds;
	}</span>

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
}</pre>
<p>Nachdem insbesondere in der <code>WicketApplication</code> so viel neuer Code hinzugekommen ist, wurde er bei <code>HomePage</code> weniger, denn dort haben wir vorher unsere Daten erzeugt. Der Konstruktor ist jetzt auf eine Zeile geschrumpft, sonst hat sich nichts geändert:</p>
<pre>public class HomePage extends WebPage {

	private static final long serialVersionUID = 1L;

	<span class="javadoc comment">/**
	 * Constructor that is invoked when page is invoked without a session.
	 *
	 * @param parameters
	 *            Page parameters
	 */</span>
	public HomePage(final PageParameters parameters) {
	    <span class="changed">add(new NewsList("news", WicketApplication.get().getNews()).setRenderBodyOnly(true));</span>
	}
}</pre>
<p>Jetzt haben wir &#8220;echte&#8221; Daten und können das gleich nochmal ausprobieren. Die Liste der News ist jetzt recht lang, sie ist unübersichtlich und hässlich, aber hey, daran kann man ja noch arbeiten.</p>
<h3>Das Styling</h3>
<p>Auch wenn man sich das eigentlich für den Schluss aufheben könnte, so finde ich es doch wesentlich angenehmer, wenn man mit etwas halbwegs ansehnlichem arbeiten kann. Deswegen habe ich ein paar Zeilen CSS-Code geschrieben und diesen ins selbe Package gelegt, in dem auch schon <code>NewsList.java</code> und <code>NewsList.html</code> liegen. Hier ist also die <code>newslist.css</code>, die ich nicht näher ausführen werde, da ich nicht zu sehr abschweifen will, aber kompliziert ist es ohnehin nicht:</p>
<pre>ul.newslist {
	margin: 0;
	padding: 0;
	list-style: none;
	max-width: 800px;
	font-family: Arial, sans-serif;
	font-size: 13px;
}

ul.newslist > li {
	margin: 10px 0;
	padding: 5px;
	border: 1px solid #999;
	background-color: #ccc;
	color: #000;
}

ul.newslist li.starcraft-2 {
	border-color: #69f;
	background-color: #adf;
}

ul.newslist li.diablo-3 {
	border-color: #c63;
	background-color: #fab;
}

ul.newslist li span.date {
	float: right;
}

ul.newslist li div.description {
	margin: 5px -5px -5px;
	padding: 0 5px;
	border-top: 1px solid #999;
}

ul.newslist li div.description p {
	margin: 5px 0;
}

ul.newslist li.starcraft-2 div.description {
	border-color: #69f;
}

ul.newslist li.diablo-3 div.description {
	border-color: #c63;
}

ul.newslist li a {
	color: #666;
	text-decoration: none;
	font-weight: bold;
}

ul.newslist li a:hover {
	text-decoration: underline;
}</pre>
<p>Damit diese Styles auch angewendet werden, müssen wir zweierlei Dinge tun. Zum einen müssen wir unseren Tags die CSS-Klassen zuweisen, das machen wir natürlich direkt im Markup. Außerdem muss die CSS-Datei auch eingebunden werden. Auch das können wir direkt in der HTML-Datei der Komponente machen:</p>
<pre>&lt;html&gt;
<span class="changed">&lt;head&gt;
&lt;wicket:head&gt;&lt;wicket:link&gt;
	&lt;link rel="stylesheet" type="text/css" href="newslist.css" /&gt;
&lt;/wicket:link&gt;&lt;/wicket:head&gt;
&lt;/head&gt;</span>

&lt;body&gt;
&lt;wicket:panel&gt;
	&lt;ul <span class="changed">class="newslist"</span>&gt;
		&lt;li wicket:id="news"&gt;
			&lt;span wicket:id="date" <span class="changed">class="date"</span>&gt;&lt;/span&gt;
			&lt;span wicket:id="page"&gt;&lt;/span&gt;: &lt;a wicket:id="link"&gt;&lt;/a&gt;
			&lt;div wicket:id="description" <span class="changed">class="description"</span>&gt;&lt;/div&gt;
		&lt;/li&gt;
	&lt;/ul&gt;
&lt;/wicket:panel&gt;
&lt;/body&gt;
&lt;/html&gt;</pre>
<p>Hier kommen zwei neue Wicket-Tags zum Einsatz, <code>&lt;wicket:head&gt;</code> und <code>&lt;wicket:link&gt;</code>. Ersteres wird verwendet, wenn Tags in den Head der HTML-Datei sollen. Jede Komponente kann so eigene Header-Angaben einbringen. Zweiteres löst automatisch den Link auf, der zur CSS-Datei führt &#8211; das klappt z.B. auch mit <code>a</code>- und <code>img</code>-Tags und ist nicht auf den Header beschränkt. Es ist auch möglich, die CSS-Datei z.B. ins <code>WEB-INF</code>-Verzeichnis zu schieben und auf andere Weise einzubinden (direkt per Java), aber für uns genügt das erstmal zur Demonstration. Abgesehen davon ist es sowieso fraglich, ob es Sinn macht, Designinformationen wie Farben direkt einer Komponente zuzuordnen, da diese auf verschiedenen Seiten sehr unpassend wirken könnten.</p>
<p>In der CSS-Datei konnte man sehen, dass für News zu StarCraft 2 eine andere Farbe verwendet wird, als für News zu Diablo 3. Im HTML-Code unserer Komponente aber hat das <code>li</code>-Tag gar kein <code>class</code>-Attribut. Wie kommt das also da dran? Viele Varianten gibt es ja nicht &#8211; es wird im Java-Code gemacht, und zwar in unserer Komponente:</p>
<pre>public class NewsList extends Panel {

	public NewsList(String id, List&lt;News&gt; news) {
		super(id);

		add(new ListView&lt;News&gt;("news", news) {
			@Override
			protected void populateItem(ListItem&lt;News&gt; item) {
				final News news = item.getModelObject();
				<span class="changed">item.add(new AttributeModifier("class", true, new AbstractReadOnlyModel&lt;String&gt;() {
					@Override
					public String getObject() {
						return news.getGame().getName().toLowerCase().replace(' ', '-');
					}
				}));</span>
				item.add(new ExternalLink("link", news.getUrl(), news.getTitle()));
				item.add(new Label("page", news.getPage()).setRenderBodyOnly(true));
				item.add(new Label("description", news.getDescription()));
				item.add(new Label("date", new PropertyModel&lt;Date&gt;(news, "date")));
			}
		});
	}
}</pre>
<p>Mit dem <code>AttributeModifier</code> können wir jedes erdenkliche Attribut ändern. In diesem Fall fügen wir ein <code>class</code>-Attribut hinzu, das <code>true</code> im Konstruktor sorgt dafür, dass es angelegt wird, wenn es noch nicht da ist. Wie so oft beim Einsatz von Wicket wird auch hier ein Model übergeben. Da die Eigenschaft nicht verändert wird, probieren wir mal ein Read-Only-Model aus. Darin holen wir uns den Namen des Spiels, wandeln diesen in Kleinbuchstaben um und ändern alle Leerzeichen in Bindestriche. Wenn damit zu rechnen wäre, dass noch andere Sonderzeichen, wie z.B. Doppelpunkte, in Spielenamen vorkommen, so sollte man sie auch in Bindestriche umwandeln oder ganz rausfallen lassen.</p>
<p>Wenn wir die Anwendung jetzt neustarten und unsere News im Browser aufrufen, so sehen sie schon wesentlich schicker aus, als vorher. Aber halt, was ist das? Scheinbar wurde der HTML-Code, der in manchen Feeds aufgetaucht ist, maskiert, so dass jetzt unschöne Tags im Text auftauchen. Außerdem reicht uns das Datum nicht mehr aus, wie wollen auch die Uhrzeit wissen.</p>
<p>Die Maskierung bekommen wir recht einfach weg. Dafür gibt es ein Flag vom Label, das wir setzen müssen. Bei näherer Betrachtung des Quellcodes unserer erzeugten Newsansicht fällt mir aber noch etwas anderes auf. So scheint es im Feed Zeilenumbrüche zu geben, die aber vom Browser ignoriert werden. Wenn man sich die News mal ansieht, nachdem man die Maskierung entfernt hat, sieht man das auch, denn es ist immer noch sehr unübersichtlich, da eben diese Zeilenumbrüche fehlen. Für solche Zwecke gibt es das <code>MultiLineLabel</code>, das einfache Zeilenumbrüche in ein <code>&lt;br/&gt;</code> und doppelte in <code>&lt;/p&gt;&lt;p&gt;</code> verwandelt. Außerdem wird der gesamte Text des Labels noch in <code>p</code>-Tags gefasst, so dass es am Ende auch alles passt. Wir ändern unsere Komponente also:</p>
<pre>public class NewsList extends Panel {

	public NewsList(String id, List&lt;News&gt; news) {
		super(id);

		add(new ListView&lt;News&gt;("news", news) {
			@Override
			protected void populateItem(ListItem&lt;News&gt; item) {
				final News news = item.getModelObject();
				item.add(new AttributeModifier("class", true, new AbstractReadOnlyModel&lt;String&gt;() {
					@Override
					public String getObject() {
						return news.getGame().getName().toLowerCase().replace(' ', '-');
					}
				}));
				item.add(new ExternalLink("link", news.getUrl(), news.getTitle())<span class="changed">.setEscapeModelStrings(false)</span>);
				item.add(new Label("page", news.getPage()).setRenderBodyOnly(true));
				item.add(new <span class="changed">MultiLineLabel</span>("description", news.getDescription())<span class="changed">.setEscapeModelStrings(false)</span>);
				item.add(new <span class="changed">DateTimeLabel</span>("date", new PropertyModel&lt;Date&gt;(news, "date")));
			}
		});
	}
}</pre>
<p>Da auch in den Titeln der News mitunter maskiertes HTML vorkommt (wie Anführungszeichen) und diese dargestellt werden sollen, haben wir auch bei den Linktexten die zusätzliche Maskierung entfernt. Außerdem benutzen wir hier ein <code>DateTimeLabel</code> &#8211; das es aber noch nicht gibt. Es ist aber im Nu erstellt. Um Objekte, die kein String sind, in einem Label darzustellen, wird ein <code>Converter</code> aufgerufen, der die Konvertierung vornimmt. Wir müssen also nur einen neuen Converter erzeugen, der die Ausgabe mit Datum und Zeit versieht.</p>
<pre><span class="javadoc comment">/**
 * Label, that displays Date objects with date and time.
 *
 * @author ex-ratt
 */</span>
public class DateTimeLabel extends Label {

	public DateTimeLabel(String id, IModel&lt;Date&gt; model) {
		super(id, model);
	}

	@SuppressWarnings("unchecked")
	@Override
	public &lt;X&gt; IConverter&lt;X&gt; getConverter(Class&lt;X&gt; type) {
		return (IConverter&lt;X&gt;) new DateConverter() {
			@Override
			public DateFormat getDateFormat(Locale locale) {
				if (locale == null){
					locale = Locale.getDefault();
				}
				return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale);
			}
		};
	}
}</pre>
<p>Jetzt sehen die News schon sehr viel übersichtlicher aus. Aber hier ist Vorsicht geboten: Sollte in einer der News mal schädlicher JavaScript-Code stehen, so landet dieser jetzt auch in unserer Übersicht. Außerdem werden z.T. auch direkt Farben im Code definiert, die mitunter in unserer Seite absolut unpassend aussehen oder sich kaum lesen lassen. Zu Demonstrationszwecken ist die abgeschaltete Maskierung also tauglich, im Produktiveinsatz sollte man sich da vielleicht eine eigene Komponente schreiben, die das <code>MultiLineLabel</code> ersetzt &#8211; vielleicht tu ich das irgendwann einmal.</p>
<p><a href='http://www.rattlab.net/wp-content/uploads/feedreader.jpg'><img src="http://www.rattlab.net/wp-content/uploads/feedreader-300x246.jpg" alt="Wicket Feedreader" title="Wicket Feedreader" width="300" height="246" class="aligncenter size-medium wp-image-33" /></a></p>
<p>Den bisher erzeugten Quellcode könnt ihr euch <a href="http://www.rattlab.net/wp-content/uploads/feedreader-1.zip">herunterladen</a>. Mit <code>mvn install</code> werden dann die Abhängigkeiten heruntergeladen, falls das noch nicht geschehen ist, und der Code kompiliert. Wie ihr das Projekt nach Eclipse importiert und ausführt, wurde bereits beschrieben. Für IntelliJ Idea müsst ihr <code>mvn idea:idea</code> ausführen, im Falle von Netbeans reicht es, nur die pom.xml zu öffnen.</p>
<h3>Ausblick</h3>
<p>Auch wenn unser Feedreader jetzt schon schick aussieht, so gibt es doch noch einiges an Optimierungspotential. Die Seite wird recht lang und die einzelnen Feeds sind recht unterschiedlich &#8211; so stehen in manchen nur kurze Beschreibungen, während in anderen die komplette News zu finden ist. Das kostet etwas Übersicht, wenn man nur die Links haben will, weil man die News ohnehin auf seiner Lieblingsseite lesen will. Dagegen werden wir im nächsten Teil etwas tun und dann kommt auch endlich etwas Dynamik ins Spiel. Ich freue mich schon darauf, etwas mit JavaScript und Ajax experimentieren zu können.</p>
<p>Den zweiten Teil gibt es jetzt: <a href="http://www.rattlab.net/2008/09/dynamik-fur-den-feedreader/">Dynamik für den Feedreader</a>.</p>
]]></content:encoded>
			<wfw:commentRss>http://rattlab.net/2008/09/wicket-feedreader/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Cygwin</title>
		<link>http://rattlab.net/2008/09/cygwin/</link>
		<comments>http://rattlab.net/2008/09/cygwin/#comments</comments>
		<pubDate>Sun, 07 Sep 2008 13:58:43 +0000</pubDate>
		<dc:creator>Peter</dc:creator>
				<category><![CDATA[Coding]]></category>

		<guid isPermaLink="false">http://www.rattlab.net/?p=29</guid>
		<description><![CDATA[Es gibt immer wieder Momente, da denke ich, dass es ganz angenehm wäre, wenn ich unter Linux entwickeln würde und nicht unter Windows. Einer dieser Momente ist es, wenn ich mit der Kommandozeile von Windows arbeiten muss, da die Vervollständigung von Pfadnamen per Tab recht bescheiden ist, mir die Linux-Befehle etwas geläufiger sind und weil unter Windows kaum sinnvolle Tools wie Subversion oder ähnliches vorinstalliert sind. Allerdings kenne ich jetzt eine Alternative zur parallelen Linux-Installation.]]></description>
			<content:encoded><![CDATA[<p>Es gibt immer wieder Momente, da denke ich, dass es ganz angenehm wäre, wenn ich unter Linux entwickeln würde und nicht unter Windows. Einer dieser Momente ist es, wenn ich mit der Kommandozeile von Windows arbeiten muss, da die Vervollständigung von Pfadnamen per Tab recht bescheiden ist, mir die Linux-Befehle etwas geläufiger sind und weil unter Windows kaum sinnvolle Tools wie Subversion oder ähnliches vorinstalliert sind. Allerdings kenne ich jetzt eine Alternative zur parallelen Linux-Installation.<span id="more-29"></span></p>
<p>Kennengelernt habe ich dieses praktische Stück Software diese Woche in meinem Praktikum. <a href="http://www.cygwin.com/">Cygwin</a> heißt das gute Stück und es stellt eine <a href="http://de.wikipedia.org/wiki/Bash#Die_Bourne-Again-Shell">Bash</a> dar, die direkt unter Windows ausgeführt werden kann. Dabei emuliert Cygwin die Unix-API, was es ermöglicht, nach Windows portierte Unix-Anwendungen zu benutzen.</p>
<p>Um das Programm zu installieren, muss zuerst die recht kleine <a href="http://www.cygwin.com/setup.exe">setup.exe</a> heruntergeladen werden. Ein Ausführen dieser Datei startet erwartungsgemäß die Installation. In dieser wählt man dann aus, ob man direkt installieren oder die benötigten Dateien nur herunterladen will, um sie später zu installieren (recht praktisch, wenn man Cygwin auf mehreren Rechnern installieren will, die teilweise auch nicht ans Internet angeschlossen sind). Für eine einzelne Installation aber reicht es auch aus, direkt zu installieren.</p>
<p>Im nächsten Schritt wird abgefragt, wohin Cygwin installiert werden soll, ob es für alle Nutzer verfügbar sein soll und welches Format Textdateien haben sollen. Hier lasse ich alles auf den Voreinstellungen. Daraufhin wird das Verzeichnis erfragt, in dem die Installationsdateien hinterlegt werden sollen. Darauf kann man für spätere Installationen wieder zurückgreifen.</p>
<p>Zuletzt muss man noch angeben, ob eine direkte Internetverbindung besteht oder ein Proxy nötig ist, bevor man schließlich noch einen Mirror auswählt. Dabei kann es sicher nicht schaden, einen Server zu wählen, der in der Nähe ist. Wenn das geschehen ist, sieht man eine Baumansicht aus verfügbaren Modulen. Dort finden sich viele nützliche Programme, wie das anfangs erwähnte Subversion oder der Kommandozeilen-Editor vi. Dann beginnt der Download und ggf. die Installation.</p>
<p>Man kann seine Installation relativ einfach updaten, indem man die setup.exe ein weiteres Mal ausführt. Die bereits installierten Pakete sind dann schon angewählt und man kann weitere hinzufügen. Nach dem Start von Cygwin wird man darauf hingewiesen, wo man .bashrc und co. finden kann, nämlich unterhalb von <code class="path">/home/username</code>. Die drei nach dem Start genannten Dateien werden nicht vom Programm überschrieben und können, wie unter jedem Linux-System, für persönliche Einstellungen genutzt werden. Die eigenen Partitionen bzw. Festplatten findet man übrigens unter <code class="path">/cygdrive/laufwerksbuchstabe</code>.</p>
<p>Eine Empfehlung meinerseits: Wenn ihr Cygwin geöffnet habt, dann macht mal einen Rechtsklick auf die Titelleiste und wählt &#8220;Eigenschaften&#8221; bzw. &#8220;Properties&#8221; aus. Dort könnt ihr im ersten Reiter &#8220;Options&#8221; den &#8220;Quick Edit Mode&#8221; anwählen, durch den es möglich wird, mit der Maus Bereiche zu markieren, um sie z.B. in die Zwischenablage zu kopieren. Unter dem Reiter &#8220;Layout&#8221; könnt ihr dann noch die Größe des Fensters verändern und, was möglicherweise wichtiger ist, es lässt sich die Anzahl der gespeicherten Zeilen erhöhen, falls man mal weit zurückscrollen muss. Bei mir steht der Wert auf 9999. Diese Einstellungen kann man übrigens auch auf die normale Windows-Kommandozeile anwenden.</p>
<p>Ein <a href="http://www.tanmar.info/content/view/24/47/">gutes Tutorial</a> beschreibt die Installation etwas ausführlicher mit Bildern und geht anschließend auf die Konfiguration von X Windows ein. So ist es möglich, Unix-Programme mit GUI unter Windows auszuführen. Zu guter Letzt wird noch beschrieben, wie man sich per SSH bzw. Telnet mit einem anderen Rechner verbindet.</p>
<p>Abschließend möchte ich nochmal die Vorzüge von Cygwin zusammenfassen:</p>
<ol>
<li>Eine vernünftige Kommandozeile</li>
<li>Unix-Kommandos</li>
<li>Shell-Scripte</li>
<li>Herunterladen von Tools über Cygwin, ohne nach Downloads suchen zu müssen</li>
<li>Ein wenig Linux-Flair, ohne Linux tatsächlich installieren zu müssen</li>
</ol>
]]></content:encoded>
			<wfw:commentRss>http://rattlab.net/2008/09/cygwin/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Wicket-Projekt mit Maven</title>
		<link>http://rattlab.net/2008/08/wicket-projekt-mit-maven/</link>
		<comments>http://rattlab.net/2008/08/wicket-projekt-mit-maven/#comments</comments>
		<pubDate>Fri, 29 Aug 2008 11:55:23 +0000</pubDate>
		<dc:creator>Peter</dc:creator>
				<category><![CDATA[Coding]]></category>

		<guid isPermaLink="false">http://www.rattlab.net/?p=18</guid>
		<description><![CDATA[Es gibt einige Technologien und Frameworks aus dem Java-Umfeld, mit denen ich mich immer mal beschäftigen wollte, wozu ich aber aus Zeitgründen oder mangels Ziel, auf das ich hinarbeiten konnte, nie kam. Und da ich, bevor ich mit meine Diplomarbeit anfange, noch ein wenig Zeit habe, werde ich die Gelegenheit beim Schopfe packen und mich mit Maven, Wicket, Hibernate und anderen Dingen auseinandersetzen.]]></description>
			<content:encoded><![CDATA[<p>Es gibt einige Technologien und Frameworks aus dem Java-Umfeld, mit denen ich mich immer mal beschäftigen wollte, wozu ich aber aus Zeitgründen oder mangels Ziel, auf das ich hinarbeiten konnte, nie kam. Und da ich, bevor ich mit meine Diplomarbeit anfange, noch ein wenig Zeit habe, werde ich die Gelegenheit beim Schopfe packen und mich mit <a href="http://maven.apache.org/">Maven</a>, <a href="http://wicket.apache.org/">Wicket</a>, <a href="http://www.hibernate.org/">Hibernate</a> und anderen Dingen auseinandersetzen.<span id="more-18"></span></p>
<h3>Einführung in Maven</h3>
<p>Anfangen möchte ich erstmal mit Maven, weil alles andere mehr oder weniger darauf aufbauen wird. Zwar ist es natürlich möglich, Frameworks wie Wicket auch ohne Maven auszuprobieren, aber ich will das gleich miteinander verbinden. Am Ende dieses Artikels werden wir ein Grundgerüst für eine auf Wicket basierende Webanwendung haben.</p>
<p>Maven ist ein Werkzeug, das viele Prozesse vereinfacht. Es lassen sich recht einfach Abhängigkeiten zu anderen Projekten und Frameworks verwalten, es unterstützt automatisiertes Testen und vieles mehr, doch stürzen wir uns direkt ins Getümmel und probieren es aus. Ich setze hier voraus, dass ein halbwegs aktuelles JDK installiert und konfiguriert ist und außerdem Java-Kenntnisse vorhanden sind.</p>
<p>Auf der <a href="http://maven.apache.org/download.html">Maven Download-Seite</a> finden wir die aktuellste Version, in meinem Fall ist das die 2.0.9. Das erhaltene Zip entpacken wir z.B. in das Verzeichnis für Programme, in meinem Fall wäre das C:\Program Files\Maven. Nun müssen wir noch zwei Systemvariablen setzen, zum einen M2_HOME, in der das Installationsverzeichnis eingetragen wird (bei mir wie oben erwähnt C:\Program Files\Maven) und zum anderen muss das bin-Verzeichnis von Maven noch zur PATH-Variable hinzugefügt werden (bei mir C:\Program Files\Maven\bin bzw. %M2_HOME%\bin).</p>
<p>Anschließend können wir die Kommandozeile aufrufen und die Version von Maven mit einem einfachen Kommando erfragen, wenn alles richtig gemacht wurde:</p>
<pre class="shell">$ mvn -v
Maven version: 2.0.9
Java version: 1.6.0_06
OS name: "windows vista" version: "6.0" arch: "x86" Family: "windows"</pre>
<p>Wenn das geklappt hat, kann es weitergehen, wenn nicht, dann müssen die ersten Schritte nochmal überprüft werden. Als nächstes legen wir ein neues Maven-Projekt an &#8211; das ginge sowohl per Hand, als auch per Archetype-Plugin. Wie wir noch sehen werden, ist Maven selbst eine Art Container, bei dem die Funktionalität durch viele Plugins zustande kommt. Wir wechseln per Kommandozeile in das hoffentlich bereits erstellte Verzeichnis, das unser kleines Testprojekt beinhalten soll und erstellen es:</p>
<pre class="shell"><span class="command">$ cd myprojects</span>
<span class="command">$ mvn archetype:create -DgroupId=net.rattlab.test -DartifactId=test -DpackageName=net.rattlab.test</span>
[...]
[INFO] OldArchetype created in dir: C:\myproject\test
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3 secons
[INFO] Finished at: Sun Aug 17 22:01:02 CEST 2008
[INFO] Final Memory: 7M/14M
[INFO] ------------------------------------------------------------------------</pre>
<p>Mit diesem Kommando wird ein sogenanntes Goal aufgerufen, in diesem Fall &#8220;create&#8221; vom PlugIn &#8220;Archetype&#8221;. Dieses erstellt das Grundgerüst für ein neues Projekt und lädt ggf. benötigte Maven-PlugIns herunter (in diesem Fall das angesprochene Archetype-PlugIn) &#8211; darum müssen wir uns nicht selber kümmern. Die übergebenen Parameter sind im Falle von groupId und artifactId Bezeichner, um das Projekt eindeutig identifizieren zu können und ein Package-Name für die zu erzeugende Java-Klasse, die nichts weiter tut, als &#8220;Hello World&#8221; auszugeben.</p>
<p>Doch schauen wir uns erstmal an, was im Projekt-Verzeichnis zu finden ist. Dort gibt es neben einem Verzeichnis für die Source-Code eine Datei namens pom.xml, in der die Konfiguration des Projekts zu finden ist. Diese Datei und die Verzeichnisstruktur hätte man auch selber per Hand erstellen können &#8211; das Archetype-Plugin also nimmt uns nur etwas Arbeit ab, wirklich nötig wäre es in diesem Fall aber nicht. Doch zurück zur pom.xml:</p>
<pre>&lt;project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"&gt;
  &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;
  &lt;groupId&gt;net.rattlab.test&lt;/groupId&gt;
  &lt;artifactId&gt;test&lt;/artifactId&gt;
  &lt;packaging&gt;jar&lt;/packaging&gt;
  &lt;version&gt;1.0-SNAPSHOT&lt;/version&gt;
  &lt;name&gt;test&lt;/name&gt;
  &lt;url&gt;http://maven.apache.org&lt;/url&gt;
  &lt;dependencies&gt;
    &lt;dependency&gt;
      &lt;groupId&gt;junit&lt;/groupId&gt;
      &lt;artifactId&gt;junit&lt;/artifactId&gt;
      &lt;version&gt;3.8.1&lt;/version&gt;
      &lt;scope&gt;test&lt;/scope&gt;
    &lt;/dependency&gt;
  &lt;/dependencies&gt;
&lt;/project&gt;</pre>
<p>Hier tauchen die als Argument übergebenen artifactId und groupId wieder auf, begleitet von einer Version &#8211; diese drei Parameter identifizieren den konkreten Stand eines Projekts. Weiterhin interessant ist der Teil, der die Abhängigkeiten (Dependencies) definiert &#8211; in diesem Falle ist standardmäßig JUnit, ein Testing-Framework, integriert. Mehr ist hier nicht nötig, da die weitere Verzeichnisstruktur dem Maven-Standard entspricht und damit nicht konfiguriert werden muss. Möchte man diese aber ändern, so sind natürlich auch mehr Zeilen in der pom.xml nötig.</p>
<p>Jetzt wollen wir das Projekt aber noch hübsch in ein JAR verpacken und anschließend ausführen. Dazu wechseln wir ins Projekt-Verzeichnis und führen eine nächste Anweisung aus, die diesmal aber nicht nur ein Goal enthält, sondern einen Lifecycle anstößt &#8211; dabei werden mehrere Goals ausgeführt und, falls nötig, die als Abhängigkeiten definierten JARs runtergeladen und im lokalen Repository gespeichert.</p>
<pre class="shell"><span class="command">cd test</span>
<span class="command">$ mvn install</span>
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Building test
[INFO]    task-segment: [install]
[INFO] ------------------------------------------------------------------------
[INFO] [resources:resources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:compile]
[INFO] Compiling 1 source file to C:\myprojects\test\target\classes
[INFO] [resources:testResources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:testCompile]
[INFO] Compiling 1 source file to C:\myprojects\test\target\test-classes
[INFO] [surefire:test]
[INFO] Surefire report directory: C:\myprojects\test\target\surefire-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running net.rattlab.AppTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.016 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO] [jar:jar]
[INFO] Building jar: C:\myprojects\test\target\test-1.0-SNAPSHOT.jar
[INFO] [install:install]
[INFO] Installing C:\myprojects\test\target\test-1.0-SNAPSHOT.jar to
C:\Users\ex-ratt\.m2\repository\net\rattlab\test\test\1.0-SNAPSHOT\test-1.0-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3 seconds
[INFO] Finished at: Sun Aug 17 22:32:15 CEST 2008
[INFO] Final Memory: 8M/16M
[INFO] ------------------------------------------------------------------------</pre>
<p>Das Log verrät uns, dass automatisch Tests ausgeführt wurden, der Quellcode kompiliert, ein JAR erstellt und anschließend ins lokale Maven-Repository kopiert wurde &#8211; und das alles nur mit einem Befehl. Jetzt schließen wir noch einen kleinen Funktionstest an:</p>
<pre><span class="command">$ java -cp target/test-1.0-SNAPSHOT.jar net.rattlab.test.App</span>
Hello World!</pre>
<p>Das sollte jetzt nur als klitzekleiner Einstieg in Maven dienen, da ich nun damit arbeiten werde. Weitergehende Informationen zu diesem nützlichen Werkzeug finden sich in Sonatypes <a href="http://www.sonatype.com/community/definitive_guide.html">Maven: The Definitive Guide</a>, mit dem ich selber den Einstieg vollzogen habe &#8211; meinem kleinen Beispiel sieht man das auch an, wenn man es mit dem ersten Beispiel des Buches vergleicht. Allerdings handelt es sich dabei eben auch um das typische Einstiegsbeispiel, auch auf der Webseite von Maven wird es im Guide <a href="http://maven.apache.org/guides/getting-started/maven-in-five-minutes.html">Maven in 5 Minutes</a> behandelt.</p>
<h3>Ein erstes Wicket-Projekt</h3>
<p>Jetzt erstellen wir ein weiteres kleines Projekt, das ebenfalls nur eine kleine Meldung ausgiebt, diesmal aber im Browser. Wie anfangs erwähnt wollen wir damit unsere ersten kleinen Schritte mit dem Web-Framework Wicket wagen. Auf der Webseite von Apache Wicket gibt es eine <a href="http://wicket.apache.org/quickstart.html">QuickStart-Anleitung</a>, in der praktischerweise schon das Maven-Kommando steht, das wir an unsere Bedürfnisse anpassen:</p>
<pre class="shell"><span class="command">$ mvn archetype:create -DarchetypeGroupId=org.apache.wicket -DarchetypeArtifactId=wicket-archetype-quickstart -DarchetypeVersion=1.3.4 -DgroupId=net.rattlab -DartifactId=wickettest</span>
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'archetype'.
[INFO] ------------------------------------------------------------------------
[INFO] Building Maven Default Project
[INFO]    task-segment: [archetype:create] (aggregator-style)
[INFO] ------------------------------------------------------------------------
[INFO] Setting property: classpath.resource.loader.class =&gt; 'org.codehaus.plexus.velocity.ContextClassLoaderResourceLoader'.
[INFO] Setting property: velocimacro.messages.on =&gt; 'false'.
[INFO] Setting property: resource.loader =&gt; 'classpath'.
[INFO] Setting property: resource.manager.logwhenfound =&gt; 'false'.
[INFO] [archetype:create] [WARNING] This goal is deprecated. Please use mvn archetype:generate instead
[INFO] Defaulting package to group ID: net.rattlab
Downloading: http://repo1.maven.org/maven2/org/apache/wicket/wicket-archetype-quickstart/1.3.4/wicket-archetype-quickstart-1.3.4.jar
4/12K
7/12K
11/12K
12/12K
12K downloaded
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating OldArchetype: wicket-archetype-quickstart:1.3.4
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: net.rattlab
[INFO] Parameter: packageName, Value: net.rattlab
[INFO] Parameter: package, Value: net.rattlab
[INFO] Parameter: artifactId, Value: wickettest
[INFO] Parameter: basedir, Value: C:\myprojects
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] ********************* End of debug info from resources from generated POM ***********************
[INFO] OldArchetype created in dir: C:\myprojects\wickettest
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3 seconds
[INFO] Finished at: Mon Aug 18 16:32:00 CEST 2008
[INFO] Final Memory: 7M/14M
[INFO] ------------------------------------------------------------------------</pre>
<p>Hier geben wir zusätzlich einen konkreten Archetype an, anhand dessen dann das Projekt erstellt wird. Dieser Archetype wird, genauso wie andere Projekte, ebenfalls mittels einer GroupId und einer ArtifactId identifiziert. Abseits davon, dass hier der Wicket-Archetype geladen und das Projekt erstellt wurde, fällt mir jetzt erst auf, dass archetype:create offenbar veraltet ist und stattdessen archetype:generate benutzt werden soll. Das sollte ich wohl demnächst mal ausprobieren.</p>
<p>Die pom.xml unseres Wicket-Testprojekts ist jetzt schon um einiges größer, als die unseres ersten Beispiels. Hinzugekommen sind jede Menge Abhängigkeiten, unter anderem natürlich zu Wicket, aber auch zu zwei Loggern und Jetty, einem Servlet-Container, den wir gleich auch noch nutzen werden.</p>
<pre>&lt;project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"&gt;

	&lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;
	&lt;groupId&gt;net.rattlab&lt;/groupId&gt;
	&lt;artifactId&gt;wickettest&lt;/artifactId&gt;
	&lt;packaging&gt;war&lt;/packaging&gt;
	&lt;version&gt;1.0-SNAPSHOT&lt;/version&gt;
	&lt;!-- TODO project name  --&gt;
	&lt;name&gt;quickstart&lt;/name&gt;
	&lt;description&gt;&lt;/description&gt;

	&lt;!-- TODO
		&lt;organization&gt;
		&lt;name&gt;company name&lt;/name&gt;
		&lt;url&gt;company url&lt;/url&gt;
		&lt;/organization&gt;
	--&gt;

	&lt;licenses&gt;
		&lt;license&gt;
			&lt;name&gt;The Apache Software License, Version 2.0&lt;/name&gt;
			&lt;url&gt;http://www.apache.org/licenses/LICENSE-2.0.txt&lt;/url&gt;
			&lt;distribution&gt;repo&lt;/distribution&gt;
		&lt;/license&gt;
	&lt;/licenses&gt;

	&lt;dependencies&gt;
		&lt;!--  WICKET DEPENDENCIES --&gt;

		&lt;dependency&gt;
			&lt;groupId&gt;org.apache.wicket&lt;/groupId&gt;
			&lt;artifactId&gt;wicket&lt;/artifactId&gt;
			&lt;version&gt;${wicket.version}&lt;/version&gt;
		&lt;/dependency&gt;
		&lt;!-- OPTIONAL
			&lt;dependency&gt;
			&lt;groupId&gt;org.apache.wicket&lt;/groupId&gt;
			&lt;artifactId&gt;wicket-extensions&lt;/artifactId&gt;
			&lt;version&gt;${wicket.version}&lt;/version&gt;
			&lt;/dependency&gt;
		--&gt;

		&lt;!-- LOGGING DEPENDENCIES - LOG4J --&gt;

		&lt;dependency&gt;
			&lt;groupId&gt;org.slf4j&lt;/groupId&gt;
			&lt;artifactId&gt;slf4j-log4j12&lt;/artifactId&gt;
			&lt;version&gt;1.4.2&lt;/version&gt;
		&lt;/dependency&gt;
		&lt;dependency&gt;
			&lt;groupId&gt;log4j&lt;/groupId&gt;
			&lt;artifactId&gt;log4j&lt;/artifactId&gt;
			&lt;version&gt;1.2.14&lt;/version&gt;
		&lt;/dependency&gt;

		&lt;!--  JUNIT DEPENDENCY FOR TESTING --&gt;
		 &lt;dependency&gt;
				 &lt;groupId&gt;junit&lt;/groupId&gt;
				 &lt;artifactId&gt;junit&lt;/artifactId&gt;
				 &lt;version&gt;3.8.2&lt;/version&gt;
				 &lt;scope&gt;test&lt;/scope&gt;
		 &lt;/dependency&gt;

		&lt;!--  JETTY DEPENDENCIES FOR TESTING  --&gt;

		&lt;dependency&gt;
			&lt;groupId&gt;org.mortbay.jetty&lt;/groupId&gt;
			&lt;artifactId&gt;jetty&lt;/artifactId&gt;
			&lt;version&gt;${jetty.version}&lt;/version&gt;
			&lt;scope&gt;provided&lt;/scope&gt;
		&lt;/dependency&gt;
		&lt;dependency&gt;
			&lt;groupId&gt;org.mortbay.jetty&lt;/groupId&gt;
			&lt;artifactId&gt;jetty-util&lt;/artifactId&gt;
			&lt;version&gt;${jetty.version}&lt;/version&gt;
			&lt;scope&gt;provided&lt;/scope&gt;
		&lt;/dependency&gt;
		&lt;dependency&gt;
			&lt;groupId&gt;org.mortbay.jetty&lt;/groupId&gt;
			&lt;artifactId&gt;jetty-management&lt;/artifactId&gt;
			&lt;version&gt;${jetty.version}&lt;/version&gt;
			&lt;scope&gt;provided&lt;/scope&gt;
		&lt;/dependency&gt;
	&lt;/dependencies&gt;

	&lt;build&gt;
		&lt;resources&gt;
			&lt;resource&gt;
				&lt;filtering&gt;false&lt;/filtering&gt;
				&lt;directory&gt;src/main/resources&lt;/directory&gt;
			&lt;/resource&gt;
			&lt;resource&gt;
				&lt;filtering&gt;false&lt;/filtering&gt;
				&lt;directory&gt;src/main/java&lt;/directory&gt;
				&lt;includes&gt;
					&lt;include&gt;**&lt;/include&gt;
				&lt;/includes&gt;
				&lt;excludes&gt;
					&lt;exclude&gt;**/*.java&lt;/exclude&gt;
				&lt;/excludes&gt;
			&lt;/resource&gt;
		&lt;/resources&gt;
		&lt;testResources&gt;
			&lt;testResource&gt;
				&lt;filtering&gt;false&lt;/filtering&gt;
				&lt;directory&gt;src/test/java&lt;/directory&gt;
				&lt;includes&gt;
					&lt;include&gt;**&lt;/include&gt;
				&lt;/includes&gt;
				&lt;excludes&gt;
					&lt;exclude&gt;**/*.java&lt;/exclude&gt;
				&lt;/excludes&gt;
			&lt;/testResource&gt;
		&lt;/testResources&gt;
		&lt;plugins&gt;
			&lt;plugin&gt;
				&lt;groupId&gt;org.mortbay.jetty&lt;/groupId&gt;
				&lt;artifactId&gt;maven-jetty-plugin&lt;/artifactId&gt;
			&lt;/plugin&gt;
			&lt;plugin&gt;
				&lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
				&lt;artifactId&gt;maven-eclipse-plugin&lt;/artifactId&gt;
				&lt;configuration&gt;
					&lt;downloadSources&gt;true&lt;/downloadSources&gt;
				&lt;/configuration&gt;
			&lt;/plugin&gt;
		&lt;/plugins&gt;
	&lt;/build&gt;

	&lt;properties&gt;
		&lt;wicket.version&gt;1.3.4&lt;/wicket.version&gt;
		&lt;jetty.version&gt;6.1.4&lt;/jetty.version&gt;
	&lt;/properties&gt;

&lt;/project&gt;</pre>
<p>Weiterhin fällt auf, dass die Versionen von Wicket und Jetty ausgelagert wurden. Ganz unten in der Datei sieht man die Abschnitt &#8220;properties&#8221; und das ist in diesem Fall deswegen nützlich, weil die gleichen Versionsnummern mehrmals gebraucht werden. Außerdem werden noch weitere Dinge konfiguriert, auf die ich aktuell nicht eingehen möchte &#8211; Einzelheiten zur pom.xml findet man im weiter oben verlinkten Buch.</p>
<p>Wir wollen uns jetzt mit dem aus meiner Sicht interessanterem Teil beschäftigen, nämlich der eben erstellten Wicket-Anwendung. Im Verzeichnis C:\myprojects\wickettest\src\main\net\rattlab (mag bei euch anders lauten) liegen drei Dateien, zwei Java-Dateien und eine im HTML-Format. Letztere beinhält eine einfache Webseite:</p>
<pre>&lt;html&gt;
    &lt;head&gt;
        &lt;title&gt;Wicket Quickstart Archetype Homepage&lt;/title&gt;
    &lt;/head&gt;
    &lt;body&gt;
        &lt;strong&gt;Wicket Quickstart Archetype Homepage&lt;/strong&gt;
        &lt;br/&gt;&lt;br/&gt;
        &lt;span wicket:id="message"&gt;message will be here&lt;/span&gt;
    &lt;/body&gt;
&lt;/html&gt;</pre>
<p>Das sieht erstmal recht normal aus, nur das Attribut wicket:id wird dem HTML-Kenner neu sein. Über dieses Attribut spricht Wicket bestimmte Elemente im Code an, wie wir in der zu dieser Seite gehörenden Java-Klasse HomePage.java sehen:</p>
<pre>package net.rattlab;

import org.apache.wicket.PageParameters;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.WebPage;

/**
 * Homepage
 */
public class HomePage extends WebPage {

	private static final long serialVersionUID = 1L;

	// TODO Add any page properties or variables here

    /**
	 * Constructor that is invoked when page is invoked without a session.
	 *
	 * @param parameters
	 *            Page parameters 	 */
    public HomePage(final PageParameters parameters) {

        // Add the simplest type of label
        add(new Label("message", "If you see this message wicket is properly configured and running"));

        // TODO Add your page's components here
    }
}</pre>
<p>Dieser Webseite wird dein Label mit dem Text &#8220;If you see this message wicket is properly configured and running&#8221; zugewiesen. Dieses Label hat eine id namens &#8220;message&#8221;, die dem Wert des wicket:id Attributs im HTML-Code gleicht. Wie wir gleich im Browser sehen werden, wird der Text im span-Element durch den des Labels ersetzt.</p>
<p>Die letzte Datei schließlich ist die WicketApplication.java, in der die Webanwendung konfiguriert wird. In unserem einfachen Beispiel wird nur die Einstiegsseite angegeben, die aufgerufen werden soll, wenn kein spezieller Pfad in der Adresszeile angegeben wird:</p>
<pre>package net.rattlab;

import org.apache.wicket.protocol.http.WebApplication;

/**
 * Application object for your web application. If you want to run this application without deploying, run the Start class.
 *
 * @see wicket.myproject.Start#main(String[])
 */
public class WicketApplication extends WebApplication
{
    /**
     * Constructor
     */
	public WicketApplication()
	{
	}

	/**
	 * @see wicket.Application#getHomePage()
	 */
	public Class getHomePage()
	{
		return HomePage.class;
	}

}</pre>
<p>Nun wollen wir den Kram aber auch mal ausprobieren. Dafür nutzen wir das Jetty-PlugIn, das bereits in der pom.xml konfiguriert wurde, wechseln ins Projektverzeichnis und führen es aus:</p>
<pre class="shell"><span class="command">$ cd wickettest</span>
<span class="command">$ mvn jetty:run</span>
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'jetty'.
[INFO] ------------------------------------------------------------------------
[INFO] Building quickstart
[INFO]    task-segment: [jetty:run]
[INFO] ------------------------------------------------------------------------
[INFO] Preparing jetty:run
[INFO] [resources:resources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:compile]
[INFO] Nothing to compile - all classes are up to date
[INFO] [resources:testResources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:testCompile]
[INFO] Nothing to compile - all classes are up to date
[INFO] [jetty:run]
[INFO] Configuring Jetty for project: quickstart
[INFO] Webapp source directory = C:\myprojects\wickettest\src\main\webapp
[INFO] web.xml file = C:\myprojects\wickettest\src\main\webapp\WEB-INF\web.xml
[INFO] Classes = C:\myprojects\wickettest\target\classes
[INFO] Context path = /wickettest
[INFO] Tmp directory =  determined at runtime
[INFO] Web defaults = org/mortbay/jetty/webapp/webdefault.xml
[INFO] Web overrides =  none
[INFO] Webapp directory = C:\myprojects\wickettest\src\main\webapp
[INFO] Starting jetty 6.1.12rc1 ...
INFO  - Application                - [WicketApplication] init: Wicket core library initializer
INFO  - RequestListenerInterface   - registered listener interface [RequestListenerInterface name=IBehaviorListener, method=public abstract void org.apache.wicket.behavior.IBehaviorListener.onRequest()]
INFO  - RequestListenerInterface   - registered listener interface [RequestListenerInterface name=IBehaviorListener, method=public abstract void org.apache.wicket.behavior.IBehaviorListener.onRequest()]
INFO  - RequestListenerInterface   - registered listener interface [RequestListenerInterface name=IFormSubmitListener, method=public abstract void org.apache.wicket.markup.html.form.IFormSubmitListener.onFormSubmitted()]
INFO  - RequestListenerInterface   - registered listener interface [RequestListenerInterface name=IFormSubmitListener, method=public abstract void org.apache.wicket.markup.html.form.IFormSubmitListener.onFormSubmitted()]
INFO  - RequestListenerInterface   - registered listener interface [RequestListenerInterface name=ILinkListener, method=public abstract void org.apache.wicket.markup.html.link.ILinkListener.onLinkClicked()]
INFO  - RequestListenerInterface   - registered listener interface [RequestListenerInterface name=ILinkListener, method=public abstract void org.apache.wicket.markup.html.link.ILinkListener.onLinkClicked()]
INFO  - RequestListenerInterface   - registered listener interface [RequestListenerInterface name=IOnChangeListener, method=public abstract void org.apache.wicket.markup.html.form.IOnChangeListener.onSelectionChanged()]
INFO  - RequestListenerInterface   - registered listener interface [RequestListenerInterface name=IOnChangeListener, method=public abstract void org.apache.wicket.markup.html.form.IOnChangeListener.onSelectionChanged()]
INFO  - RequestListenerInterface   - registered listener interface [RequestListenerInterface name=IRedirectListener, method=public abstract void org.apache.wicket.IRedirectListener.onRedirect()]
INFO  - RequestListenerInterface   - registered listener interface [RequestListenerInterface name=IRedirectListener, method=public abstract void org.apache.wicket.IRedirectListener.onRedirect()]
INFO  - RequestListenerInterface   - registered listener interface [RequestListenerInterface name=IResourceListener, method=public abstract void org.apache.wicket.IResourceListener.onResourceRequested()]
INFO  - RequestListenerInterface   - registered listener interface [RequestListenerInterface name=IResourceListener, method=public abstract void org.apache.wicket.IResourceListener.onResourceRequested()]
INFO  - RequestListenerInterface   - registered listener interface [RequestListenerInterface name=IActivePageBehaviorListener, method=public abstract void org.apache.wicket.behavior.IBehaviorListener.onRequest()]
INFO  - RequestListenerInterface   - registered listener interface [RequestListenerInterface name=IActivePageBehaviorListener, method=public abstract void org.apache.wicket.behavior.IBehaviorListener.onRequest()]
INFO  - WebApplication             - [WicketApplication] Started Wicket version 1.3.4 in development mode
[INFO] Started Jetty Server</pre>
<p>Die hier gezeigt Ausgabe wird vermutlich nicht alles sein, da Wicket in unserem Fall die Konsole gleichzeitig für das eigene Log nutzt und unter anderem davor warnt, dass es im Entwicklungs-Modus läuft. Jetzt können wir jedenfalls einen Browser unserer Wahl aufrufen und in die Adressezeile http://localhost:8080/wickettest/ eingeben und wenn alles funktioniert hat, dann erscheint dort jetzt die einfache Seite mit dem Label-Text. Um den Server zu beenden, drücken wir im Eingabefenster STRG+C, dann fährt er sich herunter.</p>
<p>Der Vollständigkeit halber wollen wir uns kurz noch die web.xml ansehen. In dieser Datei wird dem Servlet-Container, in unserem Fall Jetty, mitgeteilt, bei welchem Pfad welches Servlet aufzurufen ist. Ein Servlet ist die ursprüngliche Art und Weise, mit Java eine Webanwendung zu schreiben und darauf basieren auch alle Web-Frameworks, in Berührung damit kommen wir aber kaum. So also sieht die c:\myprojects\wickettest\src\main\webapp\WEB-INF\web.xml (mein lokaler Pfad) aus:</p>
<pre>&lt;?xml version="1.0" encoding="ISO-8859-1"?&gt;
&lt;web-app xmlns="http://java.sun.com/xml/ns/j2ee"
	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	 xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
	 version="2.4"&gt;

	&lt;display-name&gt;wickettest&lt;/display-name&gt;

	 &lt;!--
	      There are three means to configure Wickets configuration mode and they are
	      tested in the order given.
	      1) A system property: -Dwicket.configuration
	      2) servlet specific &lt;init-param&gt;
	      3) context specific &lt;context-param&gt;
	      The value might be either "development" (reloading when templates change)
	      or "deployment". If no configuration is found, "development" is the default.
	--&gt;

	&lt;filter&gt;
		&lt;filter-name&gt;wicket.wickettest&lt;/filter-name&gt;
 		&lt;filter-class&gt;org.apache.wicket.protocol.http.WicketFilter&lt;/filter-class&gt;
		&lt;init-param&gt;
			&lt;param-name&gt;applicationClassName&lt;/param-name&gt;
			&lt;param-value&gt;net.rattlab.WicketApplication&lt;/param-value&gt;
 		&lt;/init-param&gt;
 	&lt;/filter&gt;

 &lt;filter-mapping&gt;
  &lt;filter-name&gt;wicket.wickettest&lt;/filter-name&gt;
	&lt;url-pattern&gt;/*&lt;/url-pattern&gt;
 &lt;/filter-mapping&gt;

&lt;/web-app&gt;</pre>
<p>Ganz unten sehen wir das filter-mapping, dort werden URLs anhand eines Musters (englisch Pattern) einem bestimmten Servlet zugewiesen, das unter filter konfiguriert wird. Jetzt haben wir also ein Grundgerüst, mit dem wir Wicket nach Belieben ausprobieren können.</p>
<h3>Ergänzende Informationen</h3>
<p>Es gibt einen <a href="http://herebebeasties.com/2007-10-07/wicket-quickstart/">Wicket Quickstart Screencast</a> von Alastair Maw, in dem nochmal die wesentlichen Dinge gezeigt werden, die auch in diesem Artikel stehen. Außerdem zeigt er, wie man das Projekt nach Eclipse importiert, um anschließend daran zu arbeiten.</p>
<p>Wer sich mehr Details zum Grundkonzept von Wicket wünscht, der kann sich mal das Probekapitel 1 vom Buch <a href="http://www.manning.com/dashorst/">Wicket in Action</a> herunterladen. Das Buch ist fast erschienen (befindet sich im Druck, als eBook ist es bereits erhältlich) und wurde von zwei Wicket-Entwicklern geschrieben. Neben Kapitel 1 und 8 findet man dort auch ein Bonuskapitel 15, in dem erklärt wird, wie man sein Wicket-Projekt aufsetzen kann. Dabei wird unter anderem auch auf Ant und Maven eingegangen und da es von Hand entwickelt wird, findet man dort auch mehr Erklärungen zu den Elementen der pom.xml und web.xml, als hier bei mir. Lesen lohnt sich also.</p>
<p>Ein weiteres Buch ist <a href="http://www.agileskills2.org/EWDW/">&#8220;Enjoying Web Development with Wicket&#8221;</a>, von dem man sich die ersten drei Kapitel herunterladen kann. Abgesehen davon findet man auf der Seite von <a href="http://wicket.apache.org/">Apache Wicket</a> und auf <a href="http://www.wicket-library.com/">wicket-library.com</a> noch weitere Beispiele und Dokumentationen.</p>
<p>Ich werde meinen Eindruck von Wicket demnächst jedenfalls noch vertiefen und eine erste kleine Beispielanwendung schreiben, die ich hier in Form eines Tutorials veröffentlichen werde.</p>
]]></content:encoded>
			<wfw:commentRss>http://rattlab.net/2008/08/wicket-projekt-mit-maven/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>

