Wicket Feedreader

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.

Wicket Feedreader

Den Grundstein legen

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 Quickstart-Anleitung 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 Cygwin aus – das Einfügen des vorher kopierten Codes geschieht hier mittels Rechtsklick.

$ 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] ------------------------------------------------------------------------

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.

$ cd wicket
$ mvn eclipse:eclipse -DdownloadSources=true

Nach dem Starten von Eclipse importieren wir das Projekt über File > Import > General > Existing Projects into Workspace und dort wählen wir das Projektverzeichnis aus. Ganz so, wie im Screencast von Alastair Maw. Da das Projekt dann noch viele Fehler aufweist, gehen wir in Window > Preferences > Java > Build Path > Classpath Variables, legt dort per New eine neue Variable ein namens M2_REPO an und geben den Pfad auf das lokale Maven Repository ein, in meinem Fall wäre das C:/Users/ex-ratt/.m2/repository.

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 HomePage, nämlich in Zeile 10 und in Zeile 25.

Um zu testen, ob das jetzt auch wirklich geht, führen wir die Klasse Start aus, die im Test-Verzeichnis zu finden ist. Sie startet eine Jetty-Instanz und über die URL http://localhost:8080/ oder http://127.0.0.1:8080/ sieht man die übliche Nachricht „If you see this message wicket is properly configured and running“. Nachdem das geklärt ist, kann es ja endlich losgehen.

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.

public class Game {	
	private String name;
}
public class News {
	private Game game;
	private String page;
	private String url;
	private String title;
	private String description;
	private Date date;
}

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:

<html>
<body>
<wicket:panel>
	<ul>
		<li wicket:id="news">
			<span wicket:id="date">01.01.1970</span>
			<span wicket:id="page">Seite</span>: <a wicket:id="link"></a>
			<div wicket:id="description">Beschreibung</div>
		</li>
	</ul>
</wicket:panel>
</body>
</html>

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 – nämlich so oft, wie es News gibt. Bei Wicket aber wird derartiges komplett im Java-Code gemacht. Das wicket:panel-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 – man könnte sie also auch weglassen.

public class NewsList extends Panel {

	public NewsList(String id, List<News> news) {
		super(id);
		
		add(new ListView<News>("news", news) {
			@Override
			protected void populateItem(ListItem<News> 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<Date>(news, "date")));
			}
		});
	}
}

ListView ist genauso wie Label und ExternalLink eine Komponente, die Wicket von sich aus mitbringt. Dabei muss man immer eine ID angeben, die der im HTML-Code angegebenen wicket:id entsprechen muss. Die ListView ist dabei insofern besonders, als dass sie das entsprechende Tag mitsamt den Kindelementen wiederholt – hier taucht also die Schleife auf.

Die Methode populateItem() wird für jede News aufgerufen, die sich in der Liste befindet, die dem Konstruktor übergeben wurde. Jedem ListItem 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 String 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 toString umgewandelt werden.

Weiterhin fällt der Aufruf von setRenderBodyOnly() bei dem Label, das den Namen der Seite enthält, auf. Das sorgt dafür, dass das span-Tag im HTML-Code nicht dargestellt wird, sondern nur der Name der Seite. Dazu können wir uns hinterher mal den erzeugten Quellcode ansehen.

Die Komponente sollte soweit funktionieren, jetzt müssen wir sie nur noch verwenden und mit Daten füttern. Dazu ändern wir die bereits bestehende HomePage.java an unsere Bedürfnisse an:

public class HomePage extends WebPage {

	private static final long serialVersionUID = 1L;

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

		Game game = new Game("Spiel 1");
		List<News> news = new LinkedList<News>();
		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));
	}
}

Hier basteln wir kurz ein paar Testdaten zusammen und fügen unsere NewsList 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 (HomePage.html) muss natürlich auch angepasst werden:

<html>
    <head>
        <title>News Feed Reader</title>
    </head>
    <body>
        <div wicket:id="news"></div>
    </body>
</html>

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 TestHomePage.java, 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.

Würden wir jetzt unsere Anwendung starten, die Seite aufrufen und den Quelltext betrachten, so würden wir feststellen, dass dort noch die wicket:id-Attribute, sowie die <wicket:panel>-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 WicketApplication. Der wesentliche Code ist rot dargestellt:

public class WicketApplication extends WebApplication {

	/**
	 * Constructor
	 */
	public WicketApplication() {}
	
	@Override
	protected void init() {
		super.init();
		getMarkupSettings().setStripWicketTags(true);
	}
	
	/**
	 * @see wicket.Application#getHomePage()
	 */
	public Class<HomePage> getHomePage() {
		return HomePage.class;
	}

}

Nachdem wir den Server gestartet haben und nun die Seite betrachtet, sehen wir folgenden erzeugten HTML-Code:

<html>
    <head>
        <title>News Feed Reader</title>
    </head>
    <body>
        
	<ul>
		<li>
			<span>19.09.08</span>
			Seite1: <a href="http://www.asdf.com/erste-news/">Erste News</a>
			<div>Lorem ipsum bla</div>
		</li><li>
			<span>19.09.08</span>
			Seite1: <a href="http://www.asdf.com/zweite-news/">Zweite News</a>
			<div>Lorem ipsum bla</div>
		</li>
	</ul>

    </body>
</html>

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.

Echte Newsfeeds mit Rome

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 Projekt Rome. 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 pom.xml ein und lassen Maven den Rest erledigen.

<dependency>
	<groupId>rome</groupId>
	<artifactId>rome</artifactId>
	<version>0.9</version>
</dependency>

Nur woher weiß man, welche Projekte im Maven-Repository liegen und wie kommt man an groupId und artifactId? Entweder, diese Informationen findet man auf der Seite des Projekts (wie es bei Wicket der Fall ist) oder man sucht auf www.mvnrepository.com, 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 mvn install aus, damit die neuen Abhängigkeiten heruntergeladen werden.

$ cd /cygdrive/c/myprojects/wicket
$ mvn install

Spätestens jetzt wird uns Maven einen Fehler um die Ohren hauen, wenn wir vorher nicht die TestHomePage.java 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:

$ mvn eclipse:eclipse -DdownloadSources=true

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 WicketApplication 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:

package net.rattlab.wicket;

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;

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;

/**
 * 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[])
 */
public class WicketApplication extends WebApplication {
	
	private List<Game> games;
	private List<News> news;
	
	/**
	 * Constructor
	 */
	public WicketApplication() {
		games = new LinkedList<Game>();
		news = new LinkedList<News>();
		
		createGames();
		createNews();
	}
	
	/**
	 * Creates the games and news feeds.
	 */
	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§ion=345");
		starcraft2.addFeed("StarCraft 2 Mecca", "http://forum.gamersunity.de/external.php?type=rss2&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);
	}
	
	/**
	 * Creates the news from the news feeds.
	 */
	protected void createNews() {
		SyndFeedInput input = new SyndFeedInput();
		for (Game game : games) {
			Map<String , String> 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()) {
						// rome 0.9 does not support generics
						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) {}
			}
		}
		// sort news by date in descending order
		Collections.sort(news, new Comparator<News>() {
			public int compare(News news, News otherNews) {
				if (null == news.getDate() && null == otherNews.getDate()) {
					return 0;
				}
				if (null == news.getDate()) {
					return 1;
				}
				if (null == otherNews.getDate()) {
					return -1;
				}
				return -1 * news.getDate().compareTo(otherNews.getDate());
			}
		});
	}
	
	@Override
	protected void init() {
		super.init();
		getMarkupSettings().setStripWicketTags(true);
	}
	
	public static WicketApplication get() {
		return (WicketApplication) Application.get();
	}
	
	/**
	 * @see org.apache.wicket.Application#getHomePage()
	 */
	public Class<Homepage> getHomePage() {
		return HomePage.class;
	}
	
	public List<News> getNews() {
		return news;
	}

}

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 – wer mehr wissen will, kann sich die Rome Tutorials anschauen. Dass ich die Exceptions einfach ignoriere, ist nicht besonders vorbildlich, aber darum soll es hier nicht primär gehen.

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 null sind, muss hier diese doch recht hässliche Abfrage erfolgen.

Schließlich gibt es noch zwei weitere neue Methoden. Die eine davon gibt einfach nur die Liste der News zurück, die andere das WicketApplication-Objekt selber. Das brauchen wir, um von unserer Komponente aus auf die News zugreifen zu können.

Wie zu sehen ist, hat auch Game ein paar neue Methoden spendiert bekommen, addFeed() und getFeeds. Hier also die überarbeitete Klasse, diesmal mit Getter und Setter:

public class Game {
	
	private String name;
	private Map<String , String> feeds;
	
	public Game(String name) {
		setName(name);
		feeds = new HashMap<String , String>();
	}
	
	public void addFeed(String page, String url) {
		feeds.put(page, url);
	}
	
	public Map<String , String> getFeeds() {
		return feeds;
	}

	public String getName() {
		return name;
	}

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

Nachdem insbesondere in der WicketApplication so viel neuer Code hinzugekommen ist, wurde er bei HomePage weniger, denn dort haben wir vorher unsere Daten erzeugt. Der Konstruktor ist jetzt auf eine Zeile geschrumpft, sonst hat sich nichts geändert:

public class HomePage extends WebPage {

	private static final long serialVersionUID = 1L;

	/**
	 * Constructor that is invoked when page is invoked without a session.
	 * 
	 * @param parameters
	 *            Page parameters
	 */
	public HomePage(final PageParameters parameters) {
	    add(new NewsList("news", WicketApplication.get().getNews()).setRenderBodyOnly(true));
	}
}

Jetzt haben wir „echte“ 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.

Das Styling

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 NewsList.java und NewsList.html liegen. Hier ist also die newslist.css, die ich nicht näher ausführen werde, da ich nicht zu sehr abschweifen will, aber kompliziert ist es ohnehin nicht:

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;
}

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:

<html>
<head>
<wicket:head><wicket:link>
	<link rel="stylesheet" type="text/css" href="newslist.css" />
</wicket:link></wicket:head>
</head>

<body>
<wicket:panel>
	<ul class="newslist">
		<li wicket:id="news">
			<span wicket:id="date" class="date"></span>
			<span wicket:id="page"></span>: <a wicket:id="link"></a>
			<div wicket:id="description" class="description"></div>
		</li>
	</ul>
</wicket:panel>
</body>
</html>

Hier kommen zwei neue Wicket-Tags zum Einsatz, <wicket:head> und <wicket:link>. 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 – das klappt z.B. auch mit a– und img-Tags und ist nicht auf den Header beschränkt. Es ist auch möglich, die CSS-Datei z.B. ins WEB-INF-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.

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 li-Tag gar kein class-Attribut. Wie kommt das also da dran? Viele Varianten gibt es ja nicht – es wird im Java-Code gemacht, und zwar in unserer Komponente:

public class NewsList extends Panel {

	public NewsList(String id, List<News> news) {
		super(id);
		
		add(new ListView<News>("news", news) {
			@Override
			protected void populateItem(ListItem<News> item) {
				final News news = item.getModelObject();
				item.add(new AttributeModifier("class", true, new AbstractReadOnlyModel<String>() {
					@Override
					public String getObject() {
						return news.getGame().getName().toLowerCase().replace(' ', '-');
					}
				}));
				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<Date>(news, "date")));
			}
		});
	}
}

Mit dem AttributeModifier können wir jedes erdenkliche Attribut ändern. In diesem Fall fügen wir ein class-Attribut hinzu, das true 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.

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.

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 MultiLineLabel, das einfache Zeilenumbrüche in ein <br/> und doppelte in </p><p> verwandelt. Außerdem wird der gesamte Text des Labels noch in p-Tags gefasst, so dass es am Ende auch alles passt. Wir ändern unsere Komponente also:

public class NewsList extends Panel {

	public NewsList(String id, List<News> news) {
		super(id);
		
		add(new ListView<News>("news", news) {
			@Override
			protected void populateItem(ListItem<News> item) {
				final News news = item.getModelObject();
				item.add(new AttributeModifier("class", true, new AbstractReadOnlyModel<String>() {
					@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<Date>(news, "date")));
			}
		});
	}
}

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 DateTimeLabel – das es aber noch nicht gibt. Es ist aber im Nu erstellt. Um Objekte, die kein String sind, in einem Label darzustellen, wird ein Converter aufgerufen, der die Konvertierung vornimmt. Wir müssen also nur einen neuen Converter erzeugen, der die Ausgabe mit Datum und Zeit versieht.

/**
 * Label, that displays Date objects with date and time.
 * 
 * @author ex-ratt
 */
public class DateTimeLabel extends Label {

	public DateTimeLabel(String id, IModel<Date> model) {
		super(id, model);
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public <X> IConverter<X> getConverter(Class<X> type) {
		return (IConverter<X>) new DateConverter() {
			@Override
			public DateFormat getDateFormat(Locale locale) {
				if (locale == null){
					locale = Locale.getDefault();
				}
				return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale);
			}
		};
	}
}

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 MultiLineLabel ersetzt – vielleicht tu ich das irgendwann einmal.

Wicket Feedreader

Den bisher erzeugten Quellcode könnt ihr euch herunterladen. Mit mvn install 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 mvn idea:idea ausführen, im Falle von Netbeans reicht es, nur die pom.xml zu öffnen.

Ausblick

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 – 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.

Den zweiten Teil gibt es jetzt: Dynamik für den Feedreader.

Tags:

1 Pingback

  1. Dynamik für den Feedreader - rattlab.net 29. September 2008 um 21:25

Keine Antworten

Hinterlasse eine Antwort