Session-Verwaltung in Servlets mit HttpSession

Die Servlet-API bietet die Klasse HttpSession an, eine Bibliothek auf hohem Niveau für die Verwaltung einer Sitzung. Sie basiert entweder auf Cookies oder URL-Rewriting, doch wird das von der API transparent gehalten. Als Programmierer bekommen wir so gut wie gar nichts davon mit. Falls der Client keine Kekse mag, wandeln wir alle Informationen in URLs um, die wir dann anbieten. Ein Sitzungsobjekt verwaltet die gesicherten Daten auch selbstständig in einer Datenstruktur. Hier fällt für uns keine Arbeit an.

Das mit einer Sitzung verbundene Objekt HttpSession

Jede Sitzung ist mit einem Sitzungsobjekt verbunden, das die Klasse HttpSession abbildet. Bei JSPs repräsentiert das implizite Objekt session die aktuelle Sitzung.

Werte mit einer Sitzung assoziieren und auslesen

Um Informationen mit der Sitzung zu verbinden, verwenden wir die Methode setAttribute(), die einen Schlüssel und einen Wert verbindet. Daten werden mit getAttribute() wieder aus der Datenstruktur gelockt, so wie es das folgende Beispiel zeigt:

List l = (List) session.getAttribute( "artikel" );

Hier verbinden wir mit einem Schlüssel eine Liste von Waren. Im Hintergrund werden die Informationen auf der Serverseite gesichert. Die Informationen selbst werden nicht in Cookies oder in der URL abgelegt, daher spielt die Größe der Daten auch keine Rolle. Ein HttpSession-Objekt verwaltet einen Assoziativspeicher, der die Wertepaare speichert. Es ist günstig, die Elemente serialisierbar zu gestalten, um die Daten dauerhaft zu speichern.

Werfen wir abschließend einen Blick auf das Programmstück, das eine neue Ware hinzufügt:

List l = (List) session.getAttribute( "artikel" );
if ( l == null )
{
  l = new ArrayList();
  session.setAttribute( "artikel", l );
}
l.add( w );

interface javax.servlet.http.HttpSession

  • Object getAttribute( String name )

    Liefert das mit name verbundene Objekt; null, wenn es keine Assoziation gab.
  • Enumeration getAttributeNames()

    Liefert eine Aufzählung aller mit der Sitzung verbundenen Objekte.
  • void setAttribute( String name, Object value )

    Bindet name mit dem Objekt value an die Sitzung. Existierte das Objekt, wird es ersetzt. Angemeldete HttpSessionBindingListener werden über die Methode value Bound() beziehungsweise valueUnbound() informiert.
  • void removeAttribute( String name )

    Entfernt das Attribut von der Sitzung. Ungültige Namen werden ignoriert. HttpSession BindingListener werden durch Aufruf von valueUnbound() informiert.

Alle Methoden liefern eine IllegalStateException, wenn die Sitzung ungültig ist. Die Methoden putValue() und setValue() sind veraltet und wurden durch setAttribute() und getAttribute() ersetzt.

URL-Rewriting

Das Session-Management sollte im Prinzip unabhängig von der technischen Umsetzung sein. Doch leider greift das Sitzungsmanagement beim URL-Rewriting schon sehr stark ein: Bei jedem Verweis auf eine neue Seite muss die URL entsprechend angepasst werden, weil die Sitzungs-ID mitgeschickt werden muss. Cookies verwalten die Sitzungs-ID völlig anders. Das bedeutet: Werden Cookies eingesetzt, ändert sich die URL nicht und jeder kann problemlos auf eine neue Seite verweisen. Nur beim URL-Rewriting muss an die URL eine Sitzungskennung angehängt werden.

Beispiel Eine URL für einen Cookie besitzt keine Sitzungskennung.

  • http://localhost/servlet/URLRewritingSession

Mit URL-Rewriting sieht das dann etwa so aus:

  • http://localhost/servlet/URLRewritingSession;jsessionid=abcde234

Wenn wir innerhalb eines Servlets auf eine andere generierte Seite verweisen wollen, haben wir eine URL vor uns, zu der wir verzweigen möchten. Die Servlet-API kümmert sich darum, an eine Benutzer-URL die Sitzungs-ID automatisch anzuhängen. Dazu dienen die HttpServletResponse-Methoden encodeURL() und encodeRedirectURL().

Beispiel: Aufgrund einer Formularbestätigung soll auf eine JSP-Seite mit dem Namen validate.jsp verwiesen werden:

<form action='<%= response.encodeURL("/validate.jsp") %>'>

Werden der Verweis und die Kodierung aus Versehen vergessen, ist dies das Ende der Sitzung. Ob eine Sitzung mit einem Cookie behandelt wird, lässt sich mit isRequestedSessionIdFromCookie() testen. Dann kann aufgrund einer Fallunterscheidung encodeURL() verwendet werden oder nicht. Allgemein ist es aber nicht schlecht, grundsätzlich alle Verweise innerhalb einer Webapplikation mit encodeURL() zu sichern. Im Fall von Cookies wird zwar keine Kennung angehängt, eine spätere Umstellung gestaltet sich aber einfacher, falls der Nutzer die Cookies einmal ausschaltet.

Zusätzliche Informationen

Ein Sitzungsobjekt verwaltet neben den assoziierten Daten noch weitere Informationen. Jede Sitzung bekommt eine eindeutige ID, die sich mit getId() erfragen lässt. Ist die Sitzung neu und hat der Client noch nie eine Verbindung gehabt, gibt isNew() den Wert true zurück. Existiert dann die Sitzung, gibt getCreationTime() ein long zurück – kodiert sind wie üblich die vergangenen Millisekunden seit dem 1.1.1970 –, in dem sich das Erstellungsdatum erfragen lässt. Dagegen erfragt getLastAccessedTime() die Zeit, die seit dem letzten Zugriff durch den Client vergangen ist. Falls der Server die Informationen dauerhaft speichert und der Cookie nicht abläuft, erlaubt dies Meldungen der Art: »Schön, Sie nach zwei Wochen zum fünften Mal bei unserer Partnervermittlung wiederzusehen. Hat’s wieder nicht geklappt?«

Das Ende der Sitzung

Eine Sitzung ist nicht automatisch unendlich lange gültig. Bei Cookies lässt sich der Gültigkeitszeitraum einstellen. Auch Sitzungsobjekte lassen sich in der Zeit anpassen. Die Methode setMaxInactiveInterval() setzt den Wert, wie lange eine Sitzung gültig ist. Ist der Wert negativ, zeigt er an, dass die Sitzung nicht automatisch beendet wird. Die entsprechende Methode getMaxInactiveInterval() liefert die Zeit in Sekunden, in der eine Sitzung gültig ist.

interface javax.servlet.http.HttpSession

  • long getCreationTime()

    Gibt in Millisekunden ab dem 1.1.1970 an, wann die Sitzung eröffnet wurde.
  • String getId()

    Liefert eine eindeutige Kennung, die die Sitzung identifiziert.
  • long getLastAccessedTime()

    Gibt in Millisekunden ab dem 1.1.1970 zurück, wann der Client zum letzten Mal auf den Server zugegriffen hat.
  • int getMaxInactiveInterval()
  • void setMaxInactiveInterval( int interval )

    Liefert und setzt die Zeit, für die der Servlet-Container die Sitzung aufrechterhalten soll, bis sie ungültig wird.
  • boolean isNew()

    Der Rückgabewert ist true, wenn die Sitzung neu ist.

Beispiel Zum Schluss wollen wir ein Programm formulieren, das alle diese Informationen auf einmal ausgibt.

<%@ page language="java" import="java.util.*" %>
<%
int cnt = 0;
if ( session.isNew() )
{
out.println( "Willkommen Neuling!\n" );
}
else
{
out.println( "Hallo, alter Freund!\n" );
String o = (String) session.getAttribute( "cnt" );
if ( o != null )
cnt = Integer.parseInt( o );
cnt++;
}
session.setAttribute( "cnt", ""+cnt );
%>
<p>
Session-ID: <%= session.getId() %> <p>
Erzeugt am: <%= new Date(session.getCreationTime()) %> <p>
Letzter Zugriff: <%= new Date(session.getLastAccessedTime()) %> <p>
Ungültig in Minuten: <%= session.getMaxInactiveInterval()/60 %> <p>
Anzahl Zugriffe: <%= cnt %>

Das Programm liefert beispielsweise folgende Ausgabe:

Hallo, alter Freund!
ID: 91410050092487D9B5D0D2A7A3D0F072
Erzeugt am: Fri Jan 18 20:16:49 CET 2002
Letzter Zugriff: Fri Jan 18 20:23:33 CET 2002
Ungültig in Minuten: 30
Anzahl Zugriffe: 4

Die ID sieht bei jedem Server anders aus. Der Webserver von Sun erzeugt beispielsweise ganz andere Kennungen.

Eine Sich-Selbst-Implementierung

Der Zusammenhang zwischen inneren Klassen und äußeren Klassen und wie sie sich gegenseitig implementieren können.

Eine Klasse kann entweder von einer Klasse erben oder eine Schnittstelle erweitern. Es ergibt sich ein Sonderfall, wenn wir eine Schnittstelle implementieren, die innerhalb der eigenen Klasse liegt, die die Schnittstelle deklariert. Das sieht etwa so aus:

class Outer implements Outer.InterfaceInner
{
  interface InterfaceInner
  {
  void knexDirDeineWelt();
  }

  public void knexDirDeineWelt()
  {
  }
}

Prinzipiell spricht erst einmal nichts gegen diese Implementierung. Innere Klassen, wie InterfaceInner eine ist, werden auf eine extra Klassendateien abgebildet, da es innerer Klassen beziehungsweise Schnittstellen sowieso nicht für die Laufzeitumgebung gibt. In unserem Fall könnte der Compiler die Datei Outer$InterfaceInner erzeugen. Im nächsten Schritt würde dann Outerdiese Schnittstelle erweitern und wie im Beispiel eine Methode überschreiben.

So schön dies auch aussieht: Es funktioniert nicht. Frühe Compiler erlaubten diese Schreibeweise, aber sie führt zu zirkulären Abhängigkeiten.

cyclic inheritance involving Outer

Wenn InterfaceInner zuerst übersetzt würde und dann Outer, wäre es noch zu verstehen, doch das Problem machen zum Beispiel Deklarationen in der inneren Klasse, die abhängig sind von der Äußeren. Wir könnten etwa den Rückgabewert von knexDirDeineWelt() ändern, dass es ein Outer Objekt zurückliefert.

class Outer implements Outer.InterfaceInner
{
  interface InterfaceInner
  {
    Outer knexDirDeineWelt();
  }

  public void knexDirDeineWelt()
  {
  }
}

Jetzt sehen wir: Ohne InterfaceInner kein Outer, da dies knexDirDeineWelt() vorschreibt und ohne Outer kein InterfaceInner, da sonst der Rückgabewert nicht bekannt ist. Mitunter wäre das Problem noch lösbar, aber hier lässt der Compiler lieber die Finger von.

Innere Klasse vor der äußeren

Dass es nicht unmöglich ist, eine innere Klasse von der äußeren Abzuleiten zeigt folgendes Beispiel:

interface I
{
  void boo();
  interface J extends I
  {
  }

  J foo();
}

Es ist problemlos möglich, dass die innere Schnittstelle die äußere erweitert. Im Gegensatz zum vorherigen Beispiel ist in diesem Fall die Problematik genau umgekehrt. Es ist auch möglich, dass eine innere Klasse eine äußere erweitert.

class O
{
  class I extends O
  {
    void bar()
    {
    }
  }

  void bar() { }
}

Automatisches Neuladen von Servlet-Seiten

Über das implizite Objekt response lassen sich Antworten von der JSP-Seite an den Client formulieren. Das Setzen von Content-Type ist für nahezu alle Servlets unabdingbar. Daneben gibt es noch weitere, die beispielsweise für Cookies interessant sind. Ein spezieller Header kann auch das Caching beeinflussen (mit dem Datum der letzten Modifizierung) oder die Seite nach einer bestimmten Zeit neu laden. Letzteres wollen wir verwenden, um eine einfache Ausgabe zu erzeugen, die jede Sekunde neu geladen wird. (Die Seite darf jedoch nicht im Cache liegen. Um das Caching explizit auszuschalten, sollte Pragma: no-cache gesetzt werden. Bei einer lokalen Installation spielt dies jedoch keine Rolle.

<%! private String result = "*"; %>
<% response.setHeader( "Refresh", "1" ); %>
<%= result += "*" %>

Dieses Servlet erzeugt eine Reihe von Sternchen, wobei es sich die Zeichenkette jede Sekunde neu vom Server holt. Das dargestellte Programm zeigt in einfacher Weise auf, was sich noch wesentlich komplexer mit Threads anstellen lässt. Im Hintergrund hätten wir einen Thread starten können, der ständig eine neue Berechnungen durchführt, die wir dann in der println()-Zeile hätten ausgeben können.

Selbst definierte Cursor in AWT/Swing

Bei einem Cursor sind wir nicht allein auf die vordefinierten Muster angewiesen. Leicht lässt sich aus einer Grafik ein eigener Cursor definieren. Dazu bietet das Toolkit die Methode createCustomCursor() an. Als Argument geben wir ein Image-Objekt, einen Hotspot und einen Namen an. Der Hotspot definiert eine Art Nullpunkt, der die Spitze angibt. Zeigt etwa der Standardcursor mit einem Pfeil nicht wie üblich nach oben, sondern nach unten, so gibt der untere Punkt den Nullpunkt an. Der Name ist nur nötig, wenn Java-Accessibility genutzt wird, also eine Möglichkeit gegeben ist, den Cursor zum Beispiel ohne Maus anzusprechen.

Beispiel: Weise einer java.awt.Component den Cursor mit der Grafik cursor.gif und dem Hotspot auf (10,10) zu.

Cursor c = getToolkit().createCustomCursor(
new ImageIcon( "cursor.gif" ).getImage(),
new Point(10,10), "Cursor" );
component.setCursor( c );

Hinweis Animierte Cursor bietet die Java-Umgebung nicht an. Wir könnten selbstständig in einem Thread immer wieder mit setCursor() unterschiedliche Cursor setzen, um etwa eine drehende Sanduhr oder eine rotierende Festplatte zu bekommen.

Da grafische Oberflächen in der Regel keine Cursor beliebiger Auflösung und Farbanzahl zulassen, lässt sich das Toolkit auch über diese Größen erfragen. Die Methode getBestCursorSize() liefert die mögliche Größe des Cursors zurück. Es ist sinnvoll, diese Methode vorher aufzurufen, um ein passendes Bild auszuwählen. Ähnlich wie bei den Icons in der Titelleiste werden die Grafiken sonst skaliert, was nicht unbedingt schön aussehen muss. Gleiches gilt für die Farben. Nicht alle Systeme erlauben beliebig viele Farben für den Cursor. Die maximale Farbanzahl liefert die Funktion getMaximumCursorColors(). Notfalls wird der Cursor auf die Farbanzahl heruntergerechnet.

Tipp: Unterstützt die Plattform Cursor beliebiger Größe, so lässt sich dadurch einfach eine Bubble-Help realisieren, die nicht rechteckig ist. An Stelle des Cursors wird eine Grafik mit dem Cursor zusammen mit einer Hilfe angezeigt. Das Betriebssystem verwaltet den Cursor, und wir müssen den Hintergrund nicht sichern und mit der Hilfe verknüpfen.

abstract class java.awt.Toolkit

  • Cursor createCustomCursor( Image cursor, Point hotSpot, String name )

    throws IndexOutOfBoundsException

    Erzeugt ein neues Cursor-Objekt. Liegt der Hotspot außerhalb der Grenzen der Grafik, wird eine IndexOutOfBoundsException ausgelöst.

  • Dimension getBestCursorSize( int preferredWidth, int preferredHeight )

    Liefert die unterstützte Cursor-Größe, die den gewünschten Ausmaßen am nächsten liegt. Oft werden die Argumente ignoriert, wenn die Umgebung keine beliebige Cursor-Größe zulässt. Erlaubt das System überhaupt keine selbst definierten Cursor, erhalten wir ein Objekt der Dimension (0,0).
  • int getMaximumCursorColors()

    Liefert das Maximum an Farben, das das Toolkit für Cursor unterstützt. Der Rückgabewert ist null, wenn selbst definierte Cursor nicht gestattet sind.

public abstract class java.awt.Component implements ImageObserver, MenuContainer, Serializable

  • void setCursor( Cursor cursor )

    Weise einen neuen Cursor zu.

In Servlets Seiten über HTTP-Redirect umlenken

Ist eine Seite nicht mehr korrekt, kann sie umgelenkt werden. Hierfür wird ein spezieller Header gesetzt.

sendRedirect()

Dazu dient die Methode sendRedirect(String), die auf eine neue Seite verweist. Als Argument kann eine relative oder absolute URL aufgeführt werden, die auf eine temporäre neue Seite weist. Wir könnten auch mit setHeader() arbeiten, müssten dann aber von Hand den Statuscode ändern, der für Umleitungen auf 302 gesetzt sein muss. Die Arbeit können wir uns sparen. Nach dem Setzen der Umleitung sollte nicht mehr in die Ausgabe geschrieben werden.

Wozu kann nun diese Umleitung eingesetzt werden? Zum Beispiel, um über Formular-Parameter zu externen Seiten weiterzuleiten:

response.sendRedirect( url );

Nach der Umleitung steht der Ort der neuen Seite in der URL-Zeile des Browsers. Das folgende Programm verweist nun einfach auf ein anderes Servlet. Die Pfadangabe kann absolut oder relativ sein.

String url = "http://www.tutego.de/";
response.sendRedirect( url );

Was passiert beim Umlenken?

Technisch gesehen ist eine Umlenkseite eine ganz normale Webseite. Das wirkliche Umlenken ist eine Fähigkeit des Browsers und nicht des Servers. Dies ist wichtig anzumerken, weil eigene Programme, die URL-Verweise aufbauen, hier oft nicht korrekt vorgehen.

Das Servlet setzt beim sendRedirect() den Content-Type auf "text/html". Wichtig sind zwei weitere Informationen: die eine in der Statuszeile und die andere im Header. In der Statuszeile wird die Nummer 302 gesendet, die das Umlenken bezeichnet. Die Information darüber, wohin verwiesen wird, steht in einem weiteren Header namens »Location«. Somit können wir unser Redirect prinzipiell auch selbst ausformulieren, indem wir Folgendes schreiben:

response.setStatus( 302 );
response.setContentType( "text/html" );
response.setHeader( "Location", url );

Der String url ist dann eine Referenz auf die neue Seite. Der Verweis auf die externe Seite muss dann natürlich absolut sein. Dies regelt jedoch sendRedirect() automatisch.

Rechte (Permissions) und Mengenbeziehungen

Für jede geladene Klassen gilt eine Sammlung von Rechten, die für diese Klasse vergeben wurden. In der Regel wurden sie per Policy-Datei vergeben, doch natürlich sind auch andere Möglichkeiten denkbar. Diese Sammlung selbst wird in einem PermissionCollection-Objekt gespeichert, welches einer ProtectionDomain zugeordnet ist.

ProtectionDomain domain = ListPermissions.class.getProtectionDomain();
PermissionCollection permissonColl = Policy.getPolicy().getPermissions( domain );

Dem PermissionCollection-Objekt lässt sich mit einer Enumeration die gespeicherten Permissions rauskitzeln. Ein System.out liefert ebenso eine schöne Ausgabe, etwa für das eigene Programm ListPermissions ohne Sicherheitsmanager:

java.security.Permissions@c21495 (
(java.util.PropertyPermission java.specification.vendor read)
(java.util.PropertyPermission java.vm.specification.vendor read)
(java.util.PropertyPermission path.separator read)
(java.util.PropertyPermission java.vm.name read)
(java.util.PropertyPermission java.class.version read)
(java.util.PropertyPermission os.name read)
(java.util.PropertyPermission java.vendor.url read)
(java.util.PropertyPermission java.vendor read)
(java.util.PropertyPermission java.vm.vendor read)
(java.util.PropertyPermission file.separator read)
(java.util.PropertyPermission os.version read)
(java.util.PropertyPermission java.vm.version read)
(java.util.PropertyPermission java.version read)
(java.util.PropertyPermission line.separator read)
(java.util.PropertyPermission java.vm.specification.version read)
(java.util.PropertyPermission java.specification.name read)
(java.util.PropertyPermission java.vm.specification.name read)
(java.util.PropertyPermission java.specification.version read)
(java.util.PropertyPermission os.arch read)
(java.lang.RuntimePermission exitVM)
(java.lang.RuntimePermission stopThread)
(java.net.SocketPermission localhost:1024- listen,resolve)
(java.io.FilePermission \D:\JavaBook\programme\24_Sicherheitskonzepte\- read)
)

Die Rechte sind natürlich genau diejenigen, die vom System bereitgestellt worden sind. Sie beziehen sich genau auf unsere Klasse ListPermissions. Für die Systemklassen, wie java.lang.Object oderString gelten keine Einschränkungen. Ersetzen wie ListPermissions durch Objekt, so würde ein System.out genau die alles-erlaubende Permission ergeben:

(java.security.AllPermission <all permissions> <all actions>)

Schließt eine Permission eine andere Permission ein?

Permission-Objekte definieren selbst nicht nur Rechte für spezielle Eigenschaften (Lesen in einem speziellen Verzeichnis), sondern ziehen auch Recht für andere Eigenschaften nach sich (Lesen aller Dateien ab einem Verzeichnis). Wird etwa für ein Verzeichnis das Recht auf Lesen und Schreiben gesetzt, dann impliziert dies auch das Lesen. Die Fähigkeit, dass ein Recht ein anderes bedingt, ist eine Fähigkeit der Permission-Objekte. Die Klasse bietet eine implies()-Funktion, die testet, ob eine Permission eine andere Permission einschließt.

Beispiel: Wir wollen zwei FilePermission-Objetke anlegen, wobei das erste (perm1) das zweite (perm2) einschließt.

import java.io.FilePermission;
import java.security.Permission;

public class PermissionImplies
{
  public static void main( String args[] )
  {
    Permission perm1 = new FilePermission( "c:\\windows\\*", "read,write" );
    Permission perm2 = new FilePermission( "c:\\windows\\fonts", "read" );
    
    if ( perm1.implies(perm2) )
      System.out.println( perm1 + " implies " + perm2 );
    
    if ( !perm2.implies( new FilePermission("c:\\windows\\fonts", "write") ) )
      System.out.println( perm1 + " not implies " + perm2 );
  }
}

Die Ausgabe zeigt, dass diese Rechte vom System korrekt unterstützt werden.

(java.io.FilePermission c:\windows\* read,write) implies (java.io.FilePermission c:\windows\fonts read)
(java.io.FilePermission c:\windows\* read,write) not implies (java.io.FilePermission c:\windows\fonts read)

Parametersammlungen im Servlet mit getParameterValues() auslesen

Da ein Parameter auch mehr als einen Wert haben kann, hilft getParameter() nicht weiter, da dieser nur jeweils einen Wert liefert. Hier führt die Methode getParameterValues() zum Ziel, die ein Feld von Strings zurückgibt. (Damit ist kein zusammengesetzter String etwa für Suchmaschinen gemeint.) Sind wir an einer vollständigen Aufzählung der Schlüssel interessiert, liefert getParameterNames() ein Objekt vom Typ Enumeration zurück, mit dem wir durch das Feld wandern und die Werte mit getParameter() erfragen können.

<%
java.util.Enumeration paramNames = request.getParameterNames();
while ( paramNames.hasMoreElements() )
{
 String param = (String) paramNames.nextElement();
 out.print( "<p>" + param + " = " );
 String[] paramValues = request.getParameterValues( param );
 if ( paramValues.length == 1 )
 {
  String paramValue = paramValues[0];
  if ( paramValue.length() == 0 )
   out.println( "unbestimmt" );
  else
   out.println( paramValue );
 }
 else
 {
 for ( int i = 0; i < paramValues.length; i++ )
  out.print( paramValues[i] + " " ) ;
  out.println( "<p>" );
 }
}
%>

Wenn wir das Programm mit der Zeile

http://localhost:8080/jt/parameterNames.jsp?a=1&b=2&c=&a=2

im Browser starten, erzeugt das Servlet folgende Ausgabe:

b = 2
a = 1 2
c = unbestimmt

Wir sehen, dass alle Parameter hier aufgeführt sind, doch in unbestimmter Reihenfolge. Dies ist aber egal. Das Programm erkennt, ob ein Wert gesetzt ist oder nicht beziehungsweise ob einem Schlüssel ein Wert zweimal zugewiesen wurde.

Wie heißt die Klasse mit der Methode main()?

In C(++) ist das erste Element des Felds der Funktion main(int argc, char **argv) der Name des Programms. Das ist in Java anders. Die Methode enthält als ersten Parameter nicht den Namen der Klasse beziehungsweise des Programms, sondern einfach den ersten Parameter – sofern einer auf der Kommandozeile übergeben wurde. Auf einem kleinen Umweg ist das auch für manche Klassen möglich.

Der zu einer Klasse gehörende Klassenlader lässt sich mit dem Class-Objekt erfragen. Mit der Methode getResource() erhalten wir von einem Klassennamen ein URL-Objekt zurück, das (unter gewissen Voraussetzungen) die Position der Klassendatei im Dateisystem anzeigt. Das folgende Programmbeispiel zeigt, wie wir von einer Klasse den vollständigen Dateipfad zurückbekommen.

package com.tutego.insel.lang;

import java.net.*;

public class FindClassLocation
{
 static String findLocation( Class<?> clazz )
 {
  ClassLoader loader = clazz.getClassLoader();

  if ( loader == null )
   return null;

  URL url = loader.getResource( clazz.getName().replace('.', '/' ) + ".class" );

  return ( url != null ) ? url.toString() : null;
 }

 public static void main( String[] args ) throws Exception
 {
  Class<?> c = Class.forName( "com.tutego.insel.lang.FindClassLocation" );

  System.out.println( "Class: " + c.getName() );
  System.out.println( "Filename: " + findLocation(c) );
 }
}

Unter meinem Dateisystem liefert das Programm die Ausgabe:

Class: com.tutego.insel.lang.FindClassLocation
Filename: file:/S:/Insel/programme/09_Funktionsbibliothek/com/tutego/insel/lang/FindClassLocation.class

Achtung! Die Lösung funktioniert natürlich nur unter gewissen Voraussetzungen. Es geht nur für Klassen, die in kein Jar-Archiv eingebunden sind und nicht den Standardbibliotheken entstammen. Auch ist eine Dateiangabe unmöglich, wenn wir etwa einen eigenen Klassenlader schreiben, der die Klassen aus einer Datenbank bezieht; dann gibt es keinen Pfad mehr.

getResourceAsStream()

Benötigen wir den Ort einer Klasse, um mit dieser Information auf weitere Dateien im Verzeichnis zuzugreifen, geht es mit der Class.getResourceAsStream(String) einfacher. Diese Methode dient insbesondere dazu, Ressourcen wie Bilder oder Musik aus einem Jar-Archiv auszulesen. Auch der ClassLoader bietet die Methode getResourceAsStream(String) an. Diese Methoden funktionieren ebenfalls für Klassen aus Jar-Archiven, wenn die Ressource im Archiv liegt.

Durch Null-Cursor Flackern des Mauszeigers bei Animationen vermeiden

Einige Betriebssysteme haben bei Java-Animationen das Problem, dass der Mauszeiger unruhig flackert. Zur Lösung kann man einen Cursor ohne Pixel an die Stelle der Grafik setzen.

Es ist Sache der grafischen Oberfläche, den Mauszeiger mit dem Hintergrund zu verbinden. Um ein unruhiges Bild zu vermeiden, greifen wir zu einem Trick und schalten den Mauszeiger einfach ab. Dazu soll createCustomCursor() einen neuen Cursor mit transparentem Image-Objekt erzeugen. Da wir kein leeres transparentes GIF-Bild nutzen wollen, legen wir einfach mit der Klasse BufferedImage ein Bild im Speicher an. Das Argument muss dabei TYPE_INT_ARGB sein, sonst ist das Bild nicht transparent. Damit ist die Arbeit getan, der letzte Schritt besteht darin, den Cursor mit setCursor() einer Komponente zuzuweisen. Im Fall einer Animation wäre das zum Beispiel ein JComponent, im folgenden Beispiel wird das eine Schaltfläche sein:

import javax.swing.*;
import java.awt.*;
import java.awt.image.*;

public class NullCursor
{
 public static void main( String[] args )
 {
  JFrame f = new JFrame();
  f.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

  JButton b = new JButton( "Kein Cursor" );
  f.add( b, BorderLayout.PAGE_START );

  b.setCursor( Toolkit.getDefaultToolkit().createCustomCursor(
    new BufferedImage( 16, 16, BufferedImage.TYPE_INT_ARGB ),
    new Point(0,0), "" ) );

  f.setSize( 200, 200 );
  f.setVisible( true );
 }
}

Algorithmen für Rastergrafik, 1. Linien zeichnen mit Bresenham

Der Bildschirm ist in eine Reihe von Punkten zerlegt, die alle mit verschiedenen Intensität oder Farbe gesetzt sind. Die Rastergrafik versucht, die geometrischen Beschreibungen von Objekten auf die Pixel des Bildschirmes abzubilden. Diese Umrechnung muss schnell und in einigen Fällen speichersparend sein. In den Letzen Jahren wurden eine ganze Reihe von Algorithmen entwickelt, die mittlerweile in Hardware Einzug gehalten hat. Die modernen Grafikkarten unterstützen neben grafischen Operatio­nen wie Pixel und Farbe mischen auch Funktionen zum Zeichen von Linien, Kreisen, Polygonen, gefüllten Flächen und weiteres. Daneben sind dreidimensionale Koordinatentransformationen in Hardware gegossen wie das Abbilden einer Bitmap auf eine Fläche, das Erzeugen von Nebel (engl. Fog­ging) oder auch perspektivische Korrektionen (engl. Perspective Correction). Natürlich überlassen wir das Zeichen der Linien der Grafikkarte und die dreidimensionalen Transformationen der 2D-Biblio­thek. Java ist allerdings eine einfache Sprache und die Arbeitsweise kann somit schnell erklärt werden. Notfalls können wir Funktionen, die nicht in der Grafikbibliothek vorliegen, nachbilden.

Linien zeichnen

Linien sind ein einfachen Beispiel dafür, wie Punkte auf dem Raster verteilt werden. Beachten wir dazu die Grafik. Wie zeichnen eine Linie von den Koordinaten (x1,y1) zum Punkt (x2,y2).

Um nun die Punkte auf dem physikalischen Bildschirm zu setzen, müssen die Punkte so verteilt wer­den, dass sie möglichst nah an der wirklichen Linien liegen. Gehen wir die X-Achse von xl nach x2 ab, so müssen wir die Y-Punkte wählen. Bei der Wahl können wir folgende Entscheidung fällen: An jedem Punkt müssen wir einen Pixel wählen, nämlich diesen, der den geringsten Abstand zum Pixelraster besitzt. Ist die Linie horizontal, so ist der Abstand zwischen wirklicher Linie und Rasterpunkt immer Null und die Wahl ist einfach.

Ein Algorithmus kann nun wie folgt arbeiten: Es ist der Abstand zwischen der wirklichen Linie und einem Pixel auf der Y-Achse gleich einem Fehlerwert. Es ist der Punkt auf der Y-Achse auszuwählen,
der zur wirklichen Linie den kleinsten Fehlerwert aufweist. Einen schnellen Algorithmus, der genau mit diesem Fehlerwert arbeitet, wurde zuerst von j. E. Bresenham (1965) veröffentlicht. Bresenham
betrachtet Linien im ersten kartesischen Oktanten, die Steigung der Linie ist also kleiner Eins, formal: 0 < Δy / Δx < 1. Die Rasterung ist von links nach rechts. Betrachten wir nun ein Programmsegment in
Pseudocode das, welches eine Linie mit Steigung kleiner Eins zeichnet:

Δx = x2 - x1
Δy = y2 - y1
error = - Δx / 2;
setPixel( x1, y1 );
while( x < x2 ) {
 error = error + Δy;
 if ( error >= 0 ) {
  y = y + 1;
  error = error - Δx;
 }
 x = x + 1;
 setPixel( x, y );
}

Liegen die Linien in anderen Oktanten, so müssen wir den Algorithmus nur leicht abändern. So sind durch einfache Spiegelungen an Winkelhalbierenden oder Ordinate bzw. Abszisse die Punkte zu verschieben. Liegt die Linie beispielsweise im siebten Oktanten, so rastern wir entlang der Y-Achse und vertauschen dx mit -dx und liegt die Linie im achten Oktanten, so rastern wir wir im ersten Oktanten, nur dass dy mit -dy vertauscht wird.

Bresenhams Linienalgorithmus arbeitet schnell und optimal (was bedeutet, die Fehler sind am kleinsten), weil alle Berechnungen auf Ganzzahlen ausgeführt werden. Dies war in den sechziger Jahren

noch wichtiger als heute, wo eine Fließkommaoperation schon genauso viel kostet wie eine lnteger-Operation. Da der Beweis langwierig — aber für die mathematisch geschulten nicht schwierig — ist, wollen wir an dieser Stelle auf den Formalismus verzichten. Anstelle dessen betrachten den Quellcode einer Klasse, die eine einfaches Linienmuster schreibt. Ein nicht unwesentlicher Teil wird dafür aufgewendet, den Algorithmus für die verschiedenen Lagen im kartesischen Koordinatensystem anzupassen. Da wir später noch andere grafische Objekte zeichnen, dient dieses Programm gleichzeitig als Rahmen.

import java.awt.*;
import java.awt.event.*;

public class GDV extends Frame
{
  private Graphics g;
  static final int CLASSIC = 0;
  static final int BRESENHAM = 1;
  public int lineType = CLASSIC;
  
  public GDV() {
    setSize( 400, 400 );
    addWindowListener( new WindowAdapter() {
      public void windowClosing ( WindowEvent e ) { System.exit(0); }
    });
  }

  /**
   * Draws a line with algorithm of Bresenham
   */
  private void drawBresenhamLine( int x1, int y1, int x2, int y2 )
  {
    int xIncrement = 1,
        yIncrement = 1,
        dy = 2*(y2-y1),
        dx = 2*(x1-x2),
        tmp;
  
    if ( x1 > x2 ) {      // Spiegeln an Y-Achse
      xIncrement = -1;
      dx = -dx;
    }
  
    if ( y1 > y2 ) {      // Spiegeln an X-Achse
      yIncrement= -1;
      dy= -dy;
    }
  
    int e = 2*dy + dx;
    int x = x1;           // Startpunkte setzen
    int y = y1;

    if ( dy < -dx )       // Steigung < 1
    {      
      while( x != (x2+1) )
      {
        e += dy;
        if ( e > 0) 
        {
          e += dx;
          y += yIncrement;
        }
        g.drawLine( x, y, x, y );
        x += xIncrement;
      }
    }
    else // ( dy >= -dx )   Steigung >=1
    {
      // an der Winkelhalbierenden spiegeln
      tmp = -dx;
      dx = -dy;
      dy = tmp;

      e = 2*dy + dx;

      while( y != (y2+1) )
      {
        e += dy;
        if( e > 0 ) {
          e += dx;
          x += xIncrement;
        }
        g.drawLine( x, y, x, y );
        y += yIncrement;
      }
    }
  }
  
  private void line( int x1, int y1, int x2, int y2 ) {
      if ( lineType == BRESENHAM )
          drawBresenhamLine( x1, y1, x2, y2 );
      else
          g.drawLine( x1, y1, x2, y2 );
  }

  public void paint( Graphics g ) {
    this.g = g;

    for ( int i=30; i < 300; i+=20 )
      line( 10+i, 40, 300-i, 100 );
  }
  
  public static void main( String[] args ) {
    GDV line = new GDV();

//    line.lineType = CLASSIC;
    line.lineType = BRESENHAM;

    line.show();
   }
}

Bresenhams Algorithmus arbeitet ohne Frage schnell. Wir können aber noch einige Erweiterungen programmieren: Wie wird eine antialiased Linie, also eine weiche Linie gezeichnet? Es gibt mehrere

Mögichkeiten, wobei aber nicht alle schnell sind. Eine Lösung besteht darin, auf jeden Punkt einen Filter loszulassen. Dies läuft aber auf viele Multiplikationen hinaus. Bekannt geworden ist ein anderer

Ansatz, ein Algoritmus von Gupta—Sproull (1981). Dieser ist in allen bekannten Grafik-Büchern nachzulesen.

Da der Algorithmus von Bresenham sehr schnell ist, wundert es einen, wenn wir noch von einer Geschwindigkeitsoptimierung sprechen. Das ist tatsächlich möglich und wurde von Angel, Edward

und Don Morrison in einem Aufsatz in den “IEEE Computer Graphics and Applications” auf zwei Seiten Ende 1991 beschrieben. Der Hintergrund ist einfach und kann daher kurz mit einer Programmieraufgabe umgesetzt werden: Die Pixelbewegungen wiederholen sich zyklisch. In einer Linie der Steigung 1, ist mit jedem Erhöhen des X-Zählers auch ein Erhöhen des Y-Zählers Verbunden. Eine Gerade mit der Steigung 1/8 setzt vier Punkte auf der Horizontalen, geht dann einen Pixel nach oben und setzt wieder vier Punkte. Können wir den Rhythmus erkennen, dann haben wir es einfach, denn dann müssen uns nur die Zeichentechnik merken und dann immer wieder kopieren. Für parallele Prozesse gibt es nichts schöneres. Wir berechnen dazu den Größten Gemeinsamen Teiler von Δy und -Δx. Ist dieser echt größer als Eins, dann haben wir damit den ersten Punkt, an dem die Berechnungsfolge von vorne beginnt. Der Punkt zeichnet sich durch die Koordinaten (x1 + a/ggT(Δy,  -Δx, y1 + b/ggT(Δy/-yΔx) aus.

Thema der Woche: XPath und XSLT

Refactoring mit Alt + Shift + R geht nicht mehr? RadioSure kann Schuld sein

Seit einiger Zeit gab es einen merkwürden Effekt: Zeitweilig ging das Refactoring über den Shortcut Alt + Shift + R nicht mehr. Ich dachte erst an ein komisches Eclipse-Plugin, aber selbst Eclipse wollte zu dem Shortcut gar keine Operation anzeigen, es ging bis Alt + Shift und das R schluckte das System schon. Damit war klar, dass die Tastenkombination erst gar nicht bei Eclipse ankam – es musste ein anderes Windows-Programm sein. Ich schaute auf die Liste und dann fiel mir sofort RadioSure auf, und dann jetzt verstand ich auch, warum ich immer wieder unbeabsichtigte Aufnahmen hatte: Die Recording-Funktionalität wird exakt mit Alt + Shift + R getoggelt.

RadioSureEclipseShortConflict

Nach dem Deaktivieren war dann auch mit dem Refactoring alles wieder in Ordnung.