Nutzung von Office-Produkten als Server-Anwendung durch Java-Clients

Java meets Office

Bernhard Scherm, Jürgen Stöckel

Ein Großteil der heute üblichen Geschäftsapplikationen produziert seinen Output immer noch auf Papier. Dabei werden an das Layout in Zeiten von Microsoft Word und dessen Konkurrenten immer größere Anforderungen gestellt. Wie bewerkstelligt nun der geneigte Java-Applikations-Entwickler diesen Trend? Der folgende Artikel zeigt Möglichkeiten auf wie man zu guten Ergebnissen kommen kann.

Es gibt dazu zwei Alternativen: Die Erste ermöglicht die Implementierung eigener Layout- und Druckfunktionen in der Anwendung durch die in Java2 enthaltenen 2D-Grafik- und Druck-Fähigkeiten. Es können damit alle denkbaren Dokumente erzeugt und gerendert werden und sowohl am Bildschirm als auch am Drucker ausgegeben werden. Der Aufwand, der hier beim Rendern von Dokumenten betrieben werden muss, ist jedoch immens. Zu beachten ist auch, dass die Druck-API in der Version 1.3.1 noch immer nicht zu den stärksten Bereichen der Java-Laufzeit-Umgebung gehört. Außerdem ist der Aufbau der Dokumente inklusive aller Logos etc. damit komplett im Java-Programm zementiert und kann nicht durch den Benutzer des Programms angepasst werden. Sollten Änderungen an den Dokumenten erforderlich werden, wäre immer der Programmierer gefragt. Selbst für jede kleine Formular-Anpassung müsste der Kunde sein Portemonnaie zücken.

Die zweite Alternative setzt sich zusammen aus einer Verbindung einer Java-Applikation und einem Office-Paket zu einem Client-Server-Gespann. Für das Erstellen von zeitgemäßem Text-Output und das Layouten von Dokumenten haben sich Produkte durchgesetzt wie sie z. B. im Microsoft-Office-Paket enthalten sind. Word & Co. haben außerdem regelmäßig auch eine Programmierschnittstelle im Angebot (API – Application-Programming-Interface), die ein Fernsteuern der Produkte über einen entsprechenden Client ermöglichen. Solche Produkte können darüber hinaus am Arbeitsplatz eines jeden Kunden erwartet werden und stellen damit keine zusätzlich erforderliche Investition dar. Wieso nicht die in diesen Produkten enthaltene Funktionalität nutzen und stattdessen das Rad immer neu erfinden? Ein weiterer Vorteil einer solchen Arbeitsteilung liegt darin, dass der Kunde die Administration seiner Dokument-Vorlagen mit seinem Office-Paket selbst vornehmen kann.

Es stellt sich nun die Frage wie man konkret die vorhandenen Office-Produkte sinnvoller nutzen könnte. Und zwar muss es möglich sein, über die Produkte neue Dokumente – gegebenenfalls nach einem bestimmten Strickmuster (Vorlagen-Nutzung!) zu erzeugen und im Dateisystem abzulegen. Diese Dokumente müssen sich dynamisch mit Daten aus der Java-Client-Anwendung füllen lassen. Man sollte die Dokumente anschließend zur Anzeige bringen bzw. drucken können. Schematisch kann man sich das Zusammenspiel zwischen Java-Applikation und Office-Produkt so vorstellen wie dies in Abpictureung 1 zu sehen ist.

Abb. 1: Zusammenspiel von Java-Applikation und Office-Produkt

In einer Office-Schnittstellen-Klasse wird für die Client-Seite definiert, was man sich an Verhaltensweisen vom Office-Produkt wünscht (z. B. Öffne Dokument xy). Für jedes Produkt, für das eine Anbindung erreicht werden soll, ist anschließend eine Ableitung zu erstellen, in der die vom Java-Client aufgerufenen Methoden der Schnittstelle in die für das jeweilige Office-Produkt und dessen API passenden Aufrufe umgesetzt werden.

Die geforderten Verhaltensweisen lassen sich in einer ersten Ausbaustufe zusammenfassen (Listing 1).

Listing 1

package office.client;

/**
* Abstrakte Klasse TextDocument
* Schnittstelle zu einem Text-Dokument eines Office-Produktes
*/
public abstract class TextDocument {

/**
* Öffnet ein vorhandenes Dokument, das unter  gespeichert ist.
*/
public TextDocument(String fileName) throws OfficeException {
}

/**
* Erzeugt ein neues Dokument auf Basis der Vorlage 

Die Office-Produkte der Firma Microsoft machen es dem Java-Programmierer nicht leicht als Client ihre Funktionen zu benutzen. Diese können nämlich nicht direkt aus einem Javaprogramm heraus benutzt werden, zumindest nicht aus reinem Java, da sie nur über die so genannte COM-oder OLE-Schnittstelle dieser Produkte (Component Object Model bzw. Object Linking and Embedding der Firma Microsoft) erreichbar sind. Diese Schnittstelle ist nur aus Visual Basic bzw. aus C++ heraus erreichbar. Für eine Nutzung mit Java kommen nur entsprechende Wrapper-Klassen in Frage, die für den Zugriff auf die COM-Objekte und -Schnittstellen als nativ deklarierte Methoden enthalten. Für diese nativen Methoden ist über das JDK-Tool javah eine C-Header-Datei zu erzeugen. Anschließend sind die in der Header-Datei deklarierten JNI-Methoden in der Programmiersprache C zu implementieren. Dieser C-Code pictureet die Plattform, von der aus die COM-Objekte über ihre C++-Schnittstellen erreicht werden können. Vor Nutzung der Wrapper-Klassen mit ihren nativen Methoden ist sicherzustellen, dass die DLL (Dynamic Link Library) geladen wird, die den nativen Code enthält. Will man hier selbst in Java und C++ programmieren, vermisst man bald eine ausführliche Dokumentation der C++-COM-Schnittstelle der Office-Produkte. Am besten kommt man hier voran, wenn man die VBA-Dokumentation (VBA-Editor mit Objekt-Katalog und Hilfedokumenten) hilfsweise heranzieht.

Es gibt kommerzielle Produkte wie z.B. Java2COM der Firma Neva Object Technology [1], die den beschriebenen Weg nutzen. Mit ihren Bibliotheken wird es dem Programmierer ermöglicht, eine Java-Sicht auf die COM-Schnittstellen (COM-Interface IUnknown und seine Ableitungen) zu erhalten. Die wichtigsten Java-Klassen, die durch Java2COM zur Verfügung gestellt werden, sind:

  • com.neva.GUID: 128-bit-Ganzzahl, die zur eindeutigen Identifikation von COM-Klassen und -Interfaces verwendet wird.
  • com.neva.COMHResult: 32-bit-Zeiger zu einem COM-Ergebnis-Objekt, dem Rückgabewert einer jeden Interface-Methode.
  • com.neva.COMClassObject: enthält das Protokoll zur Erzeugung von COM-Klassen-Factory-Objekten. Diese sind beim COM-Server vorhanden, um seine spezifischen Klassen entsprechend ihrer GUID-Kennungen zu instanzieren.
  • com.neva.COMIUnknown: Wrapper für das Basis COM-Interface IUnknown.
  • com.neva.COMIDispatch: implementiert den Wrapper für das Idispatch-Interface, das Arbeitspferd für die OLE-Automation.
  • com.neva.COMVariant: Wrapper für die Klasse Variant in OLE, die jede Art von Daten repräsentieren kann. Sie wird hauptsächlich in der Methode Invoke der Klasse IDispatch verwendet, um Daten zu übergeben oder zurückzugeben.
  • com.neva.COMException: Wrapper für COM-Exceptions

Wer selbst mit diesem Produkt experimentieren will, sei auf dessen gute und mit Beispielen untermauerte Dokumentation verwiesen, die genau wie das Produkt selbst über das Internet bezogen werden kann.

Einen ähnlichen Ansatz verfolgt ein Open-Source-Projekt namens Jacob (Java-Com-Bridge) [2], das auch einen Abstecher wert ist. Die Nutzung der keineswegs abschließend aufgezählten Produkte für die COM-Anbindung in Java bringt für komplexere Anwendungsfälle wie z.B. das Ersetzen einer großen Anzahl von Textmarken in einem MS-Word-Dokument das Problem mit sich, dass es zu Perfomance-Problemen kommen kann. Diese sind darin begründet, dass mit jedem Attribut-Zugriff und jedem entfernten Methodenaufruf der teure Weg durch das Java-Native-Interface, um anschließend vom nativen Teil der Schnittstelle aus über die Prozessgrenzen hinweg zum MS-Word-Prozess und von dort wieder zurück in den Java-Prozess zurückgelegt werden muss. Diesen Performance-Engpass kann man minimieren, wenn man in eine selbst programmierte C++-Schicht komplexere Funktionen legt und diese anstatt der in Java gewrappten COM-Schnittstellen und -Interfaces aus Java über das JNI benutzt. Es reduziert sich dadurch die Anzahl der Zugriffe durch das JNI und die damit erforderlichen Datenkonvertierungen. Der schematische Aufbau dieser Art von Office-Schnittstelle geht aus Abpictureung 2 hervor.

Abb. 2: Schematischer Aufbau einer Office-Schnittstelle

StarOffice ist ein Office-Produkt mit langer Tradition, das bereits seit dem Beginn des PC-Zeitalters von der Hamburger Firma Star-Division erstellt und vertrieben wurde. Eine Wende in der Entwicklung von StarOffice pictureete die Übernahme des Produktes durch die Firma Sun Microsystems im August 1999. Sun hat StarOffice als OpenOffice [3] der Open Source Gemeinde zur Verfügung gestellt und will auf diese Weise die Weiterentwicklung des Produktes vorantreiben. Interessierte OpenSource-Entwickler sind nun aufgefordert sich aktiv an der Entwicklung von OpenOffice zu beteiligen. Die Umwandlung von StarOffice in OpenOffice bedeutet allerdings nicht, dass es StarOffice in Zukunft nicht mehr geben wird. Unter dem Markennamen StarOffice wird durch die Firma Sun weiterhin ein leicht bedienbares Produkt zur Verfügung gestellt werden, das sich leicht installieren lässt, eine hohe gesicherte Qualität hat und für das es auch Support von Sun geben wird. Sun wird also eine OpenOffice-Distribution namens „StarOffice“ herausbringen, die sich aus Sicht eines Endbenutzers nicht von den bisherigen StarOffice-Versionen unterscheiden wird. Wenn man möchte, kann man wie gehabt eine CD mit Handbuch kaufen. Selbst die zugekauften Bestandteile, die nicht im OpenOffice enthalten sind, kann Sun wie gewohnt beifügen. Zur Zeit ist eine Testversion von StarOffice 6.0 erhältlich. Das Final Release dieser Version soll im 3. Quartal 2002 erscheinen.

Es stellt sich die Frage, welche Gründe für OpenOffice sprechen und warum man sich zur Zeit damit beschäftigen soll, obwohl es doch momentan noch nicht in einer endgültigen Version erhältlich ist. Ein gewichtiges Argument ist der Preis. Mit OpenOffice erhält man ein vollständiges Office-Paket kostenlos zur Verfügung gestellt, das sich keinesfalls hinter der erfahrenen (eXPerience-) Konkurrenz zu verstecken braucht. Das Produkt ist nicht auf eine Plattform beschränkt, sondern für die Windows-, Solaris- und Linux-Plattform erhältlich. Für den Java-Entwickler ist eine komplette Entwickler-API enthalten, damit das Produkt in eine eigene Applikation eingebunden werden kann. Alles, was für die Schnittstelle benötigt wird, ist bereits nach der Installation von OpenOffice vorhanden, ohne dass hierfür weitere Bibliotheken oder Archive installiert werden müssen. Es gibt oder gab zwar auch für StarOffice 5.2 bereits eine API für die Fernsteuerung des Paketes. Diese soll aber nicht mehr weiterentwickelt werden und wird deshalb hier nicht weiter behandelt.

Abb. 3: Architektur des OpenOffice API

Unterhalb der abstrakten Systemschicht werden verschiedene Objekttechnologien unterstützt. Dies ist neben dem Microsoft-Windows-Standard COM/OLE und neben CORBA vor allem das Universal Network Object Model (UNO). Bevor man versucht mit Hilfe eines Javaprogrammes einen Zugriff auf OpenOffice zu erreichen, sollte man zunächst einige theoretische Hintergründe über dieses eigenentwickelte neue Objektmodell kennen lernen. Dieses Komponentenmodel wird von der OpenOffice-Community mit Unterstützung von Sun Microsystems entwickelt und hat das Ziel die Kommunikation zwischen verschiedenen Programmiersprachen, Objektmodellen, Maschinenarchitekturen und verschiedenen Prozessen zu ermöglichen. Als Einsatzgebiet für UNO sollen neben einer lokalen Maschine auch ein Netzwerk oder das Internet genutzt werden können.

Als Beispielapplikation für UNO nennt die OpenOffice-Community neben OpenOffice auch das Sun ONE Webtop. Hier soll UNO für die Kommunikation zwischen einem Browser-Plugin und dem Office Applikations-Server eingesetzt werden. Außerdem soll UNO innerhalb des Sun ONE Webtops für die Kommunikation zwischen Java Server Pages und einen als C++-Prozess laufenden Universal Content Broker genutzt werden, der wiederum für die Datenbankzugriffe zuständig ist. Entwickler, die OpenOffice oder den Sun ONE Webtop nutzen, ändern oder deren Funktionalität erweitern wollen, werden sich mit UNO auseinander setzen müssen.

UNO ist nicht auf OpenOffice beschränkt. Die Basis-Bibliotheken von UNO sind kein Teil von OpenOffice und können als Framework innerhalb eigener Applikationen verwendet werden. UNO ist kostenlos erhältlich und unterliegt der LGPL-Lizenz (GNU Lesser General Public License). Es unterstützt C, C++ und Java.

Ähnlich COM und CORBA basiert auch UNO auf der Nutzung von Interfaces. Die angebotenen Komponenten implementieren diese Interfaces und füllen Sie mit Leben aus. Die eigentliche Kommunikation erfolgt durch Aufruf der in den Interfaces definierten Methoden. Durch die Nutzung von Interfaces gewinnt das Komponentenmodel eine enorme Flexibilität. Zum einen können die Interfaces in verschiedenen Programmiersprachen implementiert werden, zum anderen können einzelne Module problemlos ausgetauscht werden.

Als Ablaufumgebung für die einzelnen UNO-Komponenten dient ein Uno Runtime Environment (URE). Eine URE identifiziert sich anhand der verwendeten Programmiersprachen (C++, Java) und dem aktuellen Prozess. Es gibt keinen Performance-Overhead für Komponenten, die die selbe URE nutzen. Zum Beispiel handelt es sich bei einem Aufruf einer Komponente A durch eine Komponente B innerhalb einer URE nur um einen virtuellen Call, bei dem kein eigener Prozess gestartet wird. Für die Aufrufe von Komponenten mittels verschiedener UREs werden Bridges verwendet. Zur Zeit existieren vier Bridges in UNO, eine C++-Bridge, eine Java-Bridge, die UNO Remote-Protocol-Bridge und die IIOP-Bridge. Da OpenOffice in C++ implementiert ist, nutzt es intern zum größten Teil die C++-URE. Für den Aufruf aus einem Javaprogramm wird die Java-C++-Bridge von UNO verwendet.

Alle UNO-Interfaces sind innerhalb einer IDL (Interface-Definition-Language) spezifiziert und erben von einem Superinterface namens com.sun.star.uno.XInterface, welches die Methoden acquire, release und queryInterface bietet. Der Lebenszyklus eines UNO-Objekts wird mit Hilfe eines globalen Referenzzählers organisiert, die Fehlerbehandlung erfolgt über definierte Ausnahmen (Exceptions).
UNO garantiert die Identität von Objekten und Threads. Die Reihenfolge der Aufrufe zwischen den UREs wird sichergestellt, das bedeutet im Einzelnen:

  • Objekt-Identität: Sofern zwei Interface-Referenzen auf dasselbe Objekt zeigen, garantiert UNO, dass beim Vergleich der beiden Referenzen Gleichheit ermittelt wird.
  • Thread-Identität: In UNO wird jeder Thread mittels eines globalen einmaligen Threadzählers gekennzeichnet.
  • Verlässt ein Thread den Prozess über eine der vorhandenen Bridges, ist sichergestellt, dass bei der Rückkehr aus dem aufgerufenen Prozess der richtige Aufrufer-Thread mit samt seiner Umgebung ermittelt und fortgesetzt wird.
  • Reihenfolge der Aufrufe: UNO erlaubt Methoden synchron oder asynchron zu deklarieren. Mehrere synchrone Aufrufe werden garantiert in der Aufruf-Reihenfolge abgearbeitet.

Es stellt sich die Frage, warum Sun oder die OpenOffice-Gemeinde mit UNO ein neues Komponentenmodell entwickelt hat, statt eines der vorhandenen Modelle wie etwa COM/DCOM, CORBA oder Java RMI zu nutzen. Der Hauptgrund liegt darin, dass die genannten Objektmodelle nicht die für StarOffice oder den Sun ONE Webtop benötigten Funktionalitäten in der benötigten Performance liefern bzw. für den geforderten Funktionsumfang nicht ausreichen:

  • COM/DCOM erlauben es nicht, Exceptions zu verwenden;
  • CORBA ist nur für Remote-Kommunikation Standard. Es gibt nur eine sehr einfache Unterstützung für Prozesskommunikation (IIOP zwischen verschiedenen Prozessen). Diese wird für die Anforderungen in OpenOffice als zu langsam erachtet.
  • Java RMI kann nur zwischen Javaprogrammen genutzt werden, da UNO zwischen verschiedenen Programmiersprachen kommunizieren will, ist dies nicht ausreichend.

Zusätzlich stellt sich das Problem, dass der generierte Code, der z.B. für COM oder CORBA gebraucht wird, bei typenreichen Anwendungen riesige Bibliotheken erzeugt. Wer sich genauer mit UNO auseinander setzen möchte oder dieses Komponentenmodell in eigenen Applikationen nutzen möchte, soll an dieser Stelle an die Website von OpenOffice verwiesen werden. Dort können nicht nur weitere Informationen zu den genauen technischen Details von UNO nachgelesen werden, es können auch Tutorials über die Implementierung von eigenen UNO-Komponenten gefunden werden.

Services, Interfaces und Properties in der OpenOffice API

Wir wollen uns hier lieber mit der Frage beschäftigen wie das UNO-Komponentenmodell in der OpenOffice-API umgesetzt wurde und wie mit Hilfe von UNO eine Verbindung aus einer eigenen Applikation mit OpenOffice erstellt werden kann. Die OpenOffice-API unterstützt vor allem zwei Konzepte: Services und Interfaces. Unter Service wird hierbei ein abstraktes Konzept zur Unterstützung bestimmter Interfaces und Eigenschaften verstanden. Unter Interface versteht man eine Sammlung von Methoden, die das Ziel haben, eine bestimmte Funktionalität zur Verfügung zu stellen.

In den verschiedenen Paketen der API werden zahlreiche Services, Interfaces, Typen, Enumerationen und Datenstrukturen gruppiert. Beispiel für diese Pakete sind etwa das Paket com.sun.star.text oder com.sun.star.sheet, die die Textverarbeitungskomponente bzw. das Kalkulationsmodul von OpenOffice zugänglich machen. Es wäre aber falsch anzunehmen alle Pakete innerhalb der API ständen in einer Eins-zu-Eins-Beziehung mit genau einem Teil von OpenOffice. Es gibt etwa die übergreifenden Pakete com.sun.star.document und com.sun.star.style, die sich natürlich in allen Modulen von OpenOffice wiederfinden. Einige interessante Pakete innerhalb der API sind:

  • com.sun.star.drawing –> Zeichnen
  • com.sun.star.frame –> Fenstersteuerung
  • com.sun.star.presentation –> Präsentation
  • com.sun.star.sheet –> Tabellenkalkulation
  • com.sun.star.table –> tabellenrelevante Interfaces
  • com.sun.star.text –> Textverarbeitung

Die einzelnen Komponenten von OpenOffice bieten die Services der API an. Es handelt sich dabei jedoch um keinen definierten Objektbaum, durch den man navigieren und den man direkt benutzen könnte. Um ein Feature eines OpenOffice-Services in Form eines bestimmten Interfaces benutzen zu können, muss man sich vielmehr zunächst ein Objekt als Statthalter für das benötigte Interface beschaffen; dies geschieht in der Regel über eine ServiceFactory. Anschließend kann man sich die Objekt-Instanz von der UNO-Laufzeitumgebung mit dem gewünschten Interface belegen lassen.

POI – Armselige Verwirrung

Unter der Bezeichnung POI (Poor Obfuscation Implementation) startete vor kurzem ein neues Open-Source-Projekt [4], das inzwischen in die Verantwortung von Apache-Jakarta gestellt wurde. Dieses Projekt hat das Ziel die Microsoft-Office-Dokument-Formate vor allem für Excel und Word zu entschlüsseln und in einem Java-Framework für die Bearbeitung (Erzeugen, Lesen, Verändern und Serialisierung) zur Verfügung zu stellen.

Dieses Projekt geht in eine etwas andere Richtung als die im Artikel beschriebenen Ansätze bzw. Projekte. Dort geht es ja jeweils darum eine Office-Applikation fern zu steuern und dem Benutzer den Dialog nicht nur mit der eigenen Anwendung, sondern auch mit der Office-Anwendung zu ermöglichen. Außerdem ist es weniger aufwändig und änderungsfreundlicher die statischen Teile der Dokumente über die Office-Produkte selbst in Dokumentvorlagen zu speichern und daraus wieder zu verwenden, anstatt sie aus einem POI-Client-Java-Programm zu erzeugen. Bisher wurde von diesem Projekt das Excel-97-Dokumentformat XLS und die Dokument-Summary-Funktionen von Microsoft Office unterstützt. Aktuell wird an einer Umsetzung des Word-Dokumentformates gearbeitet. Das POI-Projekt ist ein pures Java Projekt und besteht bisher aus den folgenden Komponenten:

  • HSSF Serializer – hierbei handelt es sich um eine Sammlung von Java-Klassen, die das Serializer-Interface des Cocoon 2 -Projektes unterstützen. Bei Cocoon handelt es sich, wie sicherlich bekannt ist, um ein weiteres Projekt der Apache-Group. Cocoon hat das Ziel XML-Daten mit Hilfe von XSL in jedes gewünschte Format (HTML, PDF, XML, WML, XHTML) zu konvertieren. Mit dem HSSF Serializer des POI-Projektes hat man also die Möglichkeit die Daten im Excel-97-Format zu speichern.
  • HSSF library – bei dieser Bibliothek handelt es sich um eine Sammlung von Klassen, mit deren Hilfe Dateien im Excel-97-Format erzeugt, bearbeitet, gespeichert und natürlich auch wieder gelesen werden können.
  • POIFS library – bei der letzten Sammlung von Klassen handelt es sich um eine Bibliothek, mit deren Hilfe das Microsoft Compound-Document-Format gelesen und wieder gespeichert werden kann.

Für die Version 2 von POI sollen noch folgende Komponenten erstellt werden:

  • HSSF-Generator – mit Hilfe des HSSF-Generators soll es möglich sein, Excel-Dateien zu lesen und daraus SAX-Events zu erzeugen.
  • HDF library – Bei dieser Bibliothek handelt es sich um eine event-basierte API zum Lesen und Schreiben von Dokumenten im Microsoft-Word-97-Format.

Listing 2 zeigt ein Beispiel, wie POI verwendet werden kann, um ein Exceldokument zu erzeugen. Das Framework setzt darauf, dass ein Exceldokument aus einer Arbeitsmappe (Workbook) besteht, die einzelne Arbeitsblätter enthält. Diese Tabellenblätter bestehen natürlich aus Zeilen und Spalten und letztlich aus den einzelnen Zellen. Entsprechend dieser Baumstruktur wird das Exceldokument durch die einzelnen Objekte der HDDF-Library erschlossen und kann bearbeitet werden.

Ein weiterer ausführlicher Artikel in diesem Magazin geht näher auf POI ein.

Listing 2

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

import net.sourceforge.poi.hssf.usermodel.*;

public class TestPOI {
public static void main(String[] args) {
new TestPOI();
System.exit(0);
}
public TestPOI() {
try {
File file = new File("C:/temp/testPOI.xls");
FileOutputStream out = new FileOutputStream(file);
HSSFWorkbook wb = new HSSFWorkbook();
HSSFSheet s = wb.createSheet();
HSSFRow r = null;
HSSFCell c = null;
HSSFFont fnt = wb.createFont();
HSSFCellStyle cs = wb.createCellStyle();
fnt.setColor(HSSFFont.COLOR_RED);
fnt.setBoldweight(HSSFFont.BOLDWEIGHT_BOLD);
cs.setFont(fnt);
for (short rownum = (short) 0; rownum 
Geschrieben von
Bernhard Scherm, Jürgen Stöckel
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.