Fragt man einen Webserver nicht nach einer Datei, sondern nach einem Ordner,
so sucht dieser zunächst im angegebenen Ordner nach einer Datei mit Namen
index.html oder index.htm.
Ist eine solche Datei vorhanden, so wird sie als
Antwort zurückgeben. Diese Funktion wird
häufig genutzt, um den Server bei Requests auf die Domäne automatisch
einen Einstiegspunkt auf das gesamte Webangebot finden zu lassen
(eine Internetaddresse www.astrotexte.ch ist ein kleines bisschen einfacher zu
merken als www.astrotexte.ch/index.html und führt bei Eingabe
im Browser zu exakt dem gleichen Ergebnis).
Ist eine solche
Index-Datei dagegen nicht vorhanden, zeigen viele Webserver stattdessen eine Liste
der Dateien und Unterordner des angegebenen Ordners an.
Dieses Verhalten zeigt uns den eigentlichen Zweck der Index-Datei: Indem sie die gewöhnliche Inhaltsanzeige eines Dateisystems durch eine individuelle Variante ersetzt, soll sie dem Administrator ermöglichen, die Dateien des Webangebots übersichtlich und strukturiert darzustellen. Wer neu ist, soll sich leicht einen ersten Überblick über die Inhalte verschaffen können. Wer wiederkommt, will durch eine einfache und übersichtliche Navigation zu den Beiträgen geführt werden, die ihn interessieren.
Beim Standardverhalten, der blossen Anzeige der Dateiliste, kann man es meist schon deshalb nicht belassen, weil nicht alle Dateien, die in einem Ordner aufgeführt sind, die gleiche Wertigkeit haben: Ein HTML-Dokument mag einige Bilder, ein bisschen Javascript und ein Stylesheet enthalten - all diese zusätzlichen Objekte wird man im Index üblicherweise nicht darstellen wollen. Der Index soll also nicht die physische, sondern die logische Dokumentstruktur darstellen: Die Inhalte, nicht die zu deren Präsentation benötigten Ressourcen, sind von Interesse.
Die Präsentation der Daten sollte die gewählte Dokumentstruktur widerspiegeln. Dies geschieht durch Anbringen von Navigationsleisten oder -bäumen. Man sollte bei der Implementierung aber daran denken, dass man möglicherweise einmal Lust hat, die Datenpräsentation zu ändern: Vielleicht will man statt einer Drucktasten- oder Linkleiste auf einen Baum wechseln, oder umgekehrt. Für diesen Fall sollte man von vorneherein vorsorgen, denn der Geschmack wandelt sich! Aber: Wie kann man vorsorgen? Indem man
Die Aufmachung der Daten wird bei diesem Konzept in einem XSLT Stylesheet codiert.
Der Name ''Stylesheet'' für diese Objekte, die eigentlich kleine Programme zum
Transformieren von XML- in HTML-, Text- oder andere XML-Dateien darstellen, ist mehr
als gerechtfertigt, wenn wir an die CSS-Dateien für HTML-Dokumente denken: Auch
diese ''Cascading Stylesheets'' haben bereits den Zweck, den Dokumentinhalt von
seiner Aufmachung zu trennen. Der Dokumentinhalt kann sich bei Verwendung von CSS
auf den eigentlichen Text und auf einige wenige HTML-Element wie die Tabellentags
(<table>, <tr>, <td> usw.),
die Tags für Überschriften (<h1>, <h2> usw.),
Einbinden von Graphiken (<img>), Zeilenumbruch und neue Absätze
(<br> und <p>) beschränken. Der Rest ist im Stylesheet festgelegt.
XSL ist, im Unterschied zu CSS, noch konsequenter und erlaubt die Abkapselung der
gesamten Präsentationslogik vom Dokumentinhalt. Das ist bei der Kombination HTML/CSS nicht möglich.
Denn immerhin sind ja auch
Graphiken eigentlich noch "style", gehören nicht zum Inhalt, sondern zur Veranschaulichung
desselben. Auch die Überschrift-Tags <h1>, <h2> usw. deuten zwar eine rein
logische Funktion an, als würden sie verschiedene Hierarchiestufen von Kapitelüberschriften
kennzeichnen. Sie können aber in einem HTML-Dokument ganz beliebig und bar jeder Ordnung
angewendet werden; häufig werden sie nur verwendet, um eine bestimmte Schriftgrösse
in der Präsentation anzusteuern. Die Überschrift-Tags vermischen also weiterhin
die Präsentationsfunktion mit logischen Funktionen.
Ein XML-Dokument können wir frei von jeder Präsentation am Reissbrett definieren. Wie kann ein solches Dokument für einen Index aussehen? Reduziert auf den eigentlichen Inhalt, frei von jeder Präsentation, ist ein Index im Prinzip eine Liste von Verweisen. Man müsste für jeden Verweis das Sprungziel des Verweises (seine URL), einen Kurztext und eine Beschreibung in wenigen Sätzen angeben können. Ein einzelner Verweis könnte also wie folgt aussehen:
<item> <link href="./angebot1.html">Angebot 1</link> <shorttext>Hier geht es um ... </shorttext> </item>Der gesamte Inhalt des Index könnte als XML-Dokument folgenden Aufbau haben:
<?xml version="1.0" encoding="ISO-8859-1"?>
<index>
<item>
<link href="./angebot1.html">Angebot 1</link>
<shorttext>Hier geht es um ... </shorttext>
</item>
<item>
<link href="./angebot2.html">Angebot 2</link>
<shorttext>Hier geht es um ... </shorttext>
</item>
<item>
<link href="./angebot3.html">Angebot 3</link>
<shorttext>Hier geht es um ... </shorttext>
</item>
...
</index>
Mit einer XML-Datei dieser Art hätten wir die erste Aufgabe erledigt: Ein
präsentationsfreies Verzeichnis unserer Dokumentinhalte zu erstellen.
Allerdings haben wir nur eine "flache Liste" unserer Inhalte. Es fehlt die Hierarchie, die Gliederung nach Thema und Unterthema. Dazu kommen wir noch.
index.xml, so dass die Datei nur bei voller Angabe
der URL zurückgesendet würde;
index.xml nun zurückkommt, so ist nicht gesagt,
ob der Webbrowser des Anwenders etwas mit XML anfangen kann und ob er das
mitgelieferte XSLT-Stylesheet korrekt interpretiert. Im real existierenden
Browserzoo gibt es viele verschiedene XSLT-Prozessoren. Damit wächst die
Wahrscheinlichkeit, dass ein XSLT-Stylesheet unterschiedlich interpretiert wird.
Da es vermutlich nicht mit einer HTML-Datei getan ist, sondern aus einer XML-Datei mehrere Zieldateien zu generieren wären (für jedes Thema, das kein Unterthema besitzt, und für jedes Unterthema jeweils eine eigene Seite), muss man zuerst nach einem geeigneten XSLT-Prozessor Ausschau halten. Ich entschied mich für den in den neueren Java-Versionen eingebauten Xalan-Prozessor. Denn dieser bietet unter anderem die Möglichkeit, mittels sogenannter Element-Extensions auch mehrere Zieldateien zu erzeugen.
<page> und <subpage>
für Haupt- und Unterthemen.
Entsprechend der Gliederung, ziehen wir diese Elementknoten in die oben beschriebene
Liste von <item>s ein.
Sowohl <page> als auch <subpage> können die
Attribute name, title und (optional) titleLong
führen. Der name ist dabei der relative Pfad der zu generierenden
HTML-Seite. title ist ein kurzes beschreibendes Stichwort, wie es als
Aufschrift auf einer Schaltfläche verwendet werden könnte. titleLong
schliesslich ist eine etwas längere Beschreibung der Gruppe - allerdings nicht
ganze Sätze, sondern nur eine Überschrift. Überschriften sind vom Platz her nicht
so eingeschränkt wie die Texte in Navigationsleisten, daher kann titleLong
etwas länger und somit auch etwas spezifischer werden. Der folgende Code skizziert,
wie die Gliederung mit den neuen Elementen <page> und <subpage> gestaltet
wird:
<index>
...
<page name="index_subthema2_1.html" title="Thema 2">
...
<subpage name="index_subthema2_1.html" title="Subthema 2.1" titleLong="Alles über Subthema 2.1">
<item>
</item>
...
</subpage>
<subpage name="index_subthema2_2.html" title="Subthema 2.2" titleLong="Alles über Subthema 2.2">
<item>
</item>
...
</subpage>
...
</page>
...
</index>
Zu beachten ist, dass das Element <page> in diesem Fall keine eigene Zieldatei
trägt, sondern die Zieldatei der ersten <subpage>. Warum? In meinem Konzept ist
die <page>, wenn sie ihrerseits noch <subpage>s enthält, lediglich
eine Gliederungseinheit, trägt aber selbst keine Inhalte. In der Sprache von Dateisystemen ist sie
ein Ordner, der seinerseits nur Ordner enthält. Aber nur solche Ordner, die Dateien als Inhalte haben,
sollen mit einer HTML-Seite dargestellt werden. Ordner, die ihrerseits nur Ordner enthalten,
stellen nichts weiter als einen Zwischenknoten im Navigationsbaum dar.
Dennoch bedeutet es etwas, dass im Beispiel
dem <page>-Element für Thema 2 die Seite des Subthemas 2.1 zugeordnet ist: Das
Subthema 2.1 wird dadurch zum Defaultwert bei Anwahl des Themas 2 erklärt.
Ein <page>-Element kann jedoch auch eine eigene HTML-Seite als Name führen:
Das ist genau dann der Fall, wenn es keine <subpage>s enthält und somit selbst Inhalt trägt.
Um den Mechanismus zu nutzen, dass der Server automatisch nach der Seite index.html sucht, empfiehlt
es sich, der Einstiegs-<page> oder ihrer Default-<subpage> den
Dateinamen index.html zuzuordnen.
<page> und <subpage> eingeführt, das einfach <text> heißt.
Es kann zu Beginn oder zu Ende des <page>- oder <subpage>-Elements stehen,
jedoch nicht mitten in der Auflistung der <item>s, da sonst die Liststruktur durchbrochen
wäre. Somit ist auch die Mischform möglich: man kann einen Langtext und eine Liste von <item>s
auf einer <page> oder<subpage> darstellen.
Der dem <text> untergeordnete Langtext muss kein reiner Text sein. Er kann seinerseits
durch beliege HTML-Tags angereichert sein und soll mitsamt dieser HTML-Baumstruktur ins Zieldokument
übertragen werden. Damit es nicht zu XML-Validierungsfehlern beim Parsen des Dokuments kommt, ist hierbei
die XHTML-Syntax zu beachten: Zu jedem Tag muss ein schliessendes Tag existieren, Attribute müssen
stets in der Form name="wert" angegeben werden, usw.
<index>
...
<page name="index.html" title="Thema">
<text>Willkommen auf diesen Seiten. Hier ein Konterfei
ihres Autors: <img src="konterfei.jpg"/>...
</text>
<item>
...
</item>
...
</page>
</index>
Es entstand bei mir bald das Bedürfnis, etwaigen Langtext nicht in der zentralen Datei index.xml zu führen, sondern
je in einer separaten Datei – das zentrale index.xml-Dokument wäre sonst zu unübersichtlich geworden.
Das war leicht machbar, weil zum Sprachumfang von XPath die document()-Funktion gehört, die einen
externen XML-Baum im XSLT-Programm zur Verfügung stellt. Im <text>-Tag selbst wird dann nur
noch die Datei, die den Quellcode des Langtextes enthält, als Verweis geführt. Ein Beispiel:
<index>
...
<page name="index.html" title="Thema">
<text src="index_src.html">
</page>
</index>
Zu beachten ist, dass die Quelldatei – in diesem Fall index_src.html – kein vollständiges
HTML-Dokument ist, sondern nur ein Seitenfragment darstellt. Um es genau zu sagen, enthält sie einen Teilbaum
unterhalb des <body>-Knotens im Ergebnisbaum.
<item> noch ab: Wie auch in jedem ordentlichen
Dateisystem üblich, sollten wir für jedes <item> ein Erstellungs- und Änderungsdatum
führen. Für einen Webdienst ist das relevante "Erstellungsdatum" das Datum der Erstpublikation im Internet,
also das Veröffentlichungsdatum, nicht der Zeitpunkt, zu dem die Datei physisch erstellt wurde.
In erster Näherung ist dies das Datum des ersten Uploads. Warum nur in erster Näherung? Weil eine Datei eine
gewisse Zeit im Dateisystem des Webservers vor sich hin dümpeln kann, ohne vom Publikum bemerkt zu werden.
Erst durch die Aufnahme in die Indexseiten wird eine neue Seite dem Publikum aktiv bekannt gemacht. Vorher
ist die Seite zwar nicht wirklich privat – denn jemand könnte zufällig ihre URL eingeben, und wenn die
Robots der Suchmaschinen die Seite entdeckt haben, wird sie indiziert und möglicherweise vom Suchdienst aus
angesprungen. Aber dies ist eher als ein ungewollter Effekt zu betrachten, solange man sich noch nicht entschlossen
hat, die Seite in seinen Index aufzunehmen.
Ähnlich sieht es mit dem Änderungsdatum aus. Hier ist das Datum des letzten Uploads eine gute Näherung, muss aber nicht stimmen. Es kann einen technisch begründeten Upload geben, der nicht mit einer effektiven Änderung des Seiteninhalts verknüpft war.
Das bedeutet, wir brauchen zwei neue Attribute des <item>-Elements; ich habe sie
pubDate und chgDate genannt. Aus noch zu besprechenden Kompatibilitätsgründen
verwende ich für die Notation des Datums den in der Spezifikation RFC 822 beschriebenen Standard:
date-time = [ day "," ] date time ; dd mm yy
; hh:mm:ss zzz
day = "Mon" / "Tue" / "Wed" / "Thu"
/ "Fri" / "Sat" / "Sun"
date = 1*2DIGIT month 2DIGIT ; day month year
; e.g. 20 Jun 82
month = "Jan" / "Feb" / "Mar" / "Apr"
/ "May" / "Jun" / "Jul" / "Aug"
/ "Sep" / "Oct" / "Nov" / "Dec"
time = hour zone ; ANSI and Military
hour = 2DIGIT ":" 2DIGIT [":" 2DIGIT]
; 00:00:00 - 23:59:59
zone = "UT" / "GMT" ; Universal Time
; North American : UT
/ "EST" / "EDT" ; Eastern: - 5/ - 4
/ "CST" / "CDT" ; Central: - 6/ - 5
/ "MST" / "MDT" ; Mountain: - 7/ - 6
/ "PST" / "PDT" ; Pacific: - 8/ - 7
/ 1ALPHA ; Military: Z = UT;
; A:-1; (J not used)
; M:-12; N:+1; Y:+12
/ ( ("+" / "-") 4DIGIT ) ; Local differential
; hours+min. (HHMM)
Syntax einer Zeitangabe (nach RFC 822)
<item>-Element durch eine ID zu kennzeichnen, die nichts mit
dem Dateisystem zu tun hat, sondern einfach nur eine eindeutige Identifizierung dieses
Elements ermöglicht.
In Zeiten des World Wide Web bietet sich an, hier gleich eine global eindeutige ID
(Guid) zu verwenden. Windows-Benutzer haben in der DLL rpcrt4.dll in
ihrem Windows-Ordner eine Funktion UuidCreate() zur Verfügung, die solche
IDs generiert. Indem die ID des Prozessors und die aktuelle Systemzeit in die Berechnung
der Guid eingehen, ist sichergestellt, weder durch einen anderen Rechner noch zu einer
anderen Zeit dieselbe Guid wieder ermittelt wird. Sie ist also wirklich global eindeutig.
Hier ein Beispiel einer Guid, wie sie von meinem Windows-Rechner errechnet wurde:
{E14124E0-10DD-11D9-9F46-000374890932}
Eine Guid (erzeugt mit UuidCreate() in rpcrt4.dll)
Im wesentlichen handelt es sich also um einen 16-Byte-Binärwert, der von der Funktion noch in einem speziellen Ausgabeformat zurückgegeben wird (was aber eigentlich nicht wichtig ist). Auf jeden Fall garantiert Microsoft für die globale Eindeutigkeit dieser Guids. (Auch andere Plattformen dürften Funktionen zur Generierung von Guids anbieten).
Jedem auf dem Webserver abrufbaren <item> wird also bei der Publikation ein für
allemal eine Guid zugeordnet, die dieses <item> von nun ab und für immer kennzeichnet.
In meiner XML-Struktur habe ich die Guid als Attribut des <item>-Elements
vorgesehen. Ich nutze sie
allerdings nur für solche Elemente, die wirklich Verweise auf Inhalte meiner eigenen
Webseiten darstellen, nicht für Links auf externe Webseiten. Hier ein Beispiel:
<item pubDate="Sat, 25 May 2002 21:58:00 GMT"
chgDate="Sat, 15 May 2004 18:17:00 GMT"
guid="{72209E00-0C1F-11D9-9F46-000374890932}">
<link href="./sources/barbieri.html">Eine Einführung anhand eines Bildes</link>
...
</item>
Zuordnung der Guid zum Dokument im index.xml-File
Damit mithilfe der Guids eine alternative Lokation der Inhalte im Web möglich wird, benötigt
man noch ein Servlet, das die zur Guid gehörige URL ermittelt (also das href-Attribut
des zugehörigen <link>-Elements). Das nachfolgend aufgelistete Servlet GetByGuid.java
leistet genau dies: Es führt ein "Dictionary" von Guids mit ihren zugehörigen URL's als statische
Hashtabelle. Beim Request
prüft es, ob die mitgegebene Guid im Dictionary enthalten ist. Wenn nicht, ist möglicherweise
das index.xml-File mittlerweile um neue Items erweitert worden. Das Index-File wird also
in diesem Fall neu geparsed. Wird die Guid dann gefunden, so veranlasst das Servlet den Browser,
einen Redirect auf die zugeordnete URL auszuführen. Wird sie nicht gefunden, so erscheint eine
entsprechende Fehlermeldung im text/plain-Format:
package astroIX;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.parsers.SAXParser;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.Attributes;
import java.util.HashMap;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class GetByGuid extends HttpServlet {
// Puffer für die Beziehung Guid -> Href
static HashMap hrefMap = new HashMap();
// SAX Event Handler als innere Klasse definieren
class DocumentHandler extends DefaultHandler {
String key=null;
public void startElement(String namespaceURI,
String sName,
String qName,
Attributes attrs) throws SAXException {
String href;
// <item>-Element und darin enthaltenes <link>-Element abfangen
// Wenn <item> ein guid-Attribut hat, guid mit href in Hash aufnehmen
if ( qName.equals("item") &&
(key=attrs.getValue("guid"))!=null)
// Sonderzeichen aus Guid entfernen, um URL-Encoding zu vermeiden
key=key.replaceAll("[^0-9a-fA-F]","");
else if ((key != null) && qName.equals("link") &&
((href = attrs.getValue("href"))!=null)) {
// Eintrag aufnehmen
hrefMap.put(key,href);
// Globales Feld key zurücksetzen
key = null;
}
}
}
public void doGet(HttpServletRequest request, HttpServletResponse response) {
java.io.PrintWriter out;
String indexURL;
String key;
// Um eine Fehlermeldung im Browser auszugeben, falls ID nicht gefunden
response.setContentType("text/plain");
try {
out = response.getWriter();
key = request.getRequestURI();
key = key.substring(key.lastIndexOf('/')+1).toUpperCase();
// Guid nicht gefunden? Dann muss evtl. der Index neu gelesen werden
if (hrefMap.get(key) == null) {
indexURL = request.
getRequestURL().toString().
replaceFirst("(/servlet)?/getByGuid.*$","/sources/index.xml");
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();
saxParser.parse( indexURL , new DocumentHandler() );
}
// Nachschlagen. ob Guid existiert
if (hrefMap.get(key) == null)
// Nein - Fehlermeldung ausgeben...
out.println( "ID " + key + " wurde nicht gefunden");
else
// Ja - Browser zum Redirect veranlassen
response.sendRedirect( ((String) hrefMap.get(key)).substring(1));
// Alles abfangen
} catch (Throwable t) { if (out == null) t.printStackTrace( System.out);
else t.printStackTrace( out ); }
}
}
Das Servlet GetByGuid.java
Problematisch an der Funktion UuidCreate() ist, dass die von ihr erzeugten Guids Sonderzeichen enthalten, wenn auch nur in einer festen Formatierungsmaske, die die Lesbarkeit der Hexadezimalzahl verbessern soll. Um ein umständliches URL-Encoding der Sonderzeichen zu verhindern, vereinbaren wir, dass die Guid im Request ohne diese Formatierungs-Sonderzeichen mitgegeben wird. Wenn Sie beispielsweise folgende URL in Ihrem Browser eingeben,
http://www.astrotexte.ch/getByGuid/72209E000C1F11D99F46000374890932so sollten Sie ohne Umschweife zur Betrachtung "Die Astrologie" weitergeleitet werden, also zur folgenden URL:
http://www.astrotexte.ch/sources/barbieri.htmlWenn Sie dagegen die Guid leicht abändern (und sie somit mit sehr hoher Wahrscheinlichkeit ungültig machen), erhalten Sie stattdessen im Browserfenster eine Fehlermeldung - zum Beispiel:
ID 72209E000C1F11D99F46000374890931 wurde nicht gefunden
<?xml version="1.0" encoding="ISO-8859-1"?>
<index title="Astrologische Untersuchungen"
src="http://www.astrotexte.ch"
mail="ruediger.plantiko@astrotextekeinSpam.ch"
pubDate="Wed, 23 Sep 2004 12:00:00 GMT">
<shorttext>
Mit gründlichen und zeitlosen Studien
weckt und fördert astrotexte.ch das Interesse an der Astrologie.
</shorttext>
<page name="index.html" title="Über diese Seiten">
<subpage name="index.html" title="Editorial" titleLong="Liebe Leserin! Lieber Leser!">
<text src="index_src.html"/>
</subpage>
<subpage name="index_neu.html" title="Was ist neu?">
<text src="index_src_neu.html"/>
</subpage>
<subpage name="index_intro.html" title="Motivation">
<item pubDate="Sat, 25 May 2002 21:58:00 GMT"
chgDate="Sat, 15 May 2004 18:17:00 GMT"
guid="{72209E00-0C1F-11D9-9F46-000374890932}">
<link href="./sources/barbieri.html">Eine Einführung anhand eines Bildes</link>
<shorttext>Eine allegorische Darstellung der Astrologie - und was man bei einer Bildbetrachtung
über das Wesen der Astrologie lernen kann.
</shorttext>
</item>
<item>
<link href="http://www.astrologiezentrum.de/onlinetexte/einfuehrung/0_inhalt.html"
external="true">Peter Niehenke: Astrologie - eine Einführung</link>
<shorttext>Für diejenigen, denen das ganze Gebiet der Astrologie noch fremd ist, stellt dieser
auch als Reclam-Buch erschienene Text eine exzellente Einführung dar.
</shorttext>
</item>
<item>
<link href="http://www.astro.com/astrologie/in_intro_g.htm"
external="true">Astrodienst: Einführung in die Astrologie</link>
<shorttext>Ein fundierter Schnellstart in die astrologische Symbolik, Ideal, um
in das Thema einzusteigen und es von verschiedenen Seiten kennzulernen.
</shorttext>
</item>
...
Anfang meiner Indexdatei (Vollständige index.xml-Datei anzeigen)
java xslt/Transform index.xsl index.xml >log.logDiese Anweisung legt man sich sinnvollerweise in einem Batchfile ab, um einfach per Doppelclick die Aktualisierung des Index auszuführen, wenn sich am Inhalt (also an index.xml!) oder an der Darstellung desselben (also zwingend an index.xsl!) etwas geändert hat. Wenn Sie sich das nachfolgende XSLT-Stylesheet ansehen, werden Sie sehen, dass der grösste Teil der Präsentierungsdetails auch aus dem XSLT-Stylesheet verbannt ist und in ein eigenes Stylesheet index.css für die Indexseiten gewandert ist. Dort befinden sich Angaben wie Dimensionierungen der Elemente, die gelbe Hintergrundfarbe für den aktuell ausgewählten Eintrag, Angaben zu Tabellenrahmungen etc. Das XSLT-Stylesheet und somit auch der daraus erzeugte HTML-Quellcode enthält im wesentlichen nur noch Angaben zum Arrangement der Daten und stellt den Bezug der Daten zu ihren zugehörigen CSS-Stylesheet-Klassen und illustrierenden Graphiken (Icons) her.
Ein weiterer Vorteil der Seitengenerierung mit XSLT liegt darin, dass das resultierende HTML automatisch wohlgeformt ist, also z.B. keine ungültigen Schachtelungen enthält. Dass es den HTML-Regeln und nicht den Regeln der XML-Syntax folgt, liegt einzig und allein daran, dass ich dies im <xsl:output> explizit so festgelegt habe. Um eine andere Syntax – XML oder XHTML – zu erhalten, müsste man lediglich den Wert des method-Attributs auf das gewünschte Ausgabeformat verändern. Das Stylesheet muss ja einen wohlgeformten XML-Baum darstellen, sonst könnte es den Parser gar nicht passieren und somit auch nicht ausgeführt werden. Die Wohlgeformtheit des Zielbaums ist eine automatische Folge hiervon. (Es gibt zwar die Möglichkeit, diese Wohlgeformtheit für Teilknotenmengen des Quellbaums auszuschalten, aber warum sollte man hiervon Gebrauch machen?).
Das XSLT Stylesheet wird also von einem Java-Programm eingelesen und dann Zeile für Zeile interpretiert und ausgeführt. Letztlich ist XSLT also eine Skriptsprache. Die durch die Spezifikation festgelegten Grenzen machen sie jedoch im Vergleich zu anderen Programmiersprachen wenig flexibel und etwas schwerfällig. Es gibt aber auch klare Vorteile, mit XSLT zu arbeiten statt die Transformation selbst in einer anderen Programmiersprache zu definieren:
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:redirect="http://xml.apache.org/xalan/redirect"
xmlns:date="http://exslt.org/dates-and-times"
xmlns:xalan="http://xml.apache.org/xalan"
exclude-result-prefixes="xalan"
extension-element-prefixes=" date redirect ">
<xsl:output method="html"
encoding="iso-8859-1"
indent="yes"
doctype-public="-//W3C//DTD HTML 4.01 Transitional//EN"
doctype-system="http://www.w3.org/TR/html4/loose.dtd"/>
<xsl:template match="/">
<!-- Erste Seite merken, wird nachher in Schleifensteuerung verwendet -->
<xsl:variable name="firstPage" select="/index/page[1]"/>
<!-- Hauptschleife über sämtliche page- und subpage-Knoten -->
<xsl:for-each select="index/page | index/page/subpage">
<!-- currentUnit repräsentiert die aktuelle Seite oder Unterseite -->
<xsl:variable name="currentUnit" select="."/>
<!-- currentPage repräsentiert die aktuelle Seite (nicht Unterseite) -->
<xsl:variable name="currentPage" select="ancestor-or-self::page"/>
<!-- Hat der aktuelle Knoten Subpages? Nein, dann neue Datei schreiben -->
<xsl:if test="not($currentUnit/subpage)">
<!-- Um mehr als eine Zieldatei zu erzeugen, verwenden wir die xalan-Erweiterung "redirect" -->
<redirect:write file="{$currentUnit/@name}">
<!-- Hier beginnt der HTML-DOM einer einzelnen Seite -->
<html>
<head>
<!-- Zeitstempel und Version des XSLT-Prozessors sind nützliche Informationen
Für den Zeitstempel benötigen wir die Erweiterung date().
Die Funktion system-property() dagegen ist XSLT-Standard -->
<xsl:comment>Automatisch generierte HTML-Seite
Quelle: index.xml
Transformation: index.xsl
XSLT-Prozessor: Xalan Java <xsl:value-of select="system-property( 'version' )"/>
Generierungsdatum: <xsl:value-of select="date:date-time()"/>
</xsl:comment>
<meta name="description" content="Astrologische Fachartikel zu verschiedenen Themen"/>
<meta http-equiv="content-language" content="de"/>
<meta http-equiv="content-script-type" content="text/javascript"/>
<meta http-equiv="content-style-type" content="text/css"/>
<meta name="author" content="Rüdiger Plantiko"/>
<meta name="keywords" lang="de" content="Astrologe, Astrologie, astrologisch,
Horoskopie, horoskopisch, Sonne, Mond, Merkur,
Venus, Mars, Jupiter, Saturn,
Uranus, Neptun, Pluto, Aszendent, MC, Häuser,
Direktionen, Speculum, Alan Leo, Gauricus, Regiomontanus,
Campanus, Renaissance, Widder, Stier, Zwillinge, Krebs,
Löwe, Jungfrau, Waage,
Skorpion, Schütze, Steinbock, Wassermann, Fische"/>
<link rel="stylesheet" type="text/css" href="./styles/main.css"></link>
<link rel="stylesheet" type="text/css" href="./styles/index.css"></link>
<script src="scripts/global.js" type="text/javascript"></script>
<!-- Fenstertitel -->
<title><xsl:value-of select="/index/@title"/></title>
</head>
<body>
<!-- Überschrift -->
<h1><xsl:value-of select="/index/@title"/></h1>
<!-- Beginn der sichtbar gerahmten Tabelle -->
<table class="index">
<!-- Hauptnavigation aufbauen: Über alle page-Elemente loopen -->
<xsl:for-each select="/index/page">
<tr>
<td class="topic">
<!-- Die aktuelle Seite bekommt zusätzlich die Stilklasse active -->
<xsl:if test=".=$currentPage">
<xsl:attribute name="class">topic active</xsl:attribute>
</xsl:if>
<!-- Link auf Seite in Hauptnavigation -->
<a href="{./@name}">
<xsl:value-of select="./@title"/>
</a>
</td>
<!-- Die Workarea mit dem aktuellen Seiteninhalt wird nur einmal prozessiert -->
<xsl:if test="./@name=$firstPage/@name">
<td rowspan="6" class="workArea">
<!-- Unternavigation aufbauen, wenn es Subpages gibt -->
<xsl:if test="$currentPage/subpage">
<table class="subIndex">
<tr>
<!-- Über alle Subpages der aktuellen Page loopen -->
<xsl:for-each select="$currentPage/subpage">
<td class="subTopic">
<!-- Die aktuelle Subpage bekommt zusätzlich die Stilklasse active -->
<xsl:if test="./@name=$currentUnit/@name">
<xsl:attribute name="class">subTopic active</xsl:attribute>
</xsl:if>
<a href="{./@name}"><xsl:value-of select="./@title"/></a>
</td>
</xsl:for-each>
</tr>
</table>
</xsl:if>
<!-- Überschrift innerhalb der Workarea -->
<h2>
<xsl:choose>
<xsl:when test="$currentUnit/@titleLong">
<!-- Wenn ein Langtitel existiert, diesen nehmen... -->
<xsl:value-of select="$currentUnit/@titleLong"/>
</xsl:when>
<xsl:otherwise>
<!-- ... sonst den Kurztitel, der auch für die Navigation verwendet wird -->
<xsl:value-of select="$currentUnit/@title"/>
</xsl:otherwise>
</xsl:choose>
</h2>
<!-- Nach der Überschrift: Etwaigen <text>-Knoten transformieren -->
<xsl:apply-templates select="$currentUnit/text"/>
<!-- Schliesslich die Item-Liste aufbauen -->
<table class="contents">
<!-- Über alle items der aktuellen Seite loopen -->
<xsl:for-each select="$currentUnit/item">
<tr>
<td class="icons">
<!-- Aufzählungs-Icon(s) ermitteln -->
<xsl:if test="not(./link/@external='true')">
<img src="./graphics/seite.gif"
width="16"
height="15"
alt="Seite von astrotexte.ch"/>
</xsl:if>
<xsl:if test="./link/@external='true'">
<img src="./graphics/url.gif"
width="16"
height="15"
alt="Link auf externe Webseite"/>
</xsl:if>
<xsl:if test="./link/@pdf='true'">
<img src="./graphics/pdf.gif"
width="16"
height="15"
alt="Datei im PDF-Format"/>
</xsl:if>
<xsl:if test="./link/@commercial='true'">
<img src="./graphics/kostet.gif"
width="16"
height="15"
alt="Download ist kostenpflichtig"/>
</xsl:if>
</td>
<!-- Link darstellen -->
<td class="contents">
<a href="{./link/@href}">
<xsl:value-of select="link"/>
</a>
<br/>
<!-- Etwaigen erklärenden Zusatztext (<shorttext>) hier einfügen -->
<xsl:apply-templates select="shorttext"/></td>
</tr>
</xsl:for-each>
</table>
</td>
</xsl:if>
</tr>
</xsl:for-each>
</table>
<!-- Nun ist die gerahmte Tabelle fertig - es folgt noch die Fusszeile -->
<table class="footer">
<tr>
<td width="16.67%" style="text-align:left" nowrap="nowrap">
<font size="-1">
<i>Redaktion: Dr. Rüdiger Plantiko</i>
</font>
</td>
<td width="16.67%" style="text-align:center">
<a href="http://validator.w3.org/check?uri=referer">
<img border="0"
src="./graphics/valid-html401.gif"
alt="HTML Quellcode entspricht 4.01-Spezifikation"
height="31"
width="88"></img>
</a>
</td>
<td width="16.67%" style="text-align:center">
<a href="http://www.cs.fiu.edu/~flynnj/noframes.html">
<img border="0"
src="./graphics/framefree.gif"
width="70"
height="30"
alt="Dies ist eine framefreie Webseite!"></img>
</a>
</td>
<td width="16.67%" style="text-align:center">
<a href="http://jakarta.apache.org/tomcat/">
<img border="0"
src="./graphics/tomcat.gif"
width="52"
height="40"
alt="Es bedient Sie ein Apache Tomcat Java Web Server"></img>
</a>
</td>
<td width="16.67%" style="text-align:center">
<a href="erpressum.html">Impressum</a></td>
<td width="16.67%" style="text-align:right" nowrap="nowrap">
<font size="-1">
Mail an
<script type="text/javascript" language="javascript">
document.write(mailto("ruediger", "plantiko","astrotexte","ch"));
</script>
</font>
</td>
</tr>
</table>
</body>
</html>
</redirect:write>
</xsl:if>
</xsl:for-each>
</xsl:template>
<!-- Die Kurztexte werden mit all ihren HTML-Formatierungen kopiert. Die
dafür nötige Rekursion wird durch folgende beiden Templates gewährleistet: -->
<xsl:template match="shorttext">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="text">
<xsl:choose>
<xsl:when test="./@src">
<div class="pageText">
<!-- Möglichkeit anbieten, dass Text aus externem Dokument eingelesen werden kann
Sehr einfach mit der document()-Funktion zu realisieren (XSLT-Standard) -->
<xsl:comment>Inkludiertes Quelldokument <xsl:value-of select="./@src"/></xsl:comment>
<xsl:apply-templates select = "document(./@src)/include"/>
<xsl:comment>Ende inkludiertes Quelldokument <xsl:value-of select="./@src"/></xsl:comment>
</div>
</xsl:when>
<xsl:otherwise>
<div class="pageText">
<xsl:apply-templates/>
</div>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="text//*|shorttext//*|include//*">
<xsl:copy-of select="."/>
</xsl:template>
</xsl:stylesheet>
Das Stylesheet index.xsl
-Format
aus der Taufe gehoben, das es inzwischen in der Version 2.0 gibt (die genaue Spezifikation
findet man unter http://blogs.law.harvard.edu/tech/rss).
Es sieht zur Zeit so aus, dass auch RSS 2.0 wohl noch nicht das end- und letztgültige Format für das
Sammeln und Verteilen von Nachrichten und Inhalten ist.
Bei der weiten Verbreitung, die das Format mittlerweile jedoch gefunden hat, wäre es wünschenswert, die eigenen Indexseiten möglichst automatisch in dieses Format transformieren zu können. Dank der beschriebenen Trennung von Layout (index.xsl und index.css) und Inhalt (index.xml) ist das kein Problem: Man muss nur eine XSLT-Transformation schreiben, die diese Konvertierung vornimmt. In meinem Beispiel wird das durch die folgende Transformation erreicht:
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:java="http://xml.apache.org/xalan/java"
exclude-result-prefixes="java">
<xsl:output method="xml"
encoding="iso-8859-1"
indent="yes"/>
<!-- Locale US, Formatierungsklasse und aktuelles Datum instanziieren -->
<xsl:variable name="locale_us"
select="java:java.util.Locale.new('en','us')"/>
<xsl:variable name="rfc822_formatter"
select="java:java.text.SimpleDateFormat.new('EEE, dd MMM yyyy HH:mm:ss Z',$locale_us)"/>
<xsl:variable name="num_formatter"
select="java:java.text.SimpleDateFormat.new('yyyyMMddHHmmss')"/>
<xsl:variable name="date"
select="java:java.util.Date.new()"/>
<!-- Beginn der eigentlichen Verarbeitung -->
<xsl:template match="/">
<rss version="2.0">
<channel>
<!-- Kopfdaten (auf den gesamten Channel bezogen) -->
<title><xsl:value-of select="/index/@title"/></title>
<link><xsl:value-of select="/index/@src"/></link>
<description><xsl:value-of select="/index/shorttext"/></description>
<language>de-de</language>
<pubDate><xsl:value-of select="/index/@pubDate"/></pubDate>
<lastBuildDate><xsl:value-of select="java:format($rfc822_formatter, $date)"/></lastBuildDate>
<!-- Link auf die Spezifikation des RSS 2.0 Formats -->
<docs>http://blogs.law.harvard.edu/tech/rss</docs>
<managingEditor><xsl:value-of select="/index/@mail"/></managingEditor>
<!-- Schleife über die <item>s -->
<!-- Nur den eigenen Content publizieren = items, die eine Guid haben -->
<xsl:for-each select="index/page/item[@guid] | index/page/subpage/item[@guid]">
<!-- Nach Datum sortieren - Datum aus RFC822 in sortierbaren Ziffernkette wandeln -->
<xsl:sort select="string(java:format($num_formatter,java:parse($rfc822_formatter,string(./@pubDate))))"
order="descending"/>
<item>
<title><xsl:value-of select="./link"/></title>
<!-- Absoluten Link aus relativem aufbauen -->
<link><xsl:value-of select="/index/@src"/>/<xsl:value-of select="substring-after(./link/@href,'./')"/></link>
<description><xsl:value-of select="./shorttext"/></description>
<pubDate><xsl:value-of select="./@pubDate"/></pubDate>
<guid>http://www.astrotexte.ch/getByGuid/<xsl:value-of
select="java:replaceAll(java:java.lang.String.new(string(./@guid)),'[^0-9a-fA-F]+','')"/></guid>
</item>
</xsl:for-each>
</channel>
</rss>
</xsl:template>
</xsl:stylesheet>
Das Stylesheet index2rss.xsl
Vergleicht man diese Transformation mit der zuvor beschriebenen index.xsl, so fällt zunächst der viel geringere Umfang auf: Der meiste Aufwand ist der Formatkonvertierung gewidmet, für die Transformation des XML-Baums selbst werden nur wenigen Codezeilen benötigt. Das liegt zum einen daran, dass das Zielformat nun XML ist und nicht HTML, und zwar ein reines Datenformat ohne Präsentierungsinformation. Zum anderen liegt es natürlich daran, dass die Datenformate selbst so ähnlich sind, da sie beide im Prinzip das gleiche tun, nämlich eine Liste von Links zu beschreiben.
Ich erwähnte schon, dass XSLT für normale Programmieraufgaben etwas schwerfällig daherkommt. Es empfiehlt sich daher, für komplexere Berechnungen eine andere Programmiersprache heranzuziehen und diese in das XSLT-Stylesheet einzubinden. Im oben aufgeführten Stylesheet verwende ich Java-Standardklassen zur Datumsformatierung, um aus dem RFC822-Format (z.B. "Wed, 23 Sep 2004 12:00:00 +0200") eine Ziffernfolge zu erhalten (im Beispiel: "20040923120000"), mit welcher sich die Liste der <item>s absteigend nach Publikationsdatum sortieren läßt, so daß das neueste <item> zuoberst in der Liste erscheint. Der Xalan-Prozessor ist sehr flexibel durch andere Programmiersprachen erweiterbar. Speziell für die Sprache Java kommt man – wie in diesem Beispiel – häufig bereits mit dem Java-Standard aus, muss also keine zusätzlichen eigenen Hilfsklassen programmieren (obwohl auch dies selbstverständlich möglich ist). Darüberhinaus kann man auch Code in den gängigen Script-Sprachen wie Perl, JavaScript, VBScript, JPython in das XSLT-Stylesheet einbinden.
Einige Attribute der oben vorgestellten index.xml-Datei erklären sich nun: Die Information wird im RSS 2.0-Format
benötigt, gehört aber als Inhalt dennoch in das index.xml. Beispielsweise die Angabe einer
eMail-Adresse für das <managingEditor>-Tag.
Ebenso erklärt sich aus der
RSS 2.0-Spezifikation, dass ich für
Datumsangaben das etwas altmodische RFC 822-Format verwendet habe. Auf die Idee, Guids einzusetzen, bin ich erst nach
Lektüre der RSS-Spezifikation gekommen, ich halte sie aber, unabhängig von RSS, auch für meine eigene Indexverwaltung
für eine nützliche Einrichtung. Das Attribut isPermaLink des <guid>-Tags, dessen Default auf
true steht, besagt, dass der angegebene String das item nicht nur eindeutig kennzeichnet, sondern auch einen
permanenten Link auf das Item darstellt. Würde man es auf false setzen, würde man den Wert des <guid>-Elements
nur zur eindeutigen Kennzeichnung, nicht aber als Link verwenden.
Meinem oben vorgestellten Servlet GetByGuid.java ist es zu danken,
dass wir im RSS-File die Guids zugleich als Links anbringen können, so dass wir den Default des Attributs
isPermaLink verwenden können. Die restlichen Anweisungen in der <guid>-Zeile des XSLT
sind Standardmethoden der Klasse java.lang.String, die die Sonderzeichen aus der formatierten Guid
entfernen (also aus dem Wert {72209E00-0C1F-11D9-9F46-000374890932} den Wert 72209E000C1F11D99F46000374890932 machen), da der aus reinen Hexadezimalzeichen bestehende Wert für die
URL besser verwendbar ist.
Zu bemerken ist noch, dass bei dieser Transformation die durch die Pages und Subpages definierte Struktur verlorengeht. Das ist so und soll auch so sein, weil das RSS-Format nur eine flache Liste von Links darstellen kann. Strukturinformationen sind in diesem Format nicht vorgesehen und werden daher von der Transformation nicht übernommen. Es empfiehlt sich - wie hier - die Items des Index im RSS-Feed absteigend nach Veröffentlichungsdatum zu sortieren.
2003.06.04 22:24 B C:\webpages\sources\barbieri.html --> astrotexte.ch /sources barbieri.html 2003.06.10 00:59 B C:\webpages\sources\konst_f1.html --> astrotexte.ch /sources konst_f1.html 2003.06.10 00:59 B C:\webpages\sources\konstellationen.jsp --> astrotexte.ch /sources konstellationen.jsp 2003.06.10 01:06 B C:\webpages\sources\konst_f1.html --> astrotexte.ch /sources konst_f1.html 2003.06.10 01:07 B C:\webpages\sources\konst_f1.html --> astrotexte.ch /sources konst_f1.htmlEinige Zeilen von WS_FTP.log
Mit dem folgenden Perl-Script erzeuge ich mir eine Liste von Publikations- und Änderungsdaten für alle Dateien, die von einem bestimmten Ordner aus hochgeladen wurden. Wenn ich als Argument einen Dateinamen angebe, so wird für diese Datei ein Vorschlag für das öffnende <item>-Tag erzeugt, wie ich es zum Einordnen in das Indexverzeichnis benötige. Das Script läuft mit meiner Active Perl-Installation auf einem Windows-Rechner, sollte aber mit wenig Aufwand auf andere Plattformen oder FTP-Log-Formate umgestellt werden können.
#!C:\Perl\bin\perl.exe use Time::Local; use Win32::Guidgen; my $OF, $line = "", %pubdate=(), %chgdate=(), $i=0, $arg = shift, $name, $c, $p, $g; # Das ganze Log muss man sowieso einlesen, auch wenn man nur eine einzelne Datei sucht open(OF, "WS_FTP.log"); while ( $line = <OF> ) { if ( $line =~ /^(\d{4})\.(\d\d)\.(\d\d) (\d\d):(\d\d) B (.*\\(\w+\.\w+))/ ) { $t = timelocal 0,$5,$4,$3,$2-1,$1; # chgdate-Hash merkt sich das letzte Upload-Datum if (!($t1=$chgdate{$7}) or $t1<$t) {$chgdate{$7}=$t;} # pubdate-Hash merkt sich das erste Upload-Datum if (!($t1=$pubdate{$7}) or $t1>$t) {$pubdate{$7}=$t;} } } close(OF); # On the fly nach dem Dateinamen sortieren (case insensitive) for $name ( sort { lc($a) cmp lc($b) } keys %pubdate ) { # Publikationsdatum $p = gmtime $pubdate{$name}; # Das Output von gmtime ist noch nicht ganz RFC822, # es müssen noch einige Glieder umgestellt werden $p =~ /^(\w\w\w)\s+(\w\w\w)\s+(\d\d?)\s+([\d:]+)\s+(\d{4,4})/o; $p = "$1, $3 $2 $5 $4 GMT"; # Änderungsdatum, gleiches Vorgehen $c = gmtime $chgdate{$name}; $c =~ /^(\w\w\w)\s+(\w\w\w)\s+(\d\d?)\s+([\d:]+)\s+(\d{4,4})/o; $c = "$1, $3 $2 $5 $4 GMT"; # Kein Argument übergeben? Dann Liste if ($arg eq "") { printf "%-30s %s\n%30s %s\n",$name, $p, " ", $c; } # Argument übergeben? Dann <item>-Tag elsif ($name eq $arg) { $g = Win32::Guidgen::create(); print qq(<item pubDate="$p"\n chgDate="$c"\n guid="$g">); } }Das Perl-Script ws_ftp_eval.pl
Es folgt eine Beispielausgabe für den Aufruf des Scripts mit einem Dateinamen als Argument. Die aus dem Upload vorgeschlagenen Werte sind nur Näherungen für die tatsächlichen Werte, sie müssen aus den oben angeführten Gründen nicht immer stimmen. Bei der Guid ist zu beachten, dass sie natürlich nur bei der ersten Übernahme in den Index einzusetzen ist, nicht bei späteren Aktualisierungen (denn das Script zieht bei jedem Lauf eine neue Guid).
<item pubDate="Thu, 30 Sep 2004 21:35:00 GMT"
chgDate="Sun, 3 Oct 2004 21:37:00 GMT"
guid="{9E1DEC60-1656-11D9-9F46-000374890932}">
Das automatisch generierte <item>-Tag
Dagegen folgt hier ein Auszug der Liste, die mir bei Aufruf des Scripts ohne Parameter ausgegeben wird:
...
TaphiSpec.htm Mon, 12 Aug 2002 21:00:00 GMT
Sun, 12 Oct 2003 22:22:00 GMT
test.htm Sun, 26 May 2002 08:00:00 GMT
Sun, 26 May 2002 08:00:00 GMT
testHoro.html Mon, 10 Mar 2003 20:31:00 GMT
Mon, 10 Mar 2003 20:32:00 GMT
TrapSpec.htm Wed, 19 Jun 2002 20:29:00 GMT
Sun, 12 Oct 2003 22:22:00 GMT
uranusSpec.htm Sun, 20 Jun 2004 20:03:00 GMT
Sun, 20 Jun 2004 20:03:00 GMT
Vertex.zip Sun, 26 May 2002 08:00:00 GMT
Fri, 16 Aug 2002 20:19:00 GMT
...
Eine Liste mit Publikations- und Änderungsdaten
| Zum Inhaltsverzeichnis | Ohne Hintergrund ausdrucken | Zurück zur Homepage |