This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Die Reihe Xpert.press vermittelt Professionals in den Bereichen Softwareentwicklung, Internettechnologie und IT-Management aktuell und kompetent relevantes Fachwissen über Technologien und Produkte zur Entwicklung und Anwendung moderner Informationstechnologien.
Thomas Ekert
Java unter Lotus Domino Know-how für die Anwendungsentwicklung Mit 121 Abbildungen, 80 Listings und CD-ROM
123
Thomas Ekert Kommunikationsdesign Hamburg [email protected] www.tom-quadrat.de
Bibliografische Information der Deutschen Bibliothek Die Deutsche Bibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.ddb.de abrufbar.
Vorwort Mit einer explosionsartigen Verbreitung zog Java vor einigen Jahren auch in die Domino-Welt ein. Groß geworden mit Pascal, entsprach Java meinen Ansprüchen an die Ästhetik einer Programmiersprache, die gleichzeitig den aktuellen Anforderungen an die Leistungsfähigkeit moderner Anwendungen gerecht wird, und so machte ich Java schnell zum wichtigsten Standbein der Anwendungsentwicklung in vielen Projekten. Vor allem in der Webentwicklung versprach – und verspricht – die Kombination mit Java große Vorteile für die Domino-Anwendungsentwicklung und so lag es nahe, meine seit Mitte der 90er Jahre in der Entwicklung verschiedenster kleiner und großer Domino-Anwendungen gesammelte Erfahrung nun um den Einsatz von Java zu erweitern. Während für einen Domino-Programmierer – gewohnt an ein geschlossenes Entwicklungsframework bestehend aus dem Domino Designer, in dem alle Aspekte der Anwendungsentwicklung vom Debugging bis zur GUI Entwicklung abgedeckt werden – der Einsatz externer Komponenten aus Java neu ist und die Einarbeitung in neue Entwicklungsabläufe notwendig macht, steht der Java-Profi der komplexen und hoch integrierten Infrastruktur Dominos mit seinen ausgefeilten Sicherheitsmechanismen und der schier endlosen Zahl an Diensten und Anwendungen gegenüber. Im Rahmen der Entwicklungsarbeit an einem großen Projekt bei der Erstellung eines geschlossenen Anwenderportals einer Versicherung wurde im Team aus Domino- und Java-Entwicklern schnell klar, dass der Austausch von Know-how an erster Stelle stehen musste, um eine gemeinsame Sprache zu finden, die es den jeweiligen Spezialisten ermöglichte, die Bedürfnisse und Notwendigkeiten der Domino-Entwicklung auf der einen und der Java-Entwicklung auf der anderen Seite zu verstehen und zu berücksichtigen. Hierbei entstand auch die Idee zu diesem Buch, da sich herausstellte, dass es zwar an Beschreibungen des Domino-Java-APIs nicht mangelt, aber Praxiserfahrungen, Best-Practice-Beispiele und vor allem Know-how in Domino-spezifischen Design Pattern und Lösungsansätzen rar sind, was nicht zuletzt in der noch recht jungen Historie der Domino-Java-Entwicklung begründet liegt. So haben Domino- und Java-Entwickler jeweils einen spezifischen Blick auf die Aufgaben und Lösungsansätze für ihre eigenen Anforderungen und Bedürfnisse. Dieses Buch ist weder ausschließlich ein Handbuch für die Domino-Anwendungsentwicklung noch ein Java-Lehrbuch, sondern beleuchtet die speziellen Anforderungen der Domino-Anwendungsentwicklung in Java. Alle Fragestellungen, Lösungsansätze und Best-Practice-Beispiele sind aus der täglichen Praxis entnommen und weiterentwickelt und Bestandteil vieler tatsächlich im Einsatz befindlicher Anwendungen. Als begeisterter und bekennender XP-Programmierer sind automatisierte Tests und Qualitätssicherungsverfahren wichtige Bestandteile meiner Arbeit und so enthält dieses Buch – last but not least – ein umfangreiches Kapitel zum Debugging oder besser gesagt zu dessen Vermeidung, ergänzt durch Verfahren, die automatisierte Tests von Domino-Java-Anwendungen ermöglichen. Die Java-Programmierung für Domino ist aktueller denn je. Mit der derzeitigen Version gehören durch die DB2-Integration Themen wie SQL und J2EE nun zu den wichtigen Fragestellungen im Domino-Umfeld. Große Begeisterung und Erwartungen ruft der Blick auf die nächsten Domino-Generation hervor, deren Client auf Java
VI und dem Eclipse-Framework basieren wird. Damit wird die Basis für Domino-Plugins und Erweiterungen gelegt, die sich mittels des Domino-Java-API nahtlos integrieren lassen. Dieses Buch war nur durch die tägliche Arbeit und im Austausch mit vielen Domino- und Java-Spezialisten in meinem Umfeld möglich. Ich möchte daher meinen MitarbeiterInnen, aber auch den KollegInnen bei unseren Kunden danken, die es durch Fragen, Gespräche, Anregung und Kritik möglich gemacht haben, Antworten und Lösungen zu finden und Design Pattern zu entwickeln. Eine große Stütze war der Springer-Verlag, der schnell von Idee und Thema überzeugt war und mich mit Geduld begleitet hat. Thorsten Dietsche gilt mein Respekt und Dank. Er hat mit Ausdauer das Manuskript konstruktiv und kritisch hinterfragt und viel zur Geschlossenheit des Inhalts beigetragen. Ihm verdanke ich unter anderem die Anregung für das Kapitel über die Kapselung von Schleifen. Danken möchte ich meinen beiden Familien und meinen Freunden, die mich während der fast zwei Jahre der Entstehung dieses Buches nicht vergessen haben. Mein besonderer Dank gilt Nico, die nicht nur auf viel gemeinsame Zeit verzichtet, sondern die mich antrieb, dieses Buch zu beginnen und dessen Abschluss erst ermöglicht hat.
Thomas Ekert Hamburg im April 2006
VII
Über dieses Buch Dieses Buch richtet sich vor allem an Java-Programmierer, die neu in der DominoWelt sind. Dementsprechend bietet der erste Teil eine Einführung in die für die JavaEntwicklung wichtigen Domino-Komponenten, stellt die Inhalte aber bereits für die Java-Sicht optimiert dar. So eignet sich dieses Buch auch für Domino-Anwendungsentwickler, die in die Domino-Java-Programmierung einsteigen, wobei allerdings solide Java-Grundkenntnisse vorausgesetzt werden. Um die praktische Arbeit zu erleichtern, werden an vielen Stellen administrative Techniken beschrieben, die notwendig sind, um bestimmte Verfahren anwenden oder zugehörige Services und Servertasks aktivieren zu können. Auch wird auf viele Aspekte der Sicherheitsfunktionen eingegangen, auch wenn diese über die reine Anwendungsentwicklung hinausgehen oder administrativer Natur sind. Teil I Domino ist zugleich Datenbank und Anwendungsserver und dazu eine Plattform für Kollaborationsanwendungen wie E-Mail, Kalender, Aufgaben oder Ressourcenverwaltung. Datentypen, Gestaltungselemente und der Domino Designer liefern die Grundlage, auf der das Buch aufbaut. Masken, Ansichten, Agenten und natürlich der XMLähnlichen, offenen Schemastruktur Dominos gilt das Hauptaugenmerk, stellen diese doch die Grundlagen für sämtliche Anwendungen dar. Im ersten Teil dieses Buches werden die wichtigsten Komponenten erläutert und dienen als Basiswissen für die weiteren Kapitel. Teil II Der zweite Teil dieses Buches legt die Grundlagen für jede Domino-Java-Anwendung und erweitert dieses Wissen durch die Erläuterung von Best-Practice-Beispielen, Design Pattern und Spezialfällen. Die Domino-Session, als lokale oder remote Anwendung, bildet den Einstieg, ist sie doch das Herz einer jeden Domino-Java-Anwendung, erzeugt durch die NotesFactory, der zwei Kapitel gewidmet sind. Die Handhabung der Domino-Objekte erfordert viel Spezialwisssen, so dass sich jeweils ein Kapitel mit den Objekten Session, Document, Database, View und RichText beschäftigt. Hierbei ist der Überblick über die gesamte Objektstruktur ebenso berücksichtigt wie die Aufgabenstellungen der täglichen Arbeit, von der Performanceoptimierung über die Handhabung von Threads bis hin zur Garbage-Collection und dem in Domino sehr wichtigen Recycling, dem ebenfalls ein eigenes Kapitel gewidmet ist. Werden die wichtigsten Objekte beherrscht, geht es nun an den praktischen Einsatz. Dieser wird vor allem in den Kapiteln 13 bis 15 über die Objekt- und Speichertechniken und die verschiedenen Suchalgorithmen berücksichtigt, so dass ein kompletter Wissensschatz entsteht, der den Aufbau solider, alltagstauglicher Java-Anwen-
VIII dungen für Domino ermöglicht. Dort findet sich eine Anleitung für den Einsatz von Domino mit DB2 ebenso wie ausführliche Modelle zur Erweiterung und Implementierung von Domino-Java-Objekten. Teil III Wie eingangs erwähnt, bietet vor allem der Einsatz im Web-Umfeld, aber auch in großen Enterprice-Infrastrukturen eine besondere Motivation für den Einsatz von Java für Domino. Daher liefert Teil III das Spezialwissen für dieses Einsatzgebiet von der Entwicklung von JSP-Seiten mit den domtags über das Wissen für die Einbindung von Domino in eine J2EE-Infrastruktur. Abgerundet wird das Buch durch das letzte Kapitel über Debugging, JUnit Tests, Logging und Qualitätssicherung. Sourcecode Alle Sourcecode-Listings der Beispiele dieses Buches liegen als vollständige Code Sammlung auf einer CD bei. Eine begleitende Web-Site befindet sich unter http:// www.tom-quadrat.de/dominojava. Darüber hinaus wurden der CD ergänzende Beispiele und Klassen beigefügt, die als Listing nicht abgedruckt wurden. Dies ist jeweils im Text erwähnt. Notationelle Konventionen Werden im Text Java-Klassen oder Notes-Formeln erwähnt, so sind diese in Courier gesetzt, es sei denn es ist allgemeinsprachig vom gleichnamigen Domino-Objekt die Rede (z.B. „Die Domino-Session“ oder „Die Klasse Session“). Den Beispiel-Listings im Buch wurden in der Regel grafische Nummernsymbole hinzugefügt, über die die jeweiligen Codestellen im Text referenziert werden. Methoden sind nicht immer in ihrer vollen Signatur genannt, insbesondere dann, wenn es mehrere Signaturen gibt und eine Methode im Allgemeinen erwähnt wird. In Tabelle 6-1 befindet sich eine vollständige Übersicht über alle Klassen des Domino-Java-API. Ein vollständiges Klassen- und Methodenverzeichnis ist in den Index im Anhang eingearbeitet.
Über den Autor Thomas Ekert ist seit 10 Jahren freier IT-Berater und führt seit 7 Jahren als CTO und geschäftsführender Gesellschafter die IT-Agentur BITSDONTBYTE. Als zertifizierter Domino- Java- und DB2-Entwickler ist er Spezialist für Domino- und Web-Application-Server-Entwicklung.
IX Als Hauptverantwortlicher für große Projekte für öffentliche und private Auftraggeber ist er erfahren in Controlling und Qualitätssicherung von umfangreichen TÜVgeprüften Java-basierten Domino-Projekten, in denen er neben Projektierung, Konzept und Leitung auch Schulungen in Unternehmen konzipiert und durchführt.
Inhalt
Teil 1 1
2
Notes und Domino ..................................................................1
Das Domino-Prinzip.................................................................................................3 1.1 Replikation ......................................................................................................5 1.2 Schemabasiertes Objekt- und Speichermodell..............................................6 1.3 Erweiterbarkeit................................................................................................7 1.4 Zugriffskontrolle und Verschlüsselung ..........................................................9 1.5 Geschichte .....................................................................................................11 1.6 Lotus Domino und Java................................................................................14 1.7 Lotus Domino als Datenbank ......................................................................16 1.8 Die Client-Server-Umgebung Lotus und Domino .....................................18 1.9 Zusammenfassung ........................................................................................20 Notes @ Domino .....................................................................................................23 2.1 „Hello WWWorld“ – URL-Loader-Anwendung.........................................24 2.1.1 Datenbank anlegen ..........................................................................25 2.1.2 Maske anlegen ..................................................................................26 2.1.3 Ansicht anlegen................................................................................32 2.1.4 Agent anlegen...................................................................................34 2.1.5 Die Anwendung testen.....................................................................38 2.1.6 Erweiterungen und Ausblick ...........................................................39 2.2 Das Domino-Speichermodell ......................................................................41 2.2.1 Die Domino-„Note“ ........................................................................41 2.2.2 Notes Document und Notes Form..................................................43 2.2.3 Item und Feld ...................................................................................49 2.2.4 Datentypen.......................................................................................55 2.2.4.1 Text und Textliste ...............................................................55 2.2.4.2 Namen, Autoren und Leser ...............................................56 2.2.4.3 Datum / Uhrzeit.................................................................57 2.2.4.4 Zahl und Zahlliste..............................................................59 2.2.4.5 Antwortreferenzliste ..........................................................60 2.2.4.6 DokLink-Referenzliste.......................................................60 2.2.4.7 Mime Part ..........................................................................60 2.2.4.8 RichText, RichText Light und Embedded Object.............61 2.3 Replikation ....................................................................................................62 2.3.1 Replik ID und Universal ID.............................................................62
XII
3
Inhaltsverzeichnis 2.3.2 Replikations- und Speicherkonflikte ..............................................65 2.4 Das Domino-Objektmodell (Einführung)..................................................67 2.5 Suchkonzepte (Einführung).........................................................................68 2.6 Sicherheitskonzepte ......................................................................................74 2.7 Zusammenfassung ........................................................................................76 Notes IDE ..........................................................................................................79 3.1 Der Domino Designer ..................................................................................80 3.2 Masken ..........................................................................................................85 3.2.1 Feld ...................................................................................................86 3.2.2 Ebene ................................................................................................88 3.2.3 Layout-Ebene ...................................................................................88 3.2.4 Aktionen...........................................................................................88 3.2.5 Ressourcen........................................................................................88 3.2.6 Seitengestaltungselemente...............................................................91 3.2.7 Abschnitte.........................................................................................91 3.2.8 Berechneter Text...............................................................................92 3.2.9 Hotspots ...........................................................................................92 3.2.10 Eingebettetes Element......................................................................92 3.2.11 Verwendung von Masken im Notes Client und im Browser .........92 3.2.12 Verwendung von Java in Masken ....................................................97 3.3 Ansichten und Ordner..................................................................................97 3.3.1 Spaltenberechnungen ......................................................................99 3.3.2 Sortieren und Kategorisieren ..........................................................99 3.3.3 SELECT-Formeln...........................................................................102 3.3.4 Größenbeschränkungen in Ansichten ..........................................104 3.4 Agenten........................................................................................................105 3.4.1 Agenten-Trigger .............................................................................105 3.4.2 Agenten-Targets .............................................................................108 3.5 Bibliotheken ................................................................................................111 3.6 Zusammenfassung ......................................................................................112
Teil 2 Java @Domino..........................................................................113 4
Java-Anwendungen @ Domino ............................................................................115 4.1 Domino- und J2SE- und J2EE-Anwendungen .........................................116 4.2 Vorbereitungen ...........................................................................................117 4.3 Lokale Notes Session...................................................................................119 4.4 Java und Domino........................................................................................121 4.5 Statische Notes Threads..............................................................................123 4.6 Session im statischen Thread .....................................................................126 4.7 NotesThread erweitern ...............................................................................127 4.8 Runnable implementieren..........................................................................128
Domino-Java-Applets.................................................................................128 Notes Agent im Domino Designer erstellen..............................................137 Agent in Domino Designer importieren ...................................................141 Agent in eine Java-Anwendung umwandeln .............................................143 Agenten in Notes-Masken ..........................................................................144 WebQueryOpen und WebQuerySave ........................................................147 Parameterübergabe zwischen Agenten und runOnServer........................148 Sicherheit bei der Agentenausführung ......................................................154 Debugging von Domino-Java-Agenten .....................................................159 Der Domino Agent Manager......................................................................160 Domino-Java-Agent und Domino-Java-Anwendung – Konzepte im Vergleich ...............................................................................161 4.20 Zusammenfassung ......................................................................................162 Java-Web-Anwendungen @ Domino ...................................................................165 5.1 Domino-Web-Agenten ...............................................................................166 5.2 Servlets.........................................................................................................171 5.2.1 Servlets im Domino Servlet Manager ...........................................174 5.2.2 Domino-Web-Agenten versus Domino-Servlets .........................177 5.2.3 Login und Internet-Session für Web-Agenten und Servlets........179 5.3 Remote Computing via DIIOP / CORBA .................................................184 5.3.1 Server und Client Setup von DIIOP / HTTP ...............................187 5.3.2 Domino-Remote-Session ..............................................................193 5.3.3 Remote-DIIOP-Aufrufe via IOR / getIOR....................................195 5.3.4 SSL ..................................................................................................197 5.3.5 Single Sign On................................................................................202 5.3.5.1 Setup von WebSphere und Domino für SSO via LtpaToken.........................................................................207 5.3.6 Connection Pooling – Object Request Broker .............................220 5.3.7 Die NotesFactory – Überblick.......................................................229 5.4 Troubleshooting .........................................................................................232 5.5 Zusammenfassung ......................................................................................236 Domino-Objekt- und Speichermodell.................................................................239 6.1 Objekt- und Datenstruktur ........................................................................240 6.1.1 Objekte, die über Session bezogen werden können .....................242 6.1.2 Objekte, die über Database bezogen werden können ..................243 6.1.3 Objekte, die über View bezogen werden können .........................243 6.1.4 Objekte in Document ....................................................................244 6.1.5 Domino-Objektklassen .................................................................244 6.2 Basis- und Sonderklassen ...........................................................................248 6.2.1 AgentBase .......................................................................................249 6.2.2 AppletBase und JAppletBase .........................................................250 6.2.3 NotesException und NotesError ...................................................252
XIV
7
8
9
Inhaltsverzeichnis 6.3 Zusammenfassung ......................................................................................253 Document......... .....................................................................................................255 7.1 Document und Item ...................................................................................256 7.1.1 Mit Items arbeiten .........................................................................257 7.1.2 Datentypen handhaben .................................................................259 7.1.3 Item Properties und weitere Methoden in Item...........................268 7.1.4 Mit Document arbeiten – Lifecycle eines Dokuments ................271 7.1.4.1 Dokumente selektieren über Methoden in Database ....271 7.1.4.2 Dokumente selektieren über Methoden in Document..273 7.1.4.3 Dokumente selektieren über vordefinierte DocumentCollections .....................................................275 7.1.4.4 Neue Dokumente erstellen..............................................276 7.1.4.5 Speichern von Dokumenten ...........................................278 7.1.4.6 Dokumente löschen.........................................................280 7.2 Profildokumente .........................................................................................282 7.3 Antwortdokumente ....................................................................................284 7.3.1 Antwortreferenzen über Self ID und Parent ID ...........................286 7.4 Attachments ................................................................................................288 7.4.1 Übersicht über die Methoden zur Attachmentbearbeitung ........298 7.5 Encryption und Signing .............................................................................302 7.5.1 Items mit geheimen Verschlüsselungsschlüsseln verschlüsseln ...304 7.5.2 Verwendung der Verschlüsselungsschlüssel..................................306 7.5.3 Fehlerquellen bei der Verwendung von Verschlüsselungsschlüsseln............................................................308 7.5.4 Verwendung von Verschlüsselungsschlüsseln über DIIOP..........310 7.5.5 Verschlüsselung und Signatur von Dokumenten.........................311 7.5.6 Verwendung öffentlicher Verschlüsselungsschlüssel....................311 7.5.7 Signieren von Daten.......................................................................311 7.5.8 Versenden von signierten und verschlüsselten Daten ..................314 7.6 Document Properties und weitere Methoden...........................................315 7.7 Document Locking .....................................................................................319 7.8 Zusammenfassung ......................................................................................325 Session....................................................................................................................327 8.1 Bezug von Datenbanken.............................................................................328 8.2 Einfache unabhängige Stil- und Eigenschafts-Objekte.............................329 8.3 Service-Anwendungen................................................................................331 8.4 Ausführen verschiedener Befehle ...............................................................334 8.5 Verschiedene Eigenschaften........................................................................336 8.6 Zusammenfassung ......................................................................................338 Database.................................................................................................................339 9.1 Datenbanken öffnen, schliessen, replizieren und löschen ........................340 9.1.1 Vergleich – Methoden zum Öffnen einer Datenbank ..................344
Inhaltsverzeichnis 9.2 9.3
XV
Datenbanken, Dokumente und Document Collection ............................345 Allgemeine Datenbankeigenschaften .........................................................346 9.3.1 Weitere Datenbankeigenschaften ..................................................350 9.4 Suche und Catalog ......................................................................................352 9.5 Sicherheit.....................................................................................................355 9.6 Designelemente und Ordner......................................................................361 9.6.1 Designspezifische Methoden in Database ....................................362 9.7 Locking und Signatur .................................................................................365 9.8 Service Tasks................................................................................................365 9.9 Zusammenfassung ......................................................................................366 10 View........................................................................................................................367 10.1 Updates, Indizes und Performance ............................................................369 10.2 Programmatische Manipulation des Designs von Views..........................370 10.3 Allgemeine Properties und Methoden.......................................................377 10.4 View und Document ..................................................................................380 10.5 ViewNavigator, View und ViewEntry.........................................................384 10.6 Locking ........................................................................................................401 10.7 DocumentCollection und ViewEntryCollection.......................................401 10.7.1 Methoden in DocumentCollection...............................................406 10.7.2 Methoden in ViewEntryCollection ...............................................410 10.8 Schleifen – View, DocumentCollection und ViewEntryCollection..........413 10.9 Kapselung von Schleifen.............................................................................420 10.10 Zusammenfassung ......................................................................................423 11 RichText............ .....................................................................................................425 11.1 Arbeiten mit RichText.................................................................................426 11.2 RichTextItem ...............................................................................................429 11.2.1 Methoden in RichTextItem ...........................................................429 11.3 RichTextNavigator ......................................................................................435 11.3.1 Methoden in RichTextNavigator...................................................437 11.4 RichTextRange ............................................................................................439 11.4.1 Methoden in RichTextRange .........................................................440 11.5 RichTextSection ..........................................................................................442 11.5.1 Methoden in RichTextSection .......................................................442 11.6 RichTextTable ..............................................................................................442 11.6.1 Methoden in RichTextTable ..........................................................443 11.6.2 Im- und Export von RichText – Erzeugen von RichText .............445 11.7 RichTextTab.................................................................................................452 11.7.1 Methoden in RichTextTab .............................................................453 11.8 RichTextDoclink .........................................................................................453 11.8.1 Methoden in RichTextDoclink......................................................453 11.9 RichTextParagraphStyle..............................................................................455 11.9.1 Methoden in RichTextParagraphStyle ..........................................456
XVI
Inhaltsverzeichnis
11.10 RichTextStyle...............................................................................................459 11.10.1 Methoden in RichTextStyle ...........................................................459 11.11 Zusammenfassung ......................................................................................461 12 Weitere Objekte .....................................................................................................463 12.1 Das Name-Objekt .......................................................................................464 12.1.1 Methoden in Name für RFC 821 und 822 Namen.......................466 12.1.2 Methoden in Name für Notesnamen (X.400) ..............................467 12.2 Arbeiten mit Zeit-Objekten........................................................................469 12.2.1 Methoden in DateTime .................................................................470 12.2.2 Methoden in DateRange................................................................473 12.3 Arbeiten mit internationalen Einstellungen..............................................474 12.3.1 Methoden in International............................................................474 12.4 Methoden in Agent .....................................................................................475 12.5 Zusammenfassung ......................................................................................478 13 Objekt- und Speichertechniken............................................................................479 13.1 Erweitern der Objekt-Funktionalitäten.....................................................480 13.1.1 Erweiterung der Document-Funktionalitäten .............................481 13.1.2 Pseudoimplementierung von lotus.domino.Document..............485 13.1.3 Implementieren von Domino-Java-Objekten ..............................490 13.2 Cashing von Domino-Objekten.................................................................498 13.2.1 Das DJCacheDocument.................................................................499 13.2.2 Performancevergleich ....................................................................512 13.2.3 Verwendung des DJCacheDocument ...........................................513 13.3 Domino-Objekte in Multithreading-Umgebungen..................................514 13.3.1 Gemeinsame oder geteilte Session ................................................515 13.3.2 DbDirectory darf nicht über Threads geteilt werden ..................516 13.3.3 Dokumente und Multithreading...................................................516 13.3.4 Profildokument und Multithreading............................................518 13.3.5 Dokumente löschen im Multithreading .......................................518 13.3.6 Parent- und Child-Objekte im Multithreading............................518 13.3.7 Multithreading mit gemeinsamer Session ....................................519 13.3.8 Multithreading ohne gemeinsame Session ...................................522 13.4 Domino und DB2 .......................................................................................523 13.4.1 Einführung .....................................................................................523 13.4.2 Struktur ..........................................................................................525 13.4.3 Sicherheit, Benutzer und Gruppen ...............................................529 13.4.4 Setup ...............................................................................................535 13.4.5 DB2 Access View ............................................................................545 13.4.5.1 DB2 Access View – Ein Beispiel.......................................549 13.4.6 Query View.....................................................................................558 13.4.6.1 Query View – Ein Beispiel ...............................................563 13.4.7 Federated Data – Entfernte SQL-Daten und Domino in DB2 ....566
Inhaltsverzeichnis
XVII
13.5 Zusammenfassung ......................................................................................566 14 Recyling von Domino-Objekten...........................................................................569 14.1 Notwendigkeit des Recyclings ....................................................................570 14.2 Grundregeln des Recyclings .......................................................................571 14.3 Besonderheiten in verschiedenen Umgebungen .......................................582 14.3.1 Recycle (Vector) .............................................................................582 14.3.2 Unterschiede zwischen R5 und R6/7 ............................................583 14.3.3 Temporäre Dateien ........................................................................584 14.4 Vereinfachung – Die DJBuch-GC-Klasse ..................................................585 14.5 Recycling im MultiThreading.....................................................................587 14.6 Zusammenfassung ......................................................................................587 15 Wer suchet, der findet............................................................................................589 15.1 Überblick über die Suchkonzepte ..............................................................590 15.2 Zugriff über UniversalID und NoteID.......................................................593 15.3 Suche über Views ........................................................................................594 15.4 Suche über Datenbanken mit db.search ....................................................600 15.5 Volltextindizes .............................................................................................601 15.6 Weitere Suchkonzepte (Domainsearch / browsergestützt) .......................605 15.7 Performanceoptimierung ...........................................................................608 15.8 Zusammenfassung ......................................................................................610
16 J2EE Infrastruktur .................................................................................................613 16.1 Application Server ......................................................................................616 16.2 Integrationsszenarien .................................................................................616 16.2.1 Domino als primärer Server..........................................................616 16.2.2 Application Server als primärer Server .........................................619 16.2.3 Domino und Application Server...................................................622 16.3 Single Sign On.............................................................................................624 16.4 Zusammenfassung ......................................................................................626 17 J2EE @ Domino.....................................................................................................627 17.1 Servlets.........................................................................................................628 17.2 JSP für Domino...........................................................................................628 17.3 Domtags – Installation und Verwendung..................................................630 17.4 Domtags ......................................................................................................634 17.4.1 domino:session ..............................................................................635 17.4.2 domino:db......................................................................................637 17.4.3 domino:view ..................................................................................637 17.4.4 domino:document .........................................................................641 17.4.5 domino:form..................................................................................644 17.4.6 domino:ftsearch .............................................................................647
XVIII
Inhaltsverzeichnis
17.4.7 domino:runagent ...........................................................................648 17.4.8 Weitere Domtags............................................................................648 17.5 Domutils......................................................................................................650 17.6 MVC Pattern für Domino-Anwendungen ................................................652 17.7 Domino-XML .............................................................................................654 17.7.1 NoteCollection ...............................................................................657 17.7.2 XML-Methoden in Domino-Objekten.........................................661 17.7.3 DxlExporter und DxlImporter......................................................663 17.8 Zusammenfassung ......................................................................................667 18 Domino-Projektentwicklung................................................................................669 18.1 Infrastruktur, Domino-Core-Applikationen.............................................670 18.1.1 Notes Client....................................................................................672 18.1.2 Domino Designer ..........................................................................673 18.1.3 Domino Administrator..................................................................673 18.2 Anwendungen, E-Mail, Replikation...........................................................676 18.3 Entwicklungsumgebungen .........................................................................679 18.4 Team ............................................................................................................684 18.5 Zusammenfassung ......................................................................................685 19 Debugging und Qualitätssicherung......................................................................687 19.1 Notwendigkeit und verschiedene Typen von Tests ...................................688 19.2 Agenten debuggen und testen ....................................................................690 19.3 Domino-Java-Anwendungen testen ..........................................................698 19.4 Remote Debugging von Java-Agenten .......................................................700 19.5 JUnit Tests....................................................................................................703 19.6 Logging........................................................................................................707 19.6.1 Notes-Log-Klasse ...........................................................................707 19.6.2 Logging ohne Domino-Session, Die DJLog-Klasse .....................711 19.6.3 Apache log4J und Domino............................................................715 19.7 Multithreading und Lasttests .....................................................................718 19.8 Notes-INI-Variablen ...................................................................................719 19.9 Zusammenfassung ......................................................................................729 20 Anhang...................................................................................................................731 20.1 Bekannte Speicherlimits in Domino-Datenbanken..................................732 20.2 Übersicht über die Domtags Tag Library ..................................................733 20.3 Literatur und Links.....................................................................................745 20.4 Disclaimer ...................................................................................................748 20.5 Index............................................................................................................749 20.6 Abbildungsverzeichnis................................................................................799
Teil 1
Notes und Domino Teil 1 von Java unter Lotus Domino wird Sie mit Domino infizieren. Grundlagen der Programmierung unter Domino und das besondere Konzept der Domino-Datenbanken und deren Datenstrukturen bieten eine besondere Chance im Rapid Development, insbesondere beim Einsatz von Java für Domino-Anwendungen. Das grundlegende Verständnis der besonderen Domino-Programmiertechniken wird zur Basis für die Verwendung der Domino-Java-Objekte, abgerundet durch einen kurzen historischen Exkurs der Entwicklung der Domino-Plattform. Neben der Einführung in die Datenstrukturen gehört ein Überblick über die Domino-Entwicklungswerkzeuge ebenso dazu, wie das Verständnis des Domino-Servers als Plattform, der Domino-Designelemente und der speziellen Techniken der Domino-Anwendungsentwicklung.
1
1 Das Domino-Prinzip
In diesem Kapitel: Die Vision von Lotus Notes und Domino Besonderheiten von Domino als Datenbank Geschichte
4
1 Das Domino-Prinzip
Lotus Domino ist als meistverbreiteter Application- und Collaborationserver für viele mittlere und große Unternehmen der Quasi-Standard für Groupware und Anwendungsplattform. Durch die gleichzeitig explosionsartige Verbreitung von Java als Basis vieler Anwendungen, die Einbindung von Java in Domino und die konsequente Bindung an Standards durch Lotus/IBM findet Java zunehmend Verbreitung als Basis für Domino-Anwendungen und führt gleichzeitig zur weiteren stetig steigenden Verbreitung von Domino. Einer der Erfolgsfaktoren von Lotus Domino ist die Einbindung von einigen wesentlichen Funktionalitäten schon von der ersten Version im Jahr 1989 an. Dies sind Prinzipien, die heute zur Basis der meisten Anwendungsarchitekturen im Bereich Collaboration geworden sind.
• •
•
•
• • •
• •
Hierzu gehören: Konsequente Umsetzung der Client-Server-Architektur und das Konzept des Remotezugriffs mit der Bereitstellung von Mechanismen für die Remoteeinwahl. Verschlüsselung und Signatur von Daten und Authentifizierung durch die Verwendung einer RSA-Public-Key-Infrastruktur, um Dokumente und Daten nicht nur sicher zu transportieren, sondern auch gegen unerlaubte Veränderung zu schützen. Namens- und Adressbücher. Bereitstellung von zentralen Namens-Diensten zur einfachen Administration und Verwaltung von Benutzern und Gruppen und deren Berechtigungen. Replikation von Daten. Die Replikation ist eins der Alleinstellungsmerkmale von Lotus Domino. Sie ermöglicht nicht nur den einfachen Austausch und sicheren Abgleich von Daten zwischen mehreren Servern, sondern auch zwischen Server und Client. So wurde von der ersten Stunde an die Ressourcen sparende Offlinearbeit am Remoteclient möglich. Schutz von Daten auf Datensatz- und Feldebene durch ACL-Zugriffskontrolllisten. Organisation von Daten auf Basis von Schemata. Portierbarkeit der Datenbanken und Anwendungen auf eine Vielzahl von Serverund Client-Plattformen und eine Vielzahl von Betriebssystemen und Prozessoren. Erreicht wird dies durch eine Architektur in Layern, die die Betriebssysteme von der eigentlichen Domino-Anwendung trennen, so dass der Code für die verschiedenen Plattformen parallel entwickelt werden kann. Dies gilt nicht nur für die Entwicklung der Kern-Anwendung Lotus Domino, sondern auch für die Anwendungen, die auf Basis von Lotus Domino entwickelt werden. Die Erstellung von Designelementen, wie Masken und Ansichten, aber auch von Code auf Basis von LotusScript, einem Visual-Basic-Derivat, oder auf Basis von Java erfolgt Plattform-unabhängig und kann auf allen Plattformen, die Lotus Domino unterstützt, eingesetzt werden. Remoteadministration. Customization der Datenbanken.
1.1 Replikation •
•
• • •
5
Rapid Development auf Basis von integrierten Entwickler-Tools. Lotus Domino war schon in der ersten Version eine Plattform, die neben einer Vielzahl von Basisanwendungen, wie E-Mail, Dokumentenmanagement, Diskussionsdatenbanken, eine Plattform für die Entwicklung neuer Anwendungen darstellte, die sich der durch die Plattform zur Verfügung gestellten Tools und Infrastruktur bedienen konnten. Verwendung internationaler Standards für alle Protokolle und Programmiersprachen. Kaum ein Produkt integriert so konsequent wie Lotus Domino internationale Standards, bei strikter Einhaltung der RFC. Konzept des RichText zur Eingabe von gestalteten Texten. Kontextuelle Hilfe. Verlinkung von Inhalten.
Neben der einerseits strikten Einhaltung von Standards und der gleichzeitig umfangreichen Implementierung vieler dieser Standards – so kann ein Anwendungsentwickler bei der Implementierung direkt auf vorgefertigte Server und Tools, von LDAP über POP und SMTP, von Newslettersystemen bis zum HTTP und nicht zuletzt auf eine robuste RSA-Public-Key-Infrastruktur zurückgreifen, ohne hier selbst neu entwickeln zu müssen – basiert Lotus Domino auf Prinzipien, die die große Verbreitung und gleichzeitig die Alleinstellung und den Erfolg von Lotus Domino begründen.
1.1
Replikation
Immer wieder wird die Replikation als Argument für Lotus Domino ins Feld geführt. Zu Recht. Kein anderer Application Server vereinfacht die Replikation von Daten derart radikal. Datenbanken werden über die gesamte Infrastruktur einfach als so genannte Repliken, man könnte auch von Instanzen reden, angelegt. Serververbindungen machen die Server miteinander bekannt und regeln den Zeitplan, nach dem der Datenabgleich erfolgen soll. Zugriffskontrolllisten sorgen für die Sicherheit. Der Vorgang der Replikation als solcher ist völlig transparent für die Anwender. Es werden alle Repliken im Serververbund – sofern die Replikation aktiviert ist – miteinander abgeglichen. Änderungen auf allen Instanzen werden auf alle anderen beteiligten Instanzen übertragen. Der Vorgang lässt sich so fein und granulär kontrollieren, dass selbst Änderungen einzelner Felder zwischen Datensätzen gemischt werden können. Konflikte, die durch Änderungen gleicher Datensätze auf verschiedenen Servern entstehen können, werden erkannt und in Konflikt-Datensätzen gespeichert. Bei der Java-Programmierung der Backendverarbeitung ist es wichtig das Konzept der Replikation zu verstehen und zu berücksichtigen. Das Verständnis verteilter Umgebungen ist daher Bestandteil dieses Buches (s. Kap. 2.3).
6
1 Das Domino-Prinzip
1.2
Schemabasiertes Objekt- und Speichermodell
Das Herz jeder Domino-Anwendung ist die Domino-Datenbank oder auch die „Note“-Datenbank – vereinfacht gesprochen ein Container für „Datensätze“. Das Konzept von Lotus Notes – an dieser Stelle wird zur Verdeutlichung der ursprüngliche Name des Lotus-Domino-Servers verwendet, der seinen Namen erst im Jahr 1996 erhielt – sieht vor, dass alle Bestandteile einer Anwendung in der Notes-Datenbank, dem NSF (Notes Storage Facility), File gespeichert werden. Gemeint ist, dass wirklich alles, vom Datensatz bis zur Zugriffskontrollliste im NSF-File gespeichert wird. Umgekehrt ist jeder Bestandteil einer Notes-Anwendung eine „Note“. Jedes Designelement – so heißen neben WYSIWYG Gestaltungselementen wie Masken, die Programm-Bibliotheken von Lotus Notes – wird in einer „Note“ gespeichert. Auch z.B. die ACL oder Datenbank-Eigenschaften werden in speziellen „Notes“ gespeichert. Dieses Konzept, nur eine einzige Datei für eine gesamte Anwendung als zentrales Repository anzulegen, ist unerlässlich für eine leicht zu administrierende Replikation und natürlich ein großer Vorteil für die Administration insgesamt. Selbstverständlich werden insbesondere die eigentlichen Daten in Notes gespeichert. Diese Notes werden auch als Dokumente bezeichnet. Jedes Dokument und jede „Note“ besteht nun wieder aus mehreren so genannten Items, die in einem ersten Ansatz als Felder bezeichnet werden können, vergleichbar mit den Spalten einer SQL-Tabelle, wobei später noch näher auf die Eigenschaft von Items eingegangen wird. Items können einerseits mehrere Werte und andererseits in bester objektorientierter Manier wiederum Items aufnehmen. Das Speicherkonzept von Lotus Domino unterscheidet sich jedoch wesentlich von SQL-Datenbanken. In diesen Datenbanken wird exakt festgelegt, welche Regeln den Daten zugrunde liegen und in welchen Verhältnissen diese zueinander stehen. Dies erfordert eine strikte Administration und umfangreiche Planung, auch in der späteren Handhabung der Daten. Das Konzept von Notes geht hier einen anderen Weg und speichert die Daten in losen Schemata, d.h. ein einzelnes Dokument kann beliebige Items oder auch Felder enthalten. Lediglich über Masken, die eine unverbindliche Vorgabe bei der Erstellung von Dokumenten geben, werden Dokumente kategorisiert und mit einem – zunächst – stringenten Schema versehen, das jedoch nicht bindend ist oder erzwungen wird. Am ehesten lässt sich die Datenstruktur von Domino mit der von XML vergleichen. Auch dort werden die Daten in Namens-Wert-Paaren gespeichert und sind nicht zwingend, wenn auch in der Regel üblich, einem Schema unterworfen. Die Tatsache, dass Lotus Domino nicht relational aufgebaut ist und insbesondere, dass die Schemata nicht erzwungen werden, wird oft als Kritik an Lotus Domino genannt und ist ungewohnt für Programmierer, die aus der SQL-Entwicklung kommen. Die Einhaltung eines Schemas und der daraus entstehende Verwaltungs- und Programmieraufwand ist jedoch nicht unerheblich und erfordert ein hohes Niveau an Kenntnissen und Erfahrungen. Dies hat die Begründer der Lotus-Notes-Datenstruktur dazu bewogen sich zugunsten eines leicht zu handhabenden und zu erweiternden Datenkonzeptes zu entscheiden.
1.3 Erweiterbarkeit
7
Die offene Schemastruktur und Objektorientierung der Lotus Notes NSF-Datei ist eine Stärke von Lotus Domino und ermöglicht die Erstellung von robusten Datenbankanwendungen im Rapid Development. Hieraus folgt nicht nur, dass in Lotus Domino Datenbanken Die offene Schemastruktur und Objektorientierung bevorzugt nicht strukturierte Daten, wie z.B. in Diskussionen geder Lotus Notes NSF-Datei ist eine Stärke von Losammelte Beiträge, gespeichert tus Domino und ermöglicht die Erstellung von rowerden können, sondern auch, busten Datenbankanwendungen im Rapid Devedass entsprechende Anwendunlopment. gen, wie Workflow- oder Kollaboration-Anwendungen bevorzugt realisiert werden. SQL-Datenbanken dagegen speichern bevorzugt strukturierte Daten, wie z.B. Produkt-Kataloge, die stark auf relationale Verknüpfungen zwischen Einzeldaten angewiesen sind, um Redundanzen zu vermeiden. Mit der Einführung von Version 7 von Domino hat dieses Bild eine grundlegende Erweiterung erfahren. Mit Lotus Domino R7 wird die Möglichkeit eingeführt, die Domino-Daten in einer DB2-Datenbank zu speichern, also in einer relationalen SQLDatenbank, wobei diese Art der Speicherung für den Notes-Programmierer oder Anwender zunächst transparent bleibt. Zusätzlich wird es die Möglichkeit geben, Domino Views anzulegen, die SQL-Daten aus DB2-Datenbanken direkt über SQL Queries laden können. Durch die Speicherung von Domino-Daten in DB2 einerseits und die Selektion von SQL-Daten andrerseits werden neue Möglichkeiten eröffnet. So wird es hierdurch zum Beispiel möglich sein, Daten aus mehreren Notes Views oder auch Notes-Datenbanken in einem neuen View anzuzeigen, der diese Daten entsprechend mischt. Dies war bisher in Domino nicht ohne weiteres möglich. Zum einen folgt hiermit IBM den Anforderungen des Marktes, SQL-Daten mit Hilfe der leicht zu erlernenden RAD Tools des Domino Designers anzeigen zu können und andrerseits die durch Domino-Anwendungen erzeugten Daten in einer relationalen Struktur speichern zu können. Gleichzeitig eröffnet IBM hiermit den Entwicklern die Möglichkeit das beste aus beiden Konzepten zu vereinen. Diese Weiterentwicklung bietet einen weiteren Schritt der Integrationsmöglichkeiten. Java-Anwendungen auf Basis von J2EE können noch nahtloser in Domino-Anwendungen integriert werden.
1.3
Erweiterbarkeit
Eines der Schlüsselkonzepte von Lotus Domino war und ist die Erweiterbarkeit. Zwar bietet Domino seit Beginn an Basismodule aus dem Bereich der Kollaboration, aber diese waren immer nur die Basis und ein Angebot an die Programmierer, sie zu verwenden und zu erweitern.
8
1 Das Domino-Prinzip
Bei der Entwicklung von Domino-Anwendungen kann zwischen zwei Elementen unterschieden werden, einerseits den so genannten Designelementen, mit denen die GUIs entwickelt werden und zum anderen programmatischen Code, der im Wesentlichen zur Programmierung von Backend-Prozessen verwendet wird. Die wichtigsten Designelemente, mit denen Notes-Datenbanken programmiert werden sind Masken und Ansichten. Masken sind Formular-Seiten, die zur BenutzerInteraktion, insbesondere zur Eingabe und Anzeige von Daten entwickelt werden. Ansichten dienen der Selektion von Daten und der Darstellung als Listen oder Tabellen. Sowohl in Masken als auch in Ansichten können so genannte @-Formeln eingesetzt werden, die im Wesentlichen der Validierung von Eingaben oder der berechneten Anzeige von Inhalten dienen. Durch die DesigneleJava war und ist der Booster für die Erweiterbarkeit mente können Anwendungen efvon Domino-Anwendungen, insbesondere durch fizient und schnell erstellt werden weiten Markt an Open-Source-Bibliotheken. den. Um im Backend DominoDaten verarbeiten zu können, führte Lotus im Januar 1996 für Version 4.0 die Programmiersprache LotusScript und im Dezember 1996 für Version 4.5 die Möglichkeit, per Java auf Domino-Objekte zuzugreifen, ein. Im Gegensatz zu LotusScript wurden für Java nur die Backend-Objekte zugänglich gemacht, so dass zunächst LotusScript wesentlich stärker verbreitet war. Inzwischen ist Java die Core-Sprache bei der Neuentwicklung von Domino-Anwendungen. Java bietet Möglichkeiten der Programmierung, die bisher mit LotusScript nicht möglich waren.
• • • • •
Hierzu gehören: Multithreading-Anwendungen Verarbeitung von URL-Verbindungen, wie z.B. HTTP, FTP, SSH etc. Einbinden von vorgefertigten Open-Source-Bibliotheken Verarbeitung von Streams (diese Möglichkeit gibt es seit Version 6 auch für LotusScript) SQL-Datenbankbindung durch native JDBC-Treiber
Durch den Einsatz von Java ist Domino zu einem vollwertigen Application Server erwachsen, der viele der J2EE-Standards unterstützt. Mit der Version 6 bietet Lotus die Möglichkeit, zusätzlich zu Domino einen WebSphere Application Server einzusetzen, der über Connector Plugins mit Domino verbunden wird. Hierdurch eröffnet sich die Möglichkeit vollwertige J2EE-Anwendungen zu schreiben, die gleichzeitig Zugriff auf die Domino-Daten haben. Hierdurch ergibt sich ein typisches Szenario, wie z.B. für ein Domino-basiertes Web Content Management. Die Redaktion bedient sich des Notes Client, um Inhalte zu pflegen. Gleichzeitig wird durch WebSphere ein Darstellungslayer realisiert, um die Daten in einer leistungsfähigen und robusten Web-Anwendung darzustellen. Für die Verwendung von Java bei der Programmierung und Erweiterung von Domino-Anwendungen ergeben sich drei wesentliche Einsatzgebiete:
1.4 Zugriffskontrolle und Verschlüsselung • • •
9
Backendverarbeitung von Domino-Daten Web-Anwendungen mit Zugriff auf Domino-Daten Enterprise-Datenbankanbindungen für Domino-Applikationen Auf alle drei Anwendungsgebiete wird in diesem Buch eingegangen werden.
1.4
Zugriffskontrolle und Verschlüsselung
Domino ist die erste wichtige kommerzielle Software, die Verschlüsselung, Signatur und Authentifizierung auf Basis von öffentlichen RSA-Schlüsseln anbot [Lotus, History]. Gleichzeitig ist dies eines der Hauptargumente, das am häufigsten für Lotus Domino ins Feld geführt wird. Zu Recht: Lotus Domino, aber auch seine Komponenten-Server wie LDAP oder HTTP werden zu den sichersten am Markt gezählt. Durch das einfache Management der Benutzer in den öffentlichen Notes-Adressbüchern, die entweder nativ oder per LDAP angesprochen werden können, bietet sich dem Java-Programmierer ein robustes und ausgereiftes Framework für die Verwaltung von Rechten. Das Konzept dieses Frameworks ist ausgereift und sehr granulär steuerbar. In einer Abstufung der Rechte, vom Server bis hin zu einzelnen Feldern in Dokumenten, kann der Programmierer genau festlegen, welche Daten eingesehen werden können. Durch die RSA-Infrastruktur stehen reichhaltige Werkzeuge zur Verschlüsselung von Daten und Datenströmen und Identifizierung von Benutzern und deren Rechten zur Verfügung. Darüber hinaus können Schlüsselpaare definiert werden, mit denen Benutzer, die Zugriff auf diese Schlüssel haben, ihre Daten oder Teile davon schützen können. Dies wird im Wesentlichen durch das Speicherkonzept der losen Schemata ermöglicht, da hierdurch objektorientiert zu schützende Items definiert werden können. Der Zugriff auf Daten wird in mehreren Schritten abgesichert. Zunächst wird der Zugriff auf den Server überprüft. Der Zugriff erfolgt immer gegen das so genannte Notes-Adressbuch. Allerdings kann das Adressbuch über die so genannte Directory Assistance entweder durch sekundäre Adressbücher oder durch LDAP-Verzeichnisse erweitert werden. So ist es zum Beispiel einfach möglich, ein Active Directory in ein Notes-Adressbuch einzubinden. Grundsätzlich wird beim Zugriff nach zwei Arten unterschieden. Zum einen der Zugriff über den Notes Client und zum anderen der Zugriff als so genannter Web-Access. Der Zugriff über den Notes Client erfolgt immer mittels einer ID-Datei, in der die privaten RSA-Schlüssel gespeichert sind. Beim Web-Zugriff können verschiedene Authentifizierungsmethoden gewählt werden. Es stehen Login-Verfahren mit X.509Client-Zertifikaten, mit Name und Passwort oder das anonyme Login zur Verfügung. Nach der Überprüfung auf den Serverzugriff erfolgt eine Überprüfung der ACL, der so genannten Zugriffskontrollliste (Access Control List). In dieser werden die einzelnen Rechte nach Benutzern, Servern und Gruppen eingetragen, die unterschiedliche Rechte als Leser, Autor, Datenbankdesigner oder Manager (s. auch Kap. 9.5) erhalten können.
10
1 Das Domino-Prinzip
Internet
Notes-IDDatei
Internet User
Notes Client
Stufe 1 - Server
Stufe 1 - Server Prüfung des Serverzugriffs Prüfung des Internet-Passworts
Prüfung des Serverzugriffs Prüfung der Notes ID
Domino Stufe 2 - Datenbank Prüfung der ACL Prüfung der Rollen
Stufe 3 - Dokument Prüfung von Leserund Autorenfeldern
Stufe 4 - Feld Feldverschlüsselungen
Abb. 1-1 Domino-Sicherheit
Zusätzlich können in der ACL Rollen definiert werden, so dass einzelne Bereiche oder Daten von Anwendungen abhängig von dem Erhalt dieser Rollen gesteuert werden können. Mit so genannten Leser- und Autorenfeldern können dann auf Dokumentenebene für jedes einzelne Dokument explizit Rechte vergeben werden. Mit Leserfeldern wird festgelegt, wer ein Dokument lesen darf. Mit Autorenfeldern wird festgelegt, wer ein Dokument verändern darf. Zusätzlich kann dann noch auf Feldebene eine Verschlüsselung erfolgen, so dass derart verschlüsselte Felder nur von Inhabern dieses Schlüssels gelesen werden können. Da diese Schlüssel in der ID-Datei gespeichert werden, kann diese Art des Schutzes nur beim Zugriff über den Notes Client, nicht jedoch bei der Benutzung über Browser erfolgen (Näheres hierzu auch in Kapitel 7.5.1ff.)
1.5 Geschichte
11
Im Einzelnen ist die Sicherheitsstruktur wie folgt angelegt: 1 – Serversicherheit – Überprüfung der Notes ID, bzw. des Internet-Benutzernamens und Passwortes. 2 – Datenbanksicherheit – Überprüfung der ACL. 3 – Dokumentensicherheit – Leser- und Autorenfelder. 4 – Sicherheit auf Feldebene – verschlüsselte Items (s. Abb. 1-1). Insbesondere das Konzept der Leser- und Autorenfelder und der Feldverschlüsselung wird erst durch das für Lotus Domino einzigartige Konzept der offenen Schemastruktur ermöglicht. Jeder Datensatz wird in einem Dokument zusammengefasst. Dieses Dokument kann Felder (Items) enthalten, die von dem einen Benutzer gelesen werden können, von einem anderen nicht. Ob das Dokument diese Felder tatsächlich enthält ist nicht relevant, da es durch das Schema nicht erzwungen wird. Dadurch hat ein Benutzer, der einen Schlüssel nicht zur Verfügung hat, dennoch die Möglichkeit ein entsprechend reduziertes Dokument mit den ihm zur Verfügung stehenden Feldern zu erzeugen. Die Java-Programmierung greift, wie bereits erwähnt, nur auf die Backend-Objekte von Domino zu. Daher ist an dieser Stelle erwähnenswert, dass auch die JavaZugriffe, egal ob diese über DIIOP1 (s. Kap. 5.3), über einen Web-Browser, oder über einen Lotus-Domino-Agenten erfolgen, Zugriff auf Schlüssel haben können. Diese müssen in diesen Fällen in der so genannten Server-ID gespeichert sein. Die ServerID ist der RSA-Schlüssel mit den Private Keys, unter denen die Server-Prozesse laufen.
1.5
Geschichte
Die ursprüngliche Idee von Lotus Notes reicht zurück ins Jahr 1973, als am Computerbased Education Research Laboratory (CERL) an der Universität Illinois PLATO Notes auf der Basis von Host-Systemen entwickelt wurde. Diese Software diente dazu, Bug-Reports an Projekt-Administratoren zu senden, die diese Bug-Reports wiederum mit Antworten versehen konnten. Dieses frühe Diskussionsinstrument sah bereits vor, Dokumente mit den User IDs der Benutzer zu versehen, um nachträgliches Löschen oder Verändern von Inhalten auszuschließen. Unter anderem ist hier auch schon ein für Notes sehr typisches Szenario sichtbar, nämlich dass Dokumente in Dokument- und AntwortdokumentHierarchien dargestellt werden. Drei Jahre später wurde PLATO zu PLATO Group Notes erweitert und konnte in dieser Version bereits zwischen privaten und öffentlichen Inhalten unterscheiden, Zugriffskontrolllisten verwalten und Links zwischen Dokumenten und anderen PLATOSystemen herstellen. Auf Basis von PLATO Group Notes entwickelte ein Team um 1
DIIOP = Domino IIOP = Internet Inter-ORB Protocol; ORB = Object Request Broker, als Teil der CORBA-Architektur. CORBA = Common Object Request Broker. CORBA wird von der Object Management Group OMG spezifiziert. [CORBA], [OMG]
12
1 Das Domino-Prinzip
Ray Ozzie, Tim Halvorsen und Len Kawell gegen Ende des Jahres 1984 mit der Unterstützung von Lotus die Basis von Lotus Notes, das aufgrund der Entwicklung des PCs nun auf DOS oder OS/2 laufen sollte, unter dem Dach von Iris Associates Inc. Für die Ur-Version von Notes waren die Funktionen E-Mail, Diskussionen, Telefonbücher und Dokumenten-Datenbanken geplant. Aufgrund der damals langsamen Netzwerke und der noch nicht weit entwickelten PC-Systeme wurde es zu einem der großen Ziele von Notes, diese Hürden zu überwinden. Auf der Basis von zentralen PC-basierten Gruppen-Servern, die bereits mit anderen solchen Servern Daten replizieren konnten, entstand so die erste wichtige Groupware-Software. Ein weiterer wesentlicher Punkt war zu dieser Zeit die Entscheidung, kein OutOf-The-Box-Produkt zu erstellen, sondern eine Plattform zu bieten, die es den Benutzern ermöglicht die Anwendungen an ihre Bedürfnisse anzupassen. Diese Entscheidung spiegelt sich auch in der heutigen Version wider. Vor-Versionen von Lotus Notes wurden bereits 1986 eingesetzt, wobei die Rechte daran 1987 von Lotus gekauft wurden. Die Version 1.0 von Lotus Notes wurde dann 1989 ausgeliefert und hatte zu dieser Zeit wesentliche Funktionen des heutigen Lotus Notes implementiert. Hierzu gehören Encryption und Signaturen, Dial-Up und Import/Export-Funktionalität, Zugriffskontrolllisten, Online Help, die @-Formelsprache und Funktionen für die Remoteadministration. Die Versionen 1.1 und 2.0 und 3.0 brachten zum einen neue Fähigkeiten für mittlere und große Unternehmen, so dass auch 10.000 und mehr Benutzer unterstützt wurden, zum anderen weitere Funktionen, wie zum Beispiel die C API, die Unterstützung von RichText und Funktionen für die Volltextsuche, wobei insbesondere mit der Version 3.0 die Cross-Plattform-Fähigkeit weiter ausgebaut wurde. 1994 kaufte Lotus Iris Associates und 1995 kaufte IBM Lotus, insbesondere wegen Lotus Notes. Die Versionen 4.0 und 4.5 spiegeln die Entwicklung dieser Zeit wider, so dass etliche der Internet-Protokolle Einzug in die Notes-Technologie fanden. Neben Socks und HTTP-Proxy wurden SMTP/MIME, POP3 und HTTP unterstützt, wobei Lotus Notes mit einem eigenen HTTP-Server aufwartete. Der Notes-Server wurde mit Version 4.5 in Domino umbenannt, wobei der Client weiter Lotus Notes Client hieß. Version 4.0 erhielt mit LotusScript, basierend auf Visual Basic, eine erste Programmiersprache, mit der komplette Anwendungsentwicklung betrieben werden konnte. Bereits in Version 4.5 wurde Java unterstützt, so dass Agenten komplett in Java programmiert werden konnten. Lotus Domino konnte nun die meisten der Notes-Funktionalitäten und Anwendungen auch über den Web-Browser darstellen. Hierzu gehört auch die Fähigkeit des Notes Client, Internetdienste abzurufen und umgekehrt Notes-Dienste wie E-Mail über den Browser abzurufen. Mit Version 5.0 erhielt Lotus Domino 1999 ein komplettes Face-Lifting. Die etwas aus der Mode gekommene Oberfläche wurde vollständig überarbeitet. Technisch wurde die Integration von Internet-Funktionalitäten vorangetrieben. Standard-Protokolle wurden integriert, wie LDAP, JavaScript und CORBA, bereits vorhandene Technologien, wie MIME und SMTP wurden überarbeitet und insbeson-
1.5 Geschichte
13
dere streng an den Industriestandards ausgerichtet. Gleichzeitig wurden die Entwickler- und Administrationswerkzeuge (Notes Designer, Notes Administrator) verbessert. Alle wichtigen Internet-Protokolle und -Standards werden mit der Version 5.0 unterstützt. Mit einem eigenen Servlet Manager geht Domino seine ersten Schritte in Richtung Web Application Server. Zusätzlich bietet diese Version Plugins und Schnittstellen zum Apache Webserver, Tomcat Application Server und dem IBM-eigenen WebSphere Application Server, die so die Brücke zur J2EE-Welt schlagen. Java hat in alle Teile des Domino-Servers Einzug erhalten, wird aber nur in der Version 1.1.8 unterstützt, so dass hier einer der wichtigen Kritikpunkte an dieser Version ansetzt. Der Lotus Notes Client in Version 6.0 und 6.5 wurde erneut optisch überarbeitet und ähnelt nun immer stärker Portal-orientierten Collaboration-Anwendungen. Viele der langjährigen Basis-Funktionalitäten wie Kalender, Aufgaben und EMail erhalten in diesen Versionen eine Überarbeitung, um sie an die modernen Anforderungen einer Groupware-Software anzupassen. So wurden Spam-Filter und Messaging-Funktionen eingebaut bzw. verbessert. Gleichzeitig wurde der Web-Zugriff auf die Notes-Funktionalitäten, die ihren Niederschlag im iNotes Web Access Client fanden, erheblich verbessert, insbesondere die Offlineservices sind eine Besonderheit in diesem Bereich und ermöglichen es Anwendern über den Browser zu arbeiten, ohne dauerhaft online sein zu müssen. Der Server wurde in vielen Bereichen überarbeitet und verbessert. In vielen Bereichen konnten erhebliche Geschwindigkeitssteigerungen erreicht werden. Native Internet-Protokolle sind nun zur Basis vieler Prozesse im Domino-Server geworden, hierzu gehört z.B. nun die nahtlose Integration einer R.509-Zertifikat- und S/MIMEInfrastruktur, die Möglichkeit, LDAP als Protokoll für Namens-Anfragen zu verwenden und die Implementierung vieler XML-Funktionen in die Domino-Core-Klassen von Java und LotusScript. Java wartet nun mit J2SE 1.3.1 auf. Um die fehlenden J2EE-Funktionalitäten auszugleichen hat sich IBM für ein neues Lizenzierungsmodell entschieden, das es erlaubt mit einer Domino-Lizenz einen WebSphere Application Server neben einem Domino-Server zu betreiben, sofern beide auf der gleichen Maschine betrieben werden und WebSphere nur auf Domino-Objekte zugreift. Diese beiden Server können nahtlos miteinander integriert werden. Single Sign On gehört ebenso zu den Standards wie ausführliche Custom-Tag-Bibliotheken (Domtags Tag Library) für den Einsatz von JSP. Die Entwickler-Umgebung von WebSphere, der WebSphere Application Developer (WSAD), wird mit dem Lotus Domino Toolkit for WebSphere Studio ausgeliefert, einem Plugin, das die Erstellung von JSP auf Basis der Domtags Tag Library erleichtert. Dieses Toolkit kann auch mit Eclipse, einem der inzwischen beliebtesten Java IDEs, das als Open-Source-Projekt von IBM gefördert wird, betrieben werden. Durch diese Strategie wird eine Entwicklung deutlich, die einen erheblichen Einfluss auf den Einsatz von Lotus Domino hat: Es bilden sich verschiedene Layer heraus, die von den verschiedenen Servern bedient werden. Lotus Domino konzentriert sich wieder stärker auf seine ureigenen Fähigkeiten, der Collaboration und Dokumenten-Management, das sich zum Beispiel im Redaktion-Layer eines Content Ma-
14
1 Das Domino-Prinzip
nagements ausprägen kann. Backend-Batch-Verarbeitung, aber auch typische WebApplikationen, also z.B. der Display Layer eines typischen Web-Content-Management-Systems und MVC-Pattern-basierte Anwendungen (Modell-View-Controller, s. auch Kap. 17.6), die eine robuste J2EE-Application-Server-Umgebung benötigen, finden im WebSphere Application Server eine Plattform. Die Fähigkeiten von Domino, Java und Servlets zu unterstützen, erleichtern diese Entwicklung erheblich, können doch Anwendungen zunächst ausschließlich auf Domino betrieben und später für einen Application Server erweitert werden. Version 7, die Ende 2005 ausgeliefert wurde, stellt einen weiteren Meilenstein für Domino dar. Diese Version bricht alte Datenstrukturen auf und ermöglicht die Speicherung der Domino-Daten im RDBMS/SQL-Umfeld. Ab Version 7 wird es möglich sein, Domino-Daten direkt in DB2-Datenbanken abzulegen und diese aber im gewohnten Domino-Umfeld, also in Masken und Views darzustellen. Java-Entwicklern steht ab Version 7 das JDK 1.4.1 zur Verfügung. Linux findet seit Domino Version 5 Einzug in die Multiplattform-Fähigkeit von Domino und ist mit Version 7 zum Standard herangereift. Die Performance unter Linux soll in dieser Version erheblich verbessert werden. Ein weiterer Schwerpunkt sind viele neue Monitoring-Funktionen für das Server Management und eine neue Multi-Domain-Fähigkeit, so dass mehrere Notes Domains auf einem Server betrieben werden können. Die Entwicklung der Internet-Standards findet erneut Einzug in die DominoEntwickler-Tools. Unter anderem können nun WebServices direkt im Domino Designer (dem Domino IDE) entwickelt werden.
1.6
Lotus Domino und Java
Java fand bereits Ende 1996 mit Version 4.5 Einzug in Domino. Es gehört zu den Grundsatzentscheidungen von IBM, international anerkannte und verbreitete Standards für Protokolle, Frameworks, Konzepte, Plattformen und Sprachen zu unterstützen. Hierzu gehört z.B. die Entscheidung, Linux als Plattform in die Entwicklung aufzunehmen und konsequent zu unterstützen. Java ist ein zentraler Punkt in dieser Strategie. Java ist die Core-Programmiersprache im Application-Server-Bereich. Durch diese frühzeitige Unterstützung ist das Java-Framework zu einem robusten Entwickler-Werkzeug für Domino erwachsen, obwohl die Gemeinde der Domino-Entwickler erst in letzter Zeit nach und nach von LotusScript auf Java umschwenkt, oder zumindest ihr Wissen in dieser Richtung erweitert. Wie bereits zuvor aufgezeigt hat der rasant wachsende Application-Server-Markt einen großen Einfluss auf die Weiterentwicklung von Lotus Domino, so dass Java eine zentrale Bedeutung zukommt. Im Objektmodell von Lotus Domino wird unterschieden nach den so genannten UI-Klassen und den Backend-Klassen. Die UI-Klassen bilden alle Objekte ab, die in direkter Interaktion mit dem Benutzer stehen. Öffnet z.B. ein Benutzer ein Dokument, so werden die Daten dieses Dokuments mit einer so genannten Maske dargestellt.
1.6 Lotus Domino und Java
15
Die UI-Klassen bilden die meisten Funktionen und Objekte ab, die im Lotus Notes Client zur Verfügung stehen. Das derart geöffnete Dokument wird in den UI-Klassen abgebildet. Mit den Methoden dieser Klassen können dann z.B. Cursor bewegt oder Fenster geschlossen werden. Gleichzeitig hat ein Dokument eine Repräsentation in einer Backendklasse, in Java ist dies die Klasse Document. Eine solche Instanz eines Dokuments kann gleichzeitig mit der UI-Instanz existieren. Die Instanz des Backend-Objekts hat jedoch keinen Zugriff auf Objekte oder Funktionen, mit denen der Benutzer interagiert, sondern lediglich auf den Datensatz als solchen. Bei der Klasse Document sind dies alle Items (Felder) eines Dokuments und Methoden zur Manipulation, die unabhängig vom Benutzer stattfinden können, wie z.B. das Verschlüsseln (document. encrypt()) oder Speichern (document.save ()) von Daten, oder das Versenden von Daten als E-Mail (document.send()). Java hat, im Gegensatz zu LotusScript, ausschließlich Zugriff auf die Backend Repräsentation eines Notes-Objektes.
• • •
Dies prädestiniert Java für folgende Anwendungsbereiche: Backend- und Batch-Verarbeitung von Domino-Daten Web-Anwendungen mit Zugriff auf Domino-Daten, auch über Remotezugriffe und Application Server wie WebSphere Enterprise-Datenbankanbindungen für Domino-Applikationen
Die fehlende Möglichkeit, Frontend-Objekte durch Java zu steuern, bedeutet nicht, dass Java nicht für Frontend-Anwendungen (Lotus Notes Client-Anwendungen) eingesetzt oder aus diesen angesteuert werden könnte. Hierzu mehr in Kapitel 4. Ohnehin haben sich vorwiegend die Backend-Klassen in den letzten Versionen weiterentwickelt. In ihnen liegt die eigentliche Macht des Domino API. Oft wird LotusScript mit Java verglichen und versucht herauszukristallisieren, wo die Vor- und Nachteile der beiden Programmiersprachen im Zusammenhang mit Lotus Domino liegen. Java ist die modernere der beiden Programmiersprachen und blickt auf ein weites Umfeld an Open-Source-Projekten und -Bibliotheken, die es dem Programmierer ermöglichen, auf vorgefertigte (Teil-)Ergebnisse zurückzugreifen und so die Entwicklungszyklen erheblich zu beschleunigen. Auch wenn LotusScript im Ansatz auch Objektorientierung und Klassen-Strukturen bietet, fällt bei der Bevorzugung einer objektorientierten Sprache eindeutig die Wahl auf Java. Interessant ist, versucht man beide Sprachen zu vergleichen, dass beide auf die gleichen C++ Objekte im Hintergrund zugreifen, denn die Domino-Java-Objekte sind im Wesentlichen nicht als originärer Java-Code implementiert, sondern basieren auf dem Java Native Interface JNI. Folglich ist sichergestellt, dass äquivalente Methoden absolut äquivalent arbeiten. Durch die besseren Möglichkeiten in Java, das Speichermanagement zu beeinflussen, lässt sich, wie später gezeigt werden wird, mit Java eine nicht unerhebliche Performance-Steigerung erreichen.
16
1 Das Domino-Prinzip
Durch die bereits in der Core-Sprache verankerten IOStream-Klassen kann Java erheblich besser mit Daten- oder Character-Strömen umgehen, wobei erwähnt werden soll, dass IBM mit der Version 6.0 auch Stream-Klassen für LotusScript ausliefert. Sollen Multithreading-Anwendungen programmiert oder URL-Verbindungen realisiert werden, muss auf Java zurückgegriffen werden, da diese Möglichkeiten nicht für LotusScript zur Verfügung stehen. Neben den nativen XML-Methoden, die das Domino API für Java (und LotusScript) zur Verfügung stellt, bietet es sich an, bei Projekten, die XML verarbeiten, auch die XML-Klassen des Apache Projektes (Xerxes, Xalan, FOP und andere) anzusehen [Apache, XML]. Durch die recht alte JVM 1.1.8, die mit Domino R5 ausgeliefert wird, empfiehlt es sich Java-Anwendungen für Domino R6 zu entwickeln, wo dies möglich ist, da diese Version mit JDK 1.3.1 ausgeliefert wird. Sollen JSP- oder Servlet-2.0-Funktionalitäten zum Einsatz kommen, können die Plugins für Domino oder WebSphere (oder auch Tomcat und sogar JBoss – hierzu mehr in Kapitel 16.1ff.) verwendet werden. Mit diesen modernen J2EE-Containern lassen sich Domino-Daten optimal verarbeiten, z.B. auch über eine Remoteverarbeitung über IIOP. Müssen Domino-Daten regelmäßig periodisch oder eventgesteuert verarbeitet werden, bietet es sich an, diese als Domino-Agenten (so heißen die Batch-Prozesse unter Domino) umzusetzen. Durch diese vielfältigen Schnittstellen ist Java zur der wichtigsten Programmiersprache herangewachsen, mit der Domino-Prozesse und -Daten verarbeitet werden.
1.7
Lotus Domino als Datenbank
Der Lotus Domino Server ist in seinem Selbstverständnis weit mehr als eine Datenbank. Lotus Domino ist vielmehr eine Entwickler-Plattform für Groupware-Anwendungen in verteilten Umgebungen. Daher stellt Domino zusätzlich zu den Datenbanken viele Dienste zur Verfügung, die für die Programmierung von Unternehmensanwendungen herangezogen werden können. Einer der wichtigsten Dienste in der Domino-Infrastruktur ist das Namensund Adressbuch. Hier werden alle Benutzer- und Berechtigungsinformationen, aber auch alle Konfigurationen für den Server gespeichert. Ganz im Sinne der Erweiterbarkeit steht seit Version 5 dieses Adressbuch als LDAP-Dienst zur Verfügung. Weiterhin stehen folgende Basisdienste zur Verfügung: • • • • • • • •
POP3, IMAP, SMTP, jeweils auch über einen SSL-verschlüsselten Kanal Domino Router LDAP, auch über SSL News IIOP, auch über SSL HTTP und HTTPS Indexer (erstellt Volltext-Indizes über einzelne Datenbanken) Domänen übergreifender Volltext-Index über Datenbanken und File-Systeme
1.7 Lotus Domino als Datenbank • •
17
Agent Manager (zur periodischen und eventgesteuerten Abwicklung von Prozessen – auch Java-Anwendungen können hierdurch gesteuert werden). diverse administrative Domino-spezifische Dienste, wie z.B. Replikation, Schedule Manager, Statistik und Monitoring
Alle diese Dienste basieren auf den RFC-Standards und können über die üblichen Java-Klassen angesprochen werden. Domino basiert auf einem Namens- und Adressbuch, dem Domino Directory oder auch „names.nsf“, das auch als LDAP-Verzeichnis betrieben werden kann und so nicht nur in der Java-Programmierung vielfältig genutzt werden kann, sondern auch Drittanwendungen zur VerfüBeispiel für eine Select-Formel einer Ansicht: gung steht. Die Domino-Datenbanken SELECT selbst basieren (vgl. aber auch die Verwendung von Domino-Da((@LowerCase(Form) = "logfile") | tenbanken in DB2, s. Kap. 13.4), (@LowerCase(Form) = "fo_logfile")) wie bereits erwähnt, auf einer vereinfacht gesprochen losen Sammlung von Datensätzen oder auch „Notes“. Organisiert werden die Daten durch den Aufbau von Indizes über so genannte Ansichten (Views). Ansichten werden in der Regel im IDE, dem Domino Designer, angelegt. Die in den Ansichten selektierten Dokumente werden über Select-Formeln ausgewählt, die den SELECT-Formeln des SQL recht ähnlich sind. Ansichten können auch über das Backend aus Java heraus generiert werden, sollten aber jeweils nur einmal angelegt werden. Wichtig für die spätere SeÜber sortierte Spalten legt Domino automatisch Inlektion von Dokumenten ist, dass dizes an. mindestens die erste Spalte einer Ansicht sortiert ist. Diese Eigenschaft kann entweder über den Domino Designer oder programmatisch erstellt werden. Über sortierte Spalten legt Domino automatisch Indizes an. Daten werden entweder über das UI Frontend von Lotus Domino in Masken (Form) erfasst und in Dokumenten gespeichert. Dokumente besitzen neben einigen Standard-Feldern, für Berechtigungen, Revisionen und Zeitstempel, eine beliebige (nur durch einige Speicherlimits begrenzte) Anzahl von Items, in denen die Daten gespeichert werden. Der große Vorteil der Dokumente ist, dass weder die Größe einzelner Felder, noch die Anzahl der Felder in einem Dokument durch ein Schema erzwungen wird. Daher sind Dokumente nur in einer starken Vereinfachung mit einer Zeile einer SQLTabelle zu vergleichen. Bei der Backend-Programmierung in Java werden die Dokumente als Instanz der Klasse Document abgebildet. Felder werden über spezielle getter- und setter-Methoden hinzugefügt, verändert oder gelöscht.
18
1 Das Domino-Prinzip
Es gibt verschiedene Möglichkeiten, Dokumente in Domino-Datenbanken zu selektieren. Hierzu mehr in Kapitel 7.1.4ff. Der wichtigste und effizienteste Weg erfolgt über Ansichten. Diese Ansichten können mit verschiedenen Methoden durchsucht werden. getAllDocumentsByKey () durchsucht z.B. die erste(n) Spalte(n) einer Ansicht anhand eines Schlüsselwortes. Eine Java-basierte Domino-Datenbank, oder besser gesagt Domino-Anwendung besteht in der Regel aus einigen Masken und Ansichten (und vielen anderen Gestaltungselementen, s. Kap. 3 und 6), die das graphische Interface für den Benutzer darstellen und Java-Programmen, die im Hintergrund Funktionen ausführen, bzw. Funktionen, die über Aktionsbuttons durch den Anwender ausgelöst werden. Alternativ kann Domino natürlich auch aus stand-alone Java-Programmen oder auch mit Servlets und JSP angesprochen werden. Nicht zuletzt können alle DominoAnwendungen oft nahtlos, ohne zusätzliche Programmierung, über einen Browser angezeigt werden. Hier kommen dann, neben den nativen Rendering-Möglichkeiten von Domino, die es erlauben, Notes-Masken „on the fly“ im Browser anzuzeigen, die J2EE-Techniken zum Einsatz. Insbesondere das Lotus Domino Toolkit for WebSphere Studio ist ein sehr hilfreiches Plugin für den Einsatz der Domtags Tag Library, mit der Domino-Objekte durch die Verwendung von JSP dargestellt werden können.
1.8
Die Client-Server-Umgebung Lotus und Domino
Lotus Domino war schon immer mit Remotefähigkeiten ausgestattet, eines der wichtigen Features von Domino war für viele Jahre die Remoteeinwahl in einen Domino Server über Telefon. Anwender konnten Domino-Applikationen lokal auf ihren Rechnern betreiben und bedienen. Sollten Daten zwischen dem Client-System und dem Server aktualisiert werden, wählte der Benutzer sich ein und über die Replikation erfolgte ein Datenabgleich. Der gesamte Datenabgleichsvorgang ist für den Benutzer transparent. Inzwischen sind die Leitungskapazitäten im LAN und WAN erheblich größer, so dass sich die Infrastruktur von Domino ebenfalls verändert hat. Immer mehr Remoteanwender wählen über WAN direkt die Notes-Anwendungen an oder verbinden sich mit ihrem Home-Server über so genannte Durchgangsserver, die zur Verbindung von Netzwerken aus Sicherheitsgründen zwischengeschaltet werden. Die Replikation ist weiterhin ein wichtiges Instrument, um in verteilten Umgebungen die Anwendungsdaten verschiedener Server aktuell zu halten und um verschiedene Netzwerke und Sicherheitszonen miteinander abgleichen zu können. So genannte Replikationsregeln ermöglichen es, in vorgelagerten Servern, die dem Internet „ausgesetzt“ sind, Daten entgegenzunehmen und „nach innen“ zu replizieren, so dass auf Servern, in weniger sicheren Umgebungen, Daten immer nur kurz vorliegen und dann per Replikation in das sichere Intranet transportiert werden können. Die Netzwerkfähigkeit von Domino in seinen verschiedenen Ausprägungen hat einen großen Einfluss darauf, wie in der Java-Programmierung auf Domino zugegriffen wird.
1.8 Die Client-Server-Umgebung Lotus und Domino • • • •
19
Zugriff lokal auf Domino-Datenbanken des Clients (Kapitel 4.3) Zugriff lokal auf einen Domino-Server (Kapitel 4.3) Zugriff per DIIOP (Domino IIOP) auf einen entfernten Server (Kapitel 5.3.2) Zugriff auf eine entfernte Domino-Datenbank auf einem als vertrauenswürdig eingestuften Server über die aktuelle Notes Session (seit Domino Version 6)(Kapitel 5.3)
Um eine Verbindung zu Domino-Datenbanken, egal ob lokal oder remote, herzustellen, bedarf es immer einer so genannten Notes Session. Eine Notes Session wird bei der Java-Programmierung innerhalb einer Domino-Datenbank, also bei der Erstellung eines Java-Agenten, über AgentBase.getSession() zur Verfügung gestellt. Für alle anderen Arten der Verbindungsherstellung muss über die Klassen NotesThread oder NotesFactory eine Session aufgebaut werden. Diese verschiedenen Zugriffe können auf unterschiedlichen Wegen ausgelöst werden: • • • •
Stand-alone Java-Programm (Kapitel 4.4) Domino-Agent (Kapitel 4.10) Servlet im Domino Servlet Manager (läuft im HTTP-Task des Domino-Servers)(Kapitel 5.2.1) Servlet oder JSP in einem Servlet Manager außerhalb von Domino (z.B. WebSphere Application Server) (Kapitel 16 und 17)
Diese unterschiedlichen Methoden erfordern verschiedene Authentifizierungen. Ein Servlet, das z.B. im Domino Servlet Manager aufgerufen wird, erhält bei der Initialisierung einer Notes Session per Default die Rechte des Domino-Servers, genauer gesagt der so genannten Server-ID, unter der der Domino-Server läuft. Standardmäßig werden dann die Aktionen dieser Notes Session als lokale Zugriffe behandelt, d.h. aus Sicht des Servlets ist der Server lokal. Alternativ wird die Notes Session des Servlets mit den Rechten des über den Browser angemeldeten Benutzers aufgebaut. Hierbei übernimmt Domino die Authentifizierung gegen sein Adressbuch oder ein entsprechend nachgeordnetes LDAP-Verzeichnis. Wird ein Application Server auf der gleichen Hardware wie der Domino-Server betrieben, dann sind ebenfalls lokale Zugriffe möglich. Alle anderen Fälle erfolgen dann remote. Die Authentifizierung kann im so genannten SSO (Single Sign On) erfolgen. Alternativ kann ein spezieller Domino-Benutzer definiert werden, unter dem dann authentifiziert wird. Hieraus ergeben sich grundsätzlich zwei Konzepte für die Authentifizierung: A
Authentifizierung über einen dedizierten Benutzer oder über die Server-ID. Alle Domino-Daten sind für diesen einen Benutzer/Server zugänglich. Die Unterscheidung der Rechte für die einzelnen Endanwender erfolgt innerhalb der Anwendung. – –
Vorteil: Kein administrativer Aufwand für die Benutzerverwaltung. Nachteil: Die Möglichkeiten der granulären Rechtevergabe in Domino werden nicht ausgeschöpft.
20 B
1 Das Domino-Prinzip Authentifizierung über registrierte Domino-Benutzer. Alle Lese- oder Schreibzugriffe auf Daten werden mit den Rechten des angemeldeten und im Domino-Adressbuch registrierten Benutzers abgeglichen. –
–
Vorteil: Jeder authentifizierte Benutzer kann nur die Daten sehen / ändern, die im Domino-Rechte-System für ihn vorgesehen sind. Rollen, Leser- und Autorenfelder. Nachteil: Mehraufwand für Administration der Benutzerverwaltung.
Die Architektur einer Java-Anwendung wird letztendlich festlegen, wie die Verbindung und die Authentifizierung mit dem Domino-Server hergestellt werden muss.
1.9
Zusammenfassung
Domino ist ein robuster Application Server mit einer langjährigen Geschichte. Durch die Vielzahl integrierter Services auf der Basis von RFC-Standards bietet sich dem Java-Entwickler eine Plattform, die auf viele vorgefertigte Tools und Dienste zurückgreifen kann. Insbesondere für Groupware-Anwendungen in verteilten Umgebungen zeigt Domino seine Stärken. Durch das Konzept der offenen Schemata für die Speicherung von Daten, Objekten, Gestaltungselementen und Zugriffskontrolllisten ist Domino eine Plattform, die durch den Anwender selbst einfach erweitert werden kann. Das wichtigste Grundprinzip der Organisation von Daten in einer Domino-Datenbank ist: • • • •
Speicherung der Daten in Dokumenten Aufteilen der Daten in Items Erstellung von Ansichten zur Indizierung von Dokumenten Selektion von Dokumenten über die Suche in Ansichten
Interessant ist für den Java-Programmierer das Konzept der Dokumente und Items, durch das Daten und Felder in Datensätzen beliebig erweitert werden können. Ansichten sind ein wesentliches Selektionswerkzeug für die Auswahl von Dokumenten. Java ist die wichtige Programmiersprache für die Anwendungsentwicklung in Lotus Domino und kann auf vielfältige Weise eingesetzt werden, insbesondere Servlets können direkt in den Domino-Server integriert werden oder über externe Application Server wie WebSphere auf Domino-Daten zurückgreifen. Mit der Version 7 wird Domino mit vielen neuen Möglichkeiten aufwarten. Insbesondere die Integration von DB2-Daten stellt eine interessante Erweiterung dar und lässt die J2EE-Application-Server-Welt und die Domino-Welt näher zusammenwachsen.
1.9 Zusammenfassung
21
Da die Stärken von Domino im Groupware- und Collaboration-Bereich liegen, eröffnet sich durch die Verwendung von Java die Möglichkeit, solche Anwendungen im Web mit einem robusten J2EE Layer auszustatten ohne auf die Stärken von Domino verzichten zu müssen.
2
2 Notes @ Domino
In diesem Kapitel: Einführung in die grundlegenden DominoProgrammierkonzepte Die Domino-Note und das Domino-Speicher- und Objektmodell Replikation in Domino Suchen und Finden Sicherheit in Domino
24
2 Notes @ Domino
Zur Java-Programmierung einer Lotus-Domino-Anwendung gehört ein Grundverständnis der Domino-Objekte, das in diesem Kapitel mit den Grundprinzipien der Objektspeicherung und -struktur vermittelt wird. Eine Java-Anwendung kann auf verschiedene Arten in eine Domino-Infrastruktur eingebunden werden bzw. Zugriff auf die Domino-Objekte erhalten. Der von Domino hierfür zunächst vorgesehene Weg ist die Erstellung eines so genannten Agenten. Domino-Agenten sind Code-Container, die eventgesteuert, periodisch oder durch manuelle Aktionen gestartet werden können. Dieses Verfahren wird in dem folgenden Beispiel, anhand der Erstellung einer kleinen Anwendung, mit einem solchen Agenten aufgezeigt und die Grundprinzipien der Domino-Objekte „Document“, „Item“, „Form“, „View“ und „Note“ erläutert. Ganz nebenbei ist diese Anwendung ein schönes Beispiel für einen Anwendungsfall, der in Domino nur mit Java gelöst werden kann, da es auf Java-Klassen zurückgreift, die z.B. in LotusScript generisch nicht vorliegen.
2.1
„Hello WWWorld“ – URL-Loader-Anwendung
Nach der bisher noch recht theoretischen Einführung soll mit diesem Beispiel die praktische Arbeit der Java-Programmierung für Domino eröffnet werden und selbstverständlich wird die Anwendung zunächst den Text „Hallo Welt“ ausgeben, um aber einen guten Überblick über die Programmierung in Lotus Domino zu geben, geht dieses Beispiel noch ein wenig darüber hinaus. Zunächst wird natürlich ein IDE benötigt, mit dem die einzelnen Bestandteile erstellt und programmiert werden können. Es wird für Domino als Teil des Notes Clients mitgeliefert und als Domino Designer (auch Lotus Notes Designer Client genannt) bezeichnet, über den im Wesentlichen die Programmierung von Designelementen, die der grafischen Benutzer-Interaktion dienen, erfolgt. Auch wenn der wesentliche Schwerpunkt dieses Buches auf der Programmierung von Java für Domino liegt, die – wie noch zu zeigen sein wird – besser außerhalb des Domino Designers erfolgt, ist es für den Java-Programmierer wichtig, ein Grundverständnis der Domino-Objekte zu haben. Die Erstellung von grafischen Oberflächen für Anwender des Lotus Notes Clients liegt außerhalb des Ziels dieses Buches und wird hier nur kurz angeschnitten. Für die Programmierung von Oberflächen für den Notes Client ist als weiterführende Literatur die Lotus Notes and Domino 6 Programming Bible [Programming Bible] empfehlenswert. Die Beispielanwendung wird Folgendes leisten: • • • •
Die Worte „Hello World“ als Ansprache an den aktuellen Benutzer richten. Die Eingabe einer URL ermöglichen. Diese URL abrufen und den HTML-Code in einem Notes-Dokument ablegen. Dem Benutzer eine Liste aller bisher abgerufenen URL anzeigen.
Anhand dieser Funktionen werden folgende Programmierprinzipien erläutert:
2.1.1 Datenbank anlegen
• •
• •
25
Die Notes-Maske als grafisches und programmatisches Element für die Anzeige von Daten im Notes Client. Der Notes View als grafisches Element zur Anzeige von vorselektierten Dokumenten und als programmatisches Element zur Suche oder Auswahl von Dokumenten. Die Aktion zur Ansteuerung eines Notes-Agenten. Der Notes-Agent zur Durchführung von Backend-Programmierung in Java.
Zunächst wird der Lotus Notes Client und der Lotus Domino Designer Client benötigt. Trial-Versionen sind bei IBM [developerWorks] erhältlich. Beachten Sie, dass Sie den Client einschließlich des Designers downloaden. Der Client ist einfach mit dem Installer zu installieren. Die Frage während des Setup, nach dem ersten Start des Clients, ob eine Verbindung mit einem Domino-Server hergestellt werden soll, kann zunächst mit „Nein“ beantwortet werden, sofern Sie noch keinen Server installiert haben, andernfalls geben Sie hier Ihren Domino-Server an.
2.1.1
Datenbank anlegen
Wie bereits eingangs erläutert, werden alle Daten und Programm-Elemente einer Domino-Anwendung in der so genannten NSF-Datei gespeichert. Diese Datei oder auch Datenbank genannt, kann entweder lokal auf der Arbeitsstation des Anwenders (oder Programmierers) erstellt werden oder auf einem Server. Um eine neue Datenbank anzulegen, wird zunächst der Domino Designer Client gestartet und im Menü2 „File“ der Eintrag „Database“ -> „New“ gewählt. Für die Beispielanwendung kann die Datenbank lokal angelegt werden, d.h. im Feld „Server“ wird der voreingestellte Wert „local“ belassen (Abb. 2-1). Lokale Datenbanken werden im Datenverzeichnis (data directory) des Notes Clients angelegt. Bei einer Standard-Installation auf einem Windows-System ist dies „C:\Lotus\Notes\ data“ oder „C:\Programme\Lotus\Notes\data“. Der Titel der Datenbank kann frei gewählt werden. Wählen Sie bitte „K2 URL Loader“ als Titel. Auch der Dateiname der Datenbank kann zwar frei gewählt werden, aber es empfiehlt sich hier einige Regeln zu beachten. Auch wenn Domino vorbildlich mit internationalen Zeichen und Groß-Kleinschreibung umgeht, hat es sich bewährt, für Dateinamen nur die üblichen Zeichen a bis z, A bis Z, den Unterstrich _ und 0..9 zu verwenden. Insbesondere auf Unix-Systemen, beim Aufruf von Datenbanken über die Domino URL (s. Kap. 7.1.4.2) macht man sich hier das Leben leichter. 2
Die Beschreibung der Domino-Anwendungen bezieht sich auf die englischen Versionen des Notes Clients, des Domino Designers, Administrators und Servers. Da der überwiegende Teil der Literatur und insbesondere die Foren zu diesem Thema im englischsprachigen Raum zu finden sind, hat dies den großen Vorteil, dass Begriffe und Bezeichnungen dort leichter wiedergefunden werden können und Bezüge leichter herzustellen sind.
26
2 Notes @ Domino
Wählen Sie als Dateiname „djbuch\ urlloader.nsf“. Durch den Backslash „\“ wird ein Ordner namens „djbuch“ angelegt. In diesem Ordner wird die Datenbank mit Namen „urlloader.nsf“ angelegt. Im Dialogfenster zeigt sich eine weitere Spezialität von Domino. Datenbanken können auf Basis von so genannten „Templates“ oder auch Schablonen erstellt werden. Eine so erstellte Datenbank wird als Kopie dieser Schablone erstellt und erbt die Funktionen und Eigenschaften der Schablone. Zum einen wird dieser Mechanismus zur Auslieferung von StandardAnwendungen an Kunden verwendet. Außerdem liefert Lotus eine große Aus- Abb. 2-1 Neue Datenbank erstellen wahl von Schablonen mit, die für Systemfunktionen, aber auch für viele Standard-Anwendungsfälle verwendet werden können (z.B. Diskussionsforen, Dokumenten-Datenbanken, Gruppen-E-Mail und andere). Die Datenbank für die Beispielanwendung wird natürlich aus der Schablone „-Blank-“ erstellt.
2.1.2
Maske anlegen
Um einem Benutzer Daten im Notes Client anzeigen zu können, oder um ihn Daten eingeben lassen zu können, muss in Notes eine so genannte Maske (engl. form) angelegt werden. Die Notes-Maske ist ein sehr mächtiges Gestaltungselement. Zunächst ist es ein Filter, mit dem bestimmt wird, welche Daten eines Datensatzes (also eines Dokuments) anzuzeigen sind. Daten werden in einem Dokument in so genannten Items gespeichert. Korrespondierend hierzu gibt es in der Maske Felder (engl. field), mit denen festgelegt wird, welche Items (auf welche Art) anzuzeigen sind. Darüber hinaus können noch viele programmatische Aufgaben mit der Maske erfüllt werden (s. Kap. 3.2). Damit der Benutzer der URL-Loader-Anwendung eine URL eingeben kann, wird eine Maske mit mindestens einem Feld zur Eingabe der URL benötigt. In der neu angelegten Datenbank wird eine neue Maske angelegt, indem Sie wie folgt vorgehen: A
B
Im Menü „Create“ findet sich der Eintrag „Design“ und dort der Eintrag „Form“. Alternativ kann mit dem Klick auf den Button „New Form“ eine neue Maske erstellt werden. Die neue Maske stellt sich im Designer zunächst als leere weiße Fläche dar. Ähnlich wie in einem HTML-Editor im WYSIWYG-Modus kann nun zeilenorientiert das Layout der Maske erstellt werden.
2.1.2 Maske anlegen C
27
Tippen Sie die Headline Klicken Sie im Lotus Domino Designer auf Objekte „URL Loader“ für die Maske und Einträge mit der rechten Maustaste. ein. Markieren Sie den Text. Klicken Sie mit der rechten So erreichen Sie die Eigenschaften der Objekte und Maustaste auf den Text und viele andere Funktionen. wählen Sie „Text Properties“. Hier können Schrift-Eigenschaften für die Überschrift eingestellt werden. D Fügen Sie nun nach einer Leerzeile die entscheidenden Worte „Hello World!“ in die Maske ein. E Aktivieren Sie in den Eigenschaften der Maske im zweiten Tabellenreiter die Eigenschaft „Automatically enable Edit Mode“ (Abb. 2-2). F Ein wichtiges Layout-Element in Notes ist die Tabelle. Wählen Sie „Create“ -> „Table“. Übernehmen Sie die Voreinstellungen, geben Sie aber 5 für die Zeilenzahl an. Nach dem Erstellen der Tabelle klicken Sie mit der rechten Maustaste in die linke Spalte und wählen Sie „Table Properties“. Stellen Sie die Breite der Spalte auf 4 cm ein. Auf die gleiche Art geben Sie der rechten Spalte eine Breite von 10 cm. G Geben Sie in die linke Spalte die Beschriftungen für die Felder ein: „Titel“, „URL“, „URL laden“, „Beschreibung“ und „HTML-Code der Seite“. H In der rechten Spalte werden nun Felder erstellt. Ein Feld wird über das Menü „Create“ -> „Field“ erstellt. Ein Klick mit der rechten Maustaste öffnet die Feld-Eigenschaften. Erstellen Sie auf diese Weise in der ersten, zweiten und vierten Zeile die Felder „F_Titel“, „F_URL“ und „Body“. Ausnahmen bestätigen bekanntlich die Regel. In den meisten DominoApplikationen hat es sich eingebürgert, dass in einer Maske, die nur ein Feld mit RichText enthält, dieses Feld den Namen „Body“ erhält. Gleiches gilt für das Feld „Title“ oder „Subject“. Unsere Maske wird diese Felder berechnen. Abb. 2-2 Maskeneigenschaften
28 I J
2 Notes @ Domino Das Feld Body soll ein RichText-Feld werden. In den Feld-Eigenschaften können Sie die Eigenschaft „Typ“ auf „RichText“ ändern. In der fünften Zeile, neben der Beschriftung „HTML-Code der Seite“ soll der geladene HTML-Code angezeigt werden. Dieser Code wird im Backend durch den Agenten erzeugt und im Item F_Code gespeichert. Um dieses Item anzuzeigen, soll kein Feld erstellt werden, damit der Benutzer keine Änderungen vornehmen kann. Stattdessen wählen Sie im Menü „Create“ -> „Computed Text“. Im rechten unteren Rahmen des Designers erscheint ein leeres weißes Feld mit der Überschrift „Computed Text : Value“. Tragen Sie hier F_Code ein. Dies ist der Name des Items, in dem der Java-Agent seine Daten speichern wird. Das Eintragen des Namens eines Items stellt bereits eine Berechnung dar und evaluiert in den Wert des Items.
Best Practice: Verwenden Sie Aliasnamen für alle Objekte. Aliasnamen für Masken (und andere Objekte) werden einfach durch Anhängen des PipeSymbols „|“ und des Aliasnamens definiert. Verwenden Sie eine Nomenklatura, die an Ihre Bedürfnisse angepasst ist, an die sich aber alle Entwickler ihres Teams halten müssen. Sinnvolle Aliasnamen beginnen mit einem (oder zwei) Buchstaben für den Typ des Objekts und einem Unterstrich, also z.B.
F_<> für ein Feld FO_<> für eine Maske V_<> für eine Ansicht, usw. Aliasnamen sind wichtig, damit der Darstellungsname unabhängig vom programmatischen Namen bleibt. Die führenden Kennzeichner sind sehr wichtig für Übersetzungen mit der Domino Global Workbench [Lotus, DGW] oder beim globalen Suchen-Ersetzen. Durch den führenden Identifizierer können so objektspezifische Operationen auf eine bestimmte Art von Objekten global angewandt werden. Zusätzlich entsteht so leicht lesbarer Code.
2.1.2 Maske anlegen K
29
Unterhalb der Tabelle erstellen Sie zwei neue Felder mit Namen „Subject“ und „Title“ (englische Schreibweise!), um den allgemeinen Gepflogenheiten zu genügen. Diese Felder erhalten die Eigenschaft „computed“ – Abb. 2-3. Wenn das Feld Subject ausgewählt ist, erscheint im unteren rechten Frame des Designers ein leeres Feld mit der Überschrift „Subject (Field) : Value“. Hier wird die Berechnung für dieses Feld eingegeben. Für unseren Fall genügt es, hier den Namen des Feldes einzutragen, dessen Wert übernommen werden soll: F_Titel. Gibt der Benutzer nun in das Feld F_Titel ei- Abb. 2-3 Berechnetes Textfeld nen Text ein, wird das Feld Subject den gleichen Wert annehmen. L Da die Felder Subject und Title nur internen Zwecken dienen, sollen sie nicht für den Benutzer sichtbar sein. Durch so genannte „Hide-When“-Formeln können Felder vor der Anzeige verborgen werden. Hierbei handelt es sich um KEINE Sicherheitsfunktion. Die Felder werden weder durch Zugriffsrechte noch etwa durch eine Encryption geschützt. Hide-When-Formeln dienen lediglich der bequemen und flexiblen Anzeige (oder Nicht-Anzeige) von Inhalten. Markieren Sie die gesamte Zeile mit den beiden Feldern, öffnen Sie mit der rechten Maustaste die Text-Eigenschaften und wählen Sie das Symbol, das entfernt an den zu flach geratenen Buchstaben „T“ erinnert. Dieses Symbol steht für eine Jalousie. Wählen Sie alle drei Checkboxen für Notes R 4.6, Web und Mobil aus (s. Abb. 2-5). M Nun soll noch ein Aktions-Button für das Laden einer HTML-Seite (natürlich durch Starten eines Java-Agenten) erstellt werden. Setzen Sie den Cursor in die dritte Zeile der rechten Spalte und wählen Sie im Menü „Create“ -> „Hotspot“ -> „Button“. In den Eigenschaften des Buttons (rechte Maustaste) können Sie die Beschriftung des Buttons in „URL laden“ ändern.
30
2 Notes @ Domino
N Die eigentliche Berechnung für das Feld erfolgt mit so genannten @-Formeln (sprich: „ät-Formel“). Geben Sie im rechten unteren Rahmen mit der Überschrift „URL Laden (Button) : Click“ folgende Formel ein: @Command ([FileSave]); @Command ([ToolsRunMacro];"AG_UrlLaden"); @PostedCommand ([ViewRefreshFields]);
O Im Einzelnen bedeuten diese Formeln: P Durch @Command ([FileSave]); wird das Dokument gespeichert, damit der Agent auf aktuelle Daten trifft. Q Durch @Command ([ToolsRunMacro];"AG_UrlLaden"); wird der Agent mit (Alias-) Namen „AG_UrlLaden“ gestartet (Abb. 2-4).
Abb. 2-4 @Formel „@command ([ToolsRunMacro])“ startet einen Java-Agenten
R
S
T
Durch @PostedCommand( [ViewRefreshFields] ); werden die Felder der Maske neu berechnet. Hierdurch wird erreicht, dass der oben erstellte berechnete Text für die Anzeige des neu geladenen HTML-Codes aktualisiert wird. @PostedCommand statt @Command muss verwendet werden, damit diese Befehle NACH den vorangegangenen ausgeführt werden. Dies ist ein etwas unerwartetes Verhalten und im Wesentlichen historisch bedingt. Die @-Formelsprache kommt in Lotus Domino in der Regel dann zum Einsatz, wenn innerhalb von Masken (oder Ansichten oder einigen anderen Gestaltungselementen) einfache Berechnungen oder Validierungen durchgeführt werden sollen. Bei der Java-Programmierung spielen diese nur eine Rolle am Rande. Da die @Formeln aber für einen Laien leicht verständlich sind und für Masken eine wichtige Rolle spielen, sollte der Java-Programmierer sich der @-Formeln bewusst sein. Nähere Informationen zu den @-Formeln finden Sie in der Lotus Domino Designer-Hilfe in der Lotus Developer Domain [Lotus, LDD], die auch online [Lotus, Designer] und [Lotus, R6Formula] zur Verfügung steht. Der zugehörige zu startende Agent fehlt noch, wird aber weiter unten erstellt. Wählen Sie „Datei“ -> „Speichern“ im Menü, um die Maske zu speichern. In den nun abgefragten Namen für die Maske geben Sie bitte ein:
2.1.2 Maske anlegen
31
Kapitel 2\1. URL Loader Dokument | FO_URL_Loader_k2 Links des Pipe-Symbols wird der Klar-Name der Maske vergeben. Dieser Name wird dem Anwender im Notes Client angezeigt. Rechts des Pipe-Symbols wird ein programmatischer Aliasname vergeben, unter dem die Maske in der Programmierung angesprochen werden kann. Das Backslash-Zeichen „\“ kategorisiert die Maskeneinträge im Menü „Create“. Die Vergabe von Aliasnamen ist zwar nicht vorgeschrieben, hat sich aber als Best Practice durchgesetzt.
Abb. 2-5 Fertiggestellte Maske und Hide-When-Formeln
Die Maske ist jetzt fertig (Abb. 2-5), aber noch fehlen einige Bestandteile, um die Anwendung richtig testen zu können. Dennoch kann die Maske auch schon allein stehend gestartet werden. Wenn die Maske geöffnet ist, wählen Sie im Menü „Design“ den Eintrag „Preview in Notes“. Sie können auch links oben das kleine runde orangerote Notes-Icon wählen. Nun startet der Notes Client und zeigt eine neue Maske mit leeren Feldern an. Die Felder Titel, URL und HTML-Code werden als bearbeitbar angezeigt und können verändert werden. Bei der Vorschau fallen zwei kleine Unzulänglichkeiten auf.
32 A
2 Notes @ Domino Der Fenstertitel lautet „untitled“. Hier handelt es sich um eine der häufigsten Unaufmerksamkeiten beim Erstellen einer Maske. Sie können dies beheben, indem Sie im unteren mittleren Frame des Designers bei geöffneter Maske unter „Objects“ auf „Window Title“ klicken und rechts folgende Formel eingeben: @if (@isNewDoc; "Neues Dokument";F_Titel);
B
Diese Formel sorgt dafür, dass im Fenstertitel der Text „Neues Dokument“ erscheint, solange das Dokument noch nicht gespeichert wurde, andernfalls wird der vom Benutzer eingegebene Titel angezeigt. Der Maske fehlt die Möglichkeit, gespeichert zu werden. Sie können bis auf weiteres hierfür die Tastenkombination STRG-S verwenden oder im Menü „File“ den Eintrag „save“. Wenn Sie ein in A neu erstelltes Dokument so speichern, können Sie beobachten, wie sich der Fenstertitel entsprechend Ihren Eingaben ändert.
Um Ihre so gespeicherten Dokumente wiederzufinden, benötigen Sie die Ansicht, die Sie in Kapitel 2.1.3 erstellen.
2.1.3
Ansicht anlegen
Mit der zuvor erstellen Maske erreichen Sie zweierlei. Zum einen kann der Benutzer Datensätze anlegen, die die gewünschten URL enthalten. Gleichzeitig ist die Maske das GUI, mit dem der Benutzer einen Agenten ansteuert, der die HTML-Daten aus dem Internet herunterladen wird. Bevor nun der Agent erstellt werden soll, wird noch eine Ansicht benötigt, damit alle erstellten Datensätze nicht in der Datenbank „verschwinden“, sondern jederzeit vom Benutzer (und wie noch zu zeigen sein wird, vom Java-Backend) wiedergefunden, selektiert, geöffnet, geändert und gelöscht werden kann. Also erfüllt auch die Ansicht mehrere Aufgaben. Neben der beschriebenen Aufgabe, Daten übersichtlich, optional sortiert und tabellarisch darzustellen, ermöglicht sie es dem Java-Programmierer, effizient Dokumente nachzuschlagen. Um eine neue Ansicht zu erstellen, gehen Sie wie folgt vor: A
B C
Im linken Frame der Arbeitsoberfläche des Designers findet sich unter anderem der Eintrag „Views“. Wählen Sie diesen Eintrag. In der sich öffnenden Übersicht finden Sie bereits eine Ansicht „untitled“. Löschen Sie diese Ansicht. Über der nun im rechten Frame erscheinenden Übersicht findet sich der Button „New View“. Klicken Sie diesen Button. Wählen Sie folgende Einstellungen für die neue Ansicht (Abb. 2-6): View name: Kapitel 2\URL Loader View type: „Shared“ Selection conditions: SELECT Form = „FO_URL_Loader_k2“
2.1.3 Ansicht anlegen
33
Beachten Sie: Rechts von dem Schlüsselwort „SELECT“ handelt es sich erneut um eine @-Formel. In diesem Fall wird lediglich ein Feld (Form) mit einer Konstante ("FO_URL_Loader_k2") verglichen. An dieser Stelle kommt zum ersten Mal der eingeführte Alias zum Tragen. Soll nun – aus welchem Grund auch immer (z.B. Übersetzung) – der Name der Maske (in der Darstellung für den Benutzer) geändert werden, dann kann der Klarname Abb. 2-6 Neue Ansicht erstellen der Maske getrost von „Kapitel 2\1. URL Loader Dokument“ (s. Abb. 26) in einen beliebigen neuen Wert geändert werden, ohne dass der Bezug zu dieser Selektionsformel verloren ginge. Durch diese Selektionsformel wird festgelegt, dass in dem View nur solche Dokumente angezeigt werden, deren Item mit dem Namen „Form“ den Inhalt „FO_URL_Loader_k2“ hat (in dem Item „Form“ wird automatisch der Name, bzw. Aliasname der Maske gespeichert). D Ein Klick auf „Save and Customize“ öffnet die neue Ansicht. E Die neue Ansicht hat bereits eine Spalte mit dem Titel „#“. Diese wird so nicht benötigt und muss über die Eigenschaften der Spalte (rechte Maustaste auf den Spaltentitel) verändert werden. In den Eigenschaften wählen Sie bitte „URL“ als Spaltentitel. F Erstellen Sie zwei zusätzliche Spalten durch einen Doppel-Klick auf die Spaltentitel rechts neben der vorhandenen Spalte. Bezeichnen Sie die Spalten mit „zuletzt geladen am“ und „Autor“. Die Breite der Spalte „URL“ setzen Sie bitte auf 20, die Breite der Spalte „zuletzt geladen am“ auf 14 und die Autoren-Spalte auf 20. G Um diese drei neuen Spalten mit Leben zu füllen, müssen Sie eine Berechnung oder einen Feldnamen in das dafür vorgesehene Feld eingeben. Wählen Sie die Spalte durch einen Klick auf die Spaltenüberschrift an. Im rechten unteren Frame des Designer-Arbeitsplatzes erscheint (befindet sich) ein leeres weißes Fenster (Rahmen) mit der Überschrift „URL (Column) : Column Value“ (Abb. 2-7). In dem Rahmen wählen Sie nun „field“ statt „simple function“ und selektieren den Feldnamen „F_URL“.
34
2 Notes @ Domino
H In der zweiten Spalte soll angezeigt werden, wann die HTML-Seite der URL aus dem Feld F_URL zuletzt geladen wurde. Dieser Wert wird natürlich nicht durch den Benutzer eingegeben, sondern durch den Agenten nach erfolgreicher Arbeit in einem Item gespeichert. Da dieses Item nicht in der Maske zu finden ist, kann hier nicht direkt ein Feldname ausgewählt werden, sondern er wird über eine @Formel Abb. 2-7 Spaltenwert ändern angesprochen. Wählen Sie „Formel“ im Rahmen für den Spaltenwert aus und geben Sie F_Ladezeit ein. Dies wird der Name des Items sein, das durch den Agenten gefüllt werden wird. I In der dritten Spalte soll der Name des Autors des Dokuments angezeigt werden. Geben Sie im Spaltenwert für die dritte Spalte die Formel @author ein. Diese @Formel zeigt den Autor des Dokuments an. J Durch Klick mit der rechten Maustaste auf den oberen rechten Rahmen können Sie die Ansicht-Eigenschaften öffnen. Hier gibt es ein eigenes Feld für den Alias. Das Pipe-Symbol muss daher an dieser Stelle nicht verwendet werden. Bitte geben Sie „V_URL_Loader_k2“ als Alias ein. K In den Eigenschaften der Ansicht findet sich ein „Reiter“ mit einem „i“ als Symbol. Hier aktivieren Sie die oberste Schaltfläche für „Default when database is first opened“. L Speichern Sie die Ansicht über „File“ -> „Save“. Auch eine Ansicht können Sie über die Preview-Funktion anzeigen. Wählen Sie „Design“ -> „Preview in Notes“. Die Ansicht öffnet sich und Sie können die in Kapitel 2.1.2 erstellten Dokumente sehen. Um neue Dokumente erstellen zu können, müssen Sie nicht erneut in den Designer wechseln. Notes platziert die Klarnamen von Masken automatisch im Menü „Create“ des Clients. Hier finden Sie den Eintrag „Kapitel 2\1. URL Loader Dokument“, mit dem Sie ein neues Dokument erstellen können.
2.1.4
Agent anlegen
Die beiden wichtigsten Elemente der Domino-Anwendung sind bereits fertig. Eine Maske wurde angelegt, um Daten durch den Benutzer in Empfang zu nehmen, bzw. um vorhandene Dokumente anzuzeigen. Eine Ansicht ermöglicht dem Benutzer, den Überblick über die bereits angelegten Datensätze zu gewinnen. Um den Agenten zu erstellen, der die gewünschten HTMLSeiten in das Dokument lädt, gehen Sie wie folgt vor:
2.1.4 Agent anlegen
35
Abb. 2-8 Basisgerüst eines Java-Agenten
A B
C
Im linken Frame des Designers wählen Sie „Shared Code“ -> „Agents“. Wählen Sie im rechten Frame „New Agent“. Ein Agent kann auf verschiedene Weise programmiert werden. Wählen Sie im rechten Frame im Aufklappmenü mit dem Eintrag „Simple action(s)“ den Eintrag Java. Die Darstellung im rechten Frame wird sich verändern und das Basisgerüst eines Java-Agenten erscheinen (Abb. 2-8). Geben Sie in den Agenteneigenschaften den Namen „Kapitel 2\1. URL Laden| AG_UrlLaden“ ein. Beachten Sie auch hier das Pipe-Symbol „|“ für die Eingabe eines Alias-Namens. Die Bedeutung des Namensbestandteils „Kapitel 2\1.“ wird in Kapitel 2.1.6 erläutert.
Der eigentliche Java-Agent hat folgenden Code (Listing 2-1): Jeder Java-Agent, der wie oben beschrieben im Designer programmiert wird, muss die AgentBase erweitern. In der AgentBase liefert Domino wichtige Objekte mit. Standardmäßig benennt Domino Designer diese Klasse mit JavaAgent. Wenn auch nicht zwingend erforderlich, ist es guter Stil, diesen Namen zu ändern.
Anmerkung: Der Einfachheit halber werden Imports immer mit Joker-Symbol * dargestellt. Best practice wäre natürlich, alle Pakete einzeln aufzulisten.
Listing 2-1 URL-Loader-Java-Agent – Sourcecode
Insbesondere bei Exceptions und wenn viele Agenten auf einem Server betrieben werden, ist dies unerlässlich, um feststellen zu können, welche Klasse, also welcher Agent, eine Fehlermeldung verursacht hat. Einstiegspunkt für einen derartigen Agenten ist die Methode NotesMain ()3, die immer implementiert werden muss. 3
Die Großschreibung der Methode NotesMain ist eine Eigenart des Domino-Java-Agenten, die leider nicht den Standards entspricht.
2.1.4 Agent anlegen
37
Best Practice: Es ist guter Stil, den Namen der Basisklasse zu ändern. Insbesondere bei Exceptions und wenn viele Agenten auf einem Server betrieben werden, ist dies unerlässlich, um feststellen zu können, welche Klasse, also welcher Agent, eine Fehlermeldung verursacht hat.
Neben Informationen über den Agenten ist hierin insbesondere die Notes Session enthalten. Eine Notes Session ist immer notwendig, wenn auf DominoObjekte zugegriffen werden soll, da alle Objekte von der Session abhängen. Die Notes Session bildet nicht nur die Verbindung zu einer Datenbank (lokal) oder einem Server (remote) ab, handelt diese und kümmert sich zum Beispiel ums Caching, sondern in der Session ist vermerkt, wer der aktuell angemeldete Benutzer ist. Der AgentContext liefert Informationen über den aktuell betriebenen Agenten. Neben der aktuellen Datenbank, die durch getCurrentDatabase geliefert wird, liefert der AgentContext Informationen über den Status des Agenten, also ob der Agent z.B. im Schedule schon einmal gelaufen ist, und auf wieviele Dokumente der Agent zugegriffen hat. Im AgentContext ist ebenfalls gespeichert, welches das aktuell geöffnete Dokument ist. Dies kann entweder das ausgewählte Dokument in einer Ansicht sein, oder aber wie in unserem Fall das geöffnete Dokument. Wird ein vom Benutzer geöffnetes Dokument wie in dem Beispiel durch einen Java-Agenten im Backend manipuliert, ist Vorsicht geboten. Wenn dieser Agent Daten im Dokument manipuliert, dann muss nach Beendigung des Agenten das Dokument aktualisiert werden. Dies geschieht durch die oben beschriebene @Formel @PostedCommand ([ViewRefreshFields]); Speichern darf der Java-Agent das Dokument auf keinen Fall, sonst kommt es zu Speicherkonflikten (s. Kap. 2.3.2). Die Standardmethode, um Items eines Dokuments auszulesen, ist getItemValueString. Diese Methode ignoriert, ob ein Item mehrere Werte hat und liefert immer nur den ersten. Um alle Werte zu erhalten, muss getItemValue verwendet werden. Mit den Methoden getItemValueDateTimeArray, getItemValueDouble, getItemValueInteger, getItemValueCustomData und getItemValueCustomDataBytes können Items anderen Datentyps ausgelesen werden. Wird mit getItemValueString auf ein Datum- oder Zahl-Objekt zugegriffen, gibt sie null zurück. Gibt es kein Item mit dem geforderten Namen, gibt sie ebenfalls null zurück. Wird versucht ein RichText Item auszulesen, wird nur der reine Text geliefert ohne Gestaltungsinformationen oder eingebettete Objekte. Der Code der gewünschten HTML-Seite wird hier mit einfachen Standardmitteln der Java-IO- und Java-Net-Klassen geladen. Das Ergebnis, also der HTML-Code als String, wird im Item F_Code abgelegt. Der Datentyp des Items bei Verwendung von replaceItemValue ist abhängig vom übergebenen Objekt.
38
2 Notes @ Domino Die aktuelle Zeit wird als Zeit-Objekt dem Item F_Ladezeit übergeben. Dieses Feld kann dann in der Ansicht angezeigt werden. Das Dokument wird gespeichert. Die Parameter (true, false) erreichen, dass einerseits ein Speichern erzwungen wird, auch wenn zwischenzeitlich eine Änderung durch wen auch immer erfolgte – dies sollte eigentlich während der Agent läuft nicht der Fall sein –, und durch den zweiten Parameter false wird erreicht, dass in einem Konfliktfall kein Speicherkonfliktdokument gespeichert wird. Das Exception Handling darf natürlich nicht fehlen. Bei einer IOException wird versucht, den Fehlercode noch ins Item F_Code zu schreiben, damit es für den Benutzer sichtbar wird. Unangenehm ist die Tatsache, dass ein extra try catch für die NotesException benötigt wird, die in diesem Fall noch geworfen werden könnte. Dies könnte z.B. in einer eigenen Exception-Handling-Methode abgewickelt werden, um eventuelle Exceptions schon dort abzufangen.
2.1.5
Die Anwendung testen
Um die neue Anwendung zu testen, schließen Sie nun den Designer und öffnen den Notes Client. Wählen Sie im Menü „Datei“ -> „Datenbank“ -> „Öffnen“. Öffnen Sie auf dem Server „lokal“ am unteren Ende der Liste das gelbe Ordnersymbol „djbuch“ durch Doppelklick. Öffnen Sie nun die Datenbank mit Titel „K2 URL Loader“ durch Doppelklick. Die Datenbank öffnet sich im Notes Client und zeigt die Ansicht URL Loader (Abb. 2-9). Der Eintrag für diese Ansicht findet sich im linken Rahmen des Notes Clients. Beachten Sie, dass der Eintrag unterhalb des Wortes „Kapitel 2“ eingerückt ist. Der Klarname der Ansicht war als „Kapitel 2\URL Loader“ angelegt worden. Der Backslash „\“ bewirkt – und dies gilt für viele Objekte in Notes –, dass der Eintrag kategorisiert dargestellt wird. Würde jetzt eine zweite Ansicht mit Klarnamen „Kapitel 2\Andere Ansicht“ angelegt, würde auch diese sich mit dem Eintrag „Andere Ansicht“ unterhalb der Kategorie „Kapitel 2“ befinden. Links der Kategorie „Kapitel 2“ befindet sich ein kleines grünes Dreieck, mit dem die Kategorie auf- und zugeklappt werden kann. Dieses Dreieck wird auch als „Twisty“ bezeichnet. Wenn Sie bereits Dokumente angelegt haben, erscheinen diese im rechten Rahmen der Anwendung. Wenn Sie neue Dokumente anlegen möchten, wählen Sie im Menü „Create“ -> „Kapitel 2“ -> „1. URL Loader Dokument“. In der nun erscheinenden Maske können Sie die Felder Titel, URL und Beschreibung ausfüllen. Beachten Sie, dass die URL gültig ist. Im Feld Beschreibung können Sie übrigens den Text frei gestalten. Klicken Sie mit der rechten Maustaste in das Feld, um Schrifteigenschaften zu verändern. Der Button „URL Laden“ startet den Download der HTML-Seite, speichert den Code im Item F_Code und aktualisiert die Bildschirmdarstellung der Maske, wodurch der Code in der Maske sichtbar wird. Nach einer kurzen Ladezeit erscheint der HTML-Code im unteren Feld. Um die Fehlerverarbeitung kontrollieren zu können, geben Sie z.B. eine URL mit HTTPS (SSL) ein. Im unteren Feld sollte dann der Text
2.1.6 Erweiterungen und Ausblick
39
Abb. 2-9 URL-Loader-Anwendung
„Ein Fehler ist beim Laden aufgetreten. java.net.SocketException: SSL implementation not available“ erscheinen, da SSL von der Beispielanwendung nicht unterstützt wird. Der Zusammenhang zwischen Items und Feldern lässt sich leicht durch die Dokumenten-Eigenschaften sichtbar machen. Klicken Sie mit der rechten Maustaste in eine beliebige Stelle der Maske und wählen Sie zunächst Feld- oder Text-Eigenschafen. Im Eigenschaften-Fenster können Sie dann im oberen Aufklappmenü „Document“ auswählen. Im zweiten Tabellenreiter (Dreieck-Symbol) findet sich eine Liste aller Items des Dokuments (s. Abb. 2-9) Gut erkennbar ist, dass das Dokument mehr Items besitzt, als die Maske in Feldern anzeigt. So wird z.B. im Item $UpdatedBy eine Liste der letzten Autoren vom System gepflegt.
2.1.6
Erweiterungen und Ausblick
Der neu erstellte Agent hat „ganz nebenbei“ noch eine weitere Funktion erhalten. Mit dieser Form von Agenten, wie er in Kapitel 2.1.4 angelegt wurde, können einzelne Dokumente in Ansichten manipuliert werden.
40
2 Notes @ Domino
Wenn Sie in der Ansicht „URL Loader“ ein einzelnes Dokument markieren, indem Sie einfach auf das Dokument klicken (es wird dann dick umrandet), können Sie im Menü „Actions“ den Eintrag „Kapitel 2“ -> „1. URL Laden“ wählen. Für einen kurzen Moment wird durch den Mauszeiger angezeigt, dass der Agent arbeitet. In der Ansicht können Sie dann beobachten, wie der Eintrag für die Ladezeit aktualisiert wird. Hat sich die Web-Seite verändert, ist auch der Code entsprechend neu im Item F_Code abgelegt. Die fertige Anwendung können Sie auch in der Datenbank djbuch\djbuch.nsf finden und die einzelnen Designelemente und Programmierungen nachvollziehen. Um aus dieser Anwendung eine vollwertige Domino-Anwendung zu machen, fehlen noch einige Punkte, die berücksichtigt werden könnten, im Wesentlichen sind dies: •
•
•
•
• • •
•
Die Sicherheit der Anwendung wurde bisher noch nicht berücksichtigt. Empfehlenswert ist, die Planung von Rollen für die Berechtigung für die Durchführung der einzelnen Aktionen festzulegen. Außerdem sind Leser- und Autorenfelder eine Möglichkeit, die erstellten Dokumente zu schützen. Lesen Sie mehr zur Sicherheit von Domino-Anwendungen in den Kapiteln 2.6, 4.16, 7.1.3, 7.5 und 9.5. Der Refresh der Maske nach Ablauf des Agenten und die Unterscheidung, ob der Agent manuell aus der Maske oder aus dem Menü „Aktionen“ gestartet wurde, lässt sich eleganter lösen. Hierfür wird jedoch LotusScript benötigt. Die Gestaltung der Anwendung kann noch verbessert werden. Zum Beispiel ermöglicht der Einsatz von Rahmengruppen, Outline, Seiten und Grafiken, die bereits sichtbaren Elemente „links -> Navigation“ und „rechts -> Anzeige einer Ansicht“ zu gestalten [Programming Bible]. Die Seiten werden jeweils nur auf Anforderung des Benutzers aktualisiert. Ein periodischer Agent kann die Aufgabe übernehmen, regelmäßig für alle URL-Loader-Dokumente die Daten abzuholen. So lässt sich einfach ein XML Feed realisieren, der per HTTP XML Daten abholt und z.B. nach einer Verarbeitung über ein XSL Stylesheet in Dokumenten speichert. Die Überprüfung der Eingaben kann verbessert werden. Die Verarbeitung von SSL-Seiten kann ermöglicht werden. Eine Interpretation des Mime-Types der geladenen Seite könnte zum Beispiel ermöglichen, HTML-Seiten von anderen Inhalten zu unterscheiden, und dann z.B. Bilder als Anhänge zu speichern. Ein Logging und Auditing sollte die Anwendung überwachen, um den dauerhaften und stabilen Betrieb zu gewährleisten. Fehlermeldungen und Exceptions sollten in einem zentralen Log gespeichert werden.
2.2 Das Domino-Speichermodell
2.2
41
Das Domino-Speichermodell
Die Domino-Datenbank wird als NSF (Notes Storage Facility) gespeichert. Wie bereits gezeigt wurde, enthält sie alle Bestandteile einer Domino-Anwendung. In der NSF-Datei sind alle Objekte vom WYSIWYG-Gestaltungselement zur Anzeige von User-Interfaces bis hin zum einzelnen Datensatz gespeichert. Die einzelnen Objekte werden in einer Domino-Datenbank als so genannte Notes gespeichert. Der elementare Datensatz in einer Domino-Datenbank wird als Dokument bezeichnet. Ein Dokument enthält Items, in denen ein oder mehrere Werte gespeichert sind. Die NSF-Datei ist also ein Container von Containern von Containern. In einer Domino-Datenbank gibt es für die Speicherung von Daten keine zwingend vorgeschriebene Struktur. Mit Masken wird lediglich ein äußerer Rahmen für die User-Interaktion geschaffen. Im Extremfall könnte jedes Dokument andere Items mit anderen Inhalten und anderen Namen als jedes andere Dokument haben (der Regelfall ist dies natürlich nicht). Das Verständnis dieser Struktur ist wichtig, um Java-Programme für Domino zu erstellen. Insbesondere wird der Java-Programmierer wenig, wenn überhaupt, mit der Erstellung von Masken zu tun haben, sondern eher im Backend Programme zur Manipulation von Domino-Daten erstellen. Daher werden die so erzeugten Daten nicht durch eine Maske gefiltert oder in bestimmten Strukturen gehalten und über das Item „Form“ in lose (gedachten) Gruppen organisiert. Planung und Vorbereitung von Java-Agenten ist also auch für Java-Anwendungen in Domino sehr wichtig.
2.2.1
Die Domino-„Note“
Alle Objekte einer Domino-Anwendung werden in so genannten Notes gespeichert. Eine Note besteht aus zwei Teilen, einem Header und einem Body. Im Header werden allgemeine Informationen über die Note abgespeichert, z.B. welcher so genannten Klasse die Note angehört. Im Body einer Note werden die eigentlichen Daten einer
Tabelle 2-1 Die Note-Typen
1 2 3 4 5
NOTE – Ein allgemeiner Container beliebigen Inhalts DOCUMENT – Ein Daten-Objekt PROFILEDOCUMENT – Ein spezielles im Hauptspeicher gecachtes (und dadurch für zeitkritische Operationen verwendetes) DOCUMENT ACL – Enthält die Zugriffskontrollliste einer Datenbank DESIGN-ELEMENT – Eines der folgenden Designelemente: form, subform, page, frameset, view, folder, sharedfield, sharedactions, imageresource, appletresource, agent, agentdata, scriptlibrary, databasescript, helpusingdocument, helpaboutdocument, dataconnection
42
2 Notes @ Domino
Tabelle 2-2 Die Note-Klassen
1 2
3
4 5 6 7 8
9
10
DOCUMENT – Enthält Dokumente FILTER – Enthält Agenten, Datenbank-Skripte (Spezieller Code, der bei bestimmten Datenbank-Events, z.B. Drag-And-Drop oder Dokumente Löschen ausgelöst wird und somit eine Customization erlaubt), Outlines (Navigationselemente) und Script-Bibliotheken (s. Kap. 3.5). FORM – Enthält Masken, Rahmengruppen (ähnlich den Framesets im HTML), Seiten (Designelement zur Darstellung statischer Informationen) und Teilmasken (vergleichbar den Masken, können jedoch als Container in andere Masken eingefügt werden). INFO – Enthält das spezielle Datenbankdokument „Über diese Datenbank“, das nur einmal in einer Datenbank vorkommen kann. HELP_INDEX – Eine datenbankinterne Note, zur Speicherung eines Hilfeindexes, der aber nur für die Notes-eigene Client-, Designer- und Administratoren-Hilfe verwendet wird. Existiert nur einmal pro Datenbank. HELP – Enthält das spezielle Datenbankdokument „Benutzen dieser Datenbank“, das nur einmal in einer Datenbank vorkommen kann. ICON – Das Datenbank-Icon. REPLFORMULA – Die so genannten Replikationsformeln werden in dieser Note gespeichert. Sie dienen dazu, die zwischen zwei Instanzen (Repliken) einer Datenbank zu replizierenden Dokumente anhand von @Formeln genau zu spezifizieren. FIELD – Enthält so genannte gemeinsame Felder. Dies sind Felder, die zentral definiert werden, d.h. für die Datentyp, Default-Wert, Validierung, etc. einmal festgelegt werden. Gemeinsame Felder werden in Masken als Felder eingesetzt, dennoch nicht zu verwechseln mit den Feldern einer Maske, die individuell pro Maske definiert werden und nur als Item einer FORM Note vorliegen. VIEW – Enthält Ansichten und Ordner (vergleichbar mit Verzeichnissen von Verweisen auf Dokumente).
Note abgelegt, wobei eine Note verschiedene Klassen annehmen kann, die die verschiedenen Anwendungsbereiche definieren. So wird im Wesentlichen nach Gestaltungs-, administrativen und Dokumenten- Notes unterschieden. Eine Note wird näher bestimmt über den Type einer Note. Der Type einer Note bestimmt sich aus der Klasse und einem (vorhandenen oder nicht vorhandenen) Item in der Note. Die möglichen Typen und Klassen der Notes finden Sie in Tabelle 2-1 und Tabelle 2-2. Eine sehr gute Möglichkeit, die Struktur einer Notes Storage Facility und insbesondere die Note-Struktur zu verstehen, ist die Auseinandersetzung mit der Definition des Domino-XML (DXL) und insbesondere mit der DTD des DXL. Nähere Informationen hierzu finden Sie in Kapitel 17.7 oder im Web bei www.lotus.com [Lotus, DTD] und [Lotus, DesignerR6]. Auch die Designer-Hilfe ist eine sehr gute Quelle für Informationen zur DTD.
2.2.2 Notes Document und Notes Form 2.2.2
43
Notes Document und Notes Form
Die wichtigste Note für den Java-Programmierer ist unweigerlich das Dokument, das durch die Klasse Document repräsentiert wird. Sämtliche Daten, die gespeichert, geladen, verändert oder gelöscht werden sollen, befinden sich in Dokumenten und müssen als Document geladen werden. Einen etwas nachrangigen Charakter hat die ACL Note, die durch die Klasse ACL repräsentiert wird und die PROFILEDOCUMENT Note, die ebenfalls durch die Klasse Document repräsentiert wird, wobei jedoch zu beachten ist, dass Profildokumente nur über die Klasse Database per getProfileDocument geladen werden können (s. Kap. 7.2). Da der Java-Programmierer mit real existierenden :-) Domino-Datenbanken arbeiten wird, ist der FORM Note neben dem Document die Aufmerksamkeit in diesem Kapitel sicher. Die übrigen Notes sind wichtig für das Verständnis der Datenstruktur. Die NotesMaske ist das Designelement in Notes, um Dokumente anzuzeigen und dem Benutzer die Möglichkeit zu geben, Dokumente zu ändern, also ein gestaltetes, interaktives Element des grafischen User Interfaces. Ein Designelement in Notes ist ein Container, in dem, neben Layout-Informationen für die Darstellung im GUI, ProgrammierCode und interaktive Schaltflächen gespeichert sind. Zu Designelementen zählen alle Note-Objekte, die Code oder Gestaltungsinformationen aufnehmen. Mögliche Designelemente werden in Kapitel 3 vorgestellt, eine Übersicht finden Sie in Tabelle 2-1, Punkt 5. Die wichtigsten Designelemente sind Maske, Seite, Frameset, Ansicht, BildRessource, Agent und Script-Bibliothek. Prinzipiell besteht die Möglichkeit, jedes Dokument mit einer beliebigen Maske und somit mit variierenden Darstellungen anzuzeigen, über das Item „Form“ ist jedoch ein Default-Zusammenhang zwischen Dokument und Maske hergestellt. Domino evaluiert die Default-Maske eines Dokuments in der folgenden Reihenfolge: Zunächst wird überprüft, ob es ein Item mit dem Namen Form gibt, falls nicht, wird überprüft, ob es ein Item mit dem Namen $Title gibt. In beiden Fällen wird die Maske mit dem Namen verwendet, der als String in diesen Items gespeichert ist. Falls es diese Items nicht gibt, wird überprüft, ob die Datenbank eine so genannte DefaultMaske hat, und das Dokument wird mit dieser Maske angezeigt. Ist auch dies nicht der Fall, wird eine entsprechende Fehlermeldung ausgegeben. Masken und Dokumente sind zwar über den Pointer im Item Form miteinander verknüpft, es ist aber wichtig für das Verständnis, im Auge zu behalten, dass beide unabhängig voneinander sind. Wird z.B. ein Feld aus einer Maske entfernt, werden keinesfalls alle korrespondierenden Items aus Dokumenten entfernt, die mit dieser Maske erstellt wurden oder die über das Item Form diese Maske referenzieren. Beim Öffnen eines Dokuments mit einer derart veränderten Maske wird lediglich das Item nicht angezeigt, wobei es weiterhin im Dokument verbleibt. Die Maske ist also tatsächlich ein reines Gestaltungselement (mit sicherlich vielen programmatischen Fähigkeiten). Beinhaltet im umgekehrten Fall eine Maske ein
44
2 Notes @ Domino
Feld, das nicht als Item im anzuzeigenden Dokument vorliegt, wird dieses Feld schlicht als nicht initialisiert betrachtet, wie dies für alle Felder in einem neuen, noch nicht gespeicherten Dokument der Fall ist. Wie bereits in der Beispielanwendung in Kapitel 2.1.2 deutlich wurde, dienen Masken in Domino dem Anwender als interaktive Schnittstelle zwischen Benutzer und Datenbank. • • •
Über Masken werden Daten vom Benutzer eingegeben, gegebenenfalls manipuliert und in Backend-Dokumenten gespeichert. Masken können Backenddaten anzeigen, wobei diese Daten zuvor möglicherweise durch @Formeln (oder LotusScript) in der Maske manipuliert wurden. Masken dienen der Gestaltung der grafischen Benutzer-Oberflächen.
Dieses Verhalten von Masken muss bei der Java-Programmierung berücksichtigt werden, wenn Dokumente im Backend manipuliert oder erzeugt werden, damit diese Daten von den Masken (und auch von Ansichten, wie noch zu zeigen sein wird) korrekt verarbeitet werden. Bei der Verarbeitung von Dokumenten, die später mit Hilfe von Masken angezeigt werden sollen, ist zu beachten: • • • • •
•
Das Item „Form“ muss den Namen der Maske enthalten, mit dem das Dokument dargestellt werden soll. Jedes Dokument sollte das Item „Form“ enthalten. Das Dokument sollte alle Items enthalten, die in der Maske in Feldern angezeigt werden sollen. Das Dokument muss alle Items enthalten, die in der Maske für Berechnungen benötigt werden. Alle Items sollten den gleichen Datentyp haben wie die Felder, mit denen sie angezeigt werden sollen. Stimmen die Datentypen nicht überein, wird der Notes Client versuchen, soweit möglich, eine Datenkonvertierung durchzuführen. Wurde im Document durch Benutzung von replaceItemValue (String, Vector) ein Item mit mehreren Werten angelegt, und hat das korrespondierende Feld nicht die Eigenschaft „mehrere Werte zulassen“, dann wird nur der erste Wert im Feld angezeigt und beim Speichern auch nur gespeichert. Weitere Werte gehen verloren.
Gleichzeitig muss natürlich damit gerechnet werden, dass die über Masken zur Verfügung gestellten Daten den Regeln der Masken unterliegen: •
•
Das durch die Maske generierte Item „Form“ enthält den Namen oder sofern vorhanden den Alias einer Maske, sofern das Feld Form nicht explizit durch Berechnung in der Maske verändert worden ist. Die Items erhalten den Datentyp, der durch die jeweiligen Felder der Maske vorgegeben ist. Durch automatische Typ-Konvertierung kann sich der Datentyp eines Items geändert haben, falls das Item vor dem Öffnen und Speichern mit der Maske einen anderen Datentyp hatte.
2.2.2 Notes Document und Notes Form • • • • •
• •
45
Durch Berechnungen oder Benutzereingaben in der Maske können Items verändert worden sein. Durch das Speichern mit einer Maske können Items gelöscht worden sein (durch die @Formel @deleteField) Neue Items können durch die Maske hinzugefügt worden sein. Einzelne Items können durch die Maske verschlüsselt worden sein. Die Berechtigungen für das Dokument können sich durch Verändern oder Hinzufügen von Leser- oder Autorenfeldern geändert haben. Hat der Ausführende eines Agenten keine entsprechenden Rechte für das Dokument, wird es nicht in Ansichten angezeigt und bleibt somit möglicherweise unsichtbar für einen Agenten. Durch Masken neu erstellte Dokumente können durch die Maske Default Rechte in Form von speziellen Autoren- oder Leserfeldern erhalten haben. Hat ein Feld die Eigenschaft „mehrere Werte zulassen“, so kann ein Benutzer eine Liste von Werten in dieses Feld eingeben. Diese Felder dürfen nicht mit getItemValueString, getItemValueInteger etc. ausgelesen werden, sondern müssen mit getItemValue ausgelesen werden.
Eine Maske kann neben Text und Feldern etliche andere Gestaltungselemente, wie Tabellen, berechnete Texte zur Darstellung von Berechnungsergebnissen als Fließtext, eingefügte Bilder oder referenzierte Bilder aus den Ressourcen, eingebettete Ansichten, die dann inline als Teilbereich der Maske angezeigt werden, RichText und Teilmasken, enthalten. Teilmasken sind Masken, die den normalen Masken sehr ähnlich sind, jedoch nicht als allein stehendes Designelement verwendet werden können, sondern lediglich dazu dienen in eine Maske eingefügt zu werden. Hierdurch lassen sich Masken modularisieren und Teile von Masken wieder verwenden. Eine Notes-Maske kann durch das Java-Objekt Form repräsentiert werden. Hierdurch können Eigenschaften einer Maske programmatisch ausgelesen und zum Teil auch verändert werden. In Listing 2-2 wird gezeigt, wie die Form-Objekte einer Datenbank ausgelesen und angezeigt werden können. Ziel des Agenten ist ein neues Dokument anzulegen, in dem in einer übersichtlichen Form allgemeine Informationen über alle Masken der Datenbank und deren Felder angezeigt wird. Der Agent soll über das Menü „Actions“ gestartet werden. Die erstellten Übersicht-Dokumente sollen mit der Maske FO_Report angezeigt werden. Um den Agenten zu erstellen und einsetzen zu können, müssen Sie zunächst eine Maske erstellen, die den Alias „FO_Report“ hat. Den Klarnamen können Sie frei wählen. Da die Dokumente, die mit dieser Maske erstellt werden sollen, nur über den Agenten erzeugt werden und nicht durch eine interaktive Eingabe durch den Benutzer, sollten Sie in den Maskeneigenschaften das Häkchen „Display“ -> „Include in Menu“ deaktivieren, damit die Maske nicht im Menü „Create“ erscheint. Fügen Sie der neuen Maske das Feld F_Titel (Textfeld) und Body (RichText) hinzu.
46
2 Notes @ Domino
Erstellen Sie einen neuen Agenten mit Klarnamen Tools\1. Masken anzeigen und mit Aliasnamen AG_ShowForms. Dieser Agent kann später über das Menü „Actions“ -> „Tools“ -> „1. Masken anzeigen“ gestartet werden. Übernehmen Sie das Listing 2-2 als Code in den Agenten. Erstellen Sie eine Ansicht mit Klarnamen „Alle Dokumente“ und Alias „V_all“. Geben Sie dieser Ansicht die Select-Formel „SELECT @All“ und fügen Sie der Ansicht die Spalte „Titel“ hinzu, die das Item „F_Titel“ anzeigt. Beachten Sie, dass Ansichten in Domino die Daten selbst nicht beeinflussen, sondern lediglich in gestalteter Form anzeigen. Diese Anzeige wird alle Dokumente der Datenbank anzeigen und von diesen Dokumenten, sofern vorhanden, das Feld F_Titel. Experimentieren Sie mit der Ansicht, indem Sie in einer zusätzlichen Spalte das Erstelldatum und die verwendete Maske anzeigen lassen. Wenn Sie Ihre Datenbank im Notes Client öffnen, sehen Sie im linken Rahmen die neue Ansicht „Alle Dokumente“. Klicken Sie auf diesen Eintrag. Starten Sie nun den Agenten über das Menü „Aktionen“. Nach Beendigung des Agenten erscheint ein neues Dokument in dieser Ansicht mit dem Titel „Alle Masken dieser Datenbank“. Dieses Dokument enthält nun Informationen über die FORM Notes der Datenbank. Listing 2-2 zeigt den Sourcecode des Agenten: Die Session und die Datenbank werden wie gewohnt über getSession und getAgentContext().getCurrentDatabase() geladen. Ein neues Dokument wird mit database.createDocument erstellt. Das Item Form wird mit dem String „FO_Report“ belegt. Hierdurch wird festgelegt, dass das erzeugte Dokument mit der Maske FO_Report geöffnet werden soll. document.createRichTextItem("Body") erzeugt ein RichText Item mit Namen „Body“, in dem die Übersicht später gespeichert werden soll. Beachten Sie, dass zwar die zuvor erstellte Maske das Feld Body zur Verfügung stellt, aber das Dokument ohne Verwendung der Maske erstellt wird. Daher müssen alle Items manuell über den Java-Code erstellt werden. Bei der Erstellung muss der Datentyp berücksichtigt werden, mit dem die Maske später das Dokument anzeigen soll. Um die Masken aus der Datenbank zu laden, wird die Methode database.getForms verwendet, die einen Vector von Form-Objekten liefert. Ein RichText Item kann nicht direkt mit Inhalt gefüllt werden, wie z.B. ein Text Item, sondern wird mit Teilobjekten, wie z.B. Text, Style-Informationen oder eingebetteten Anhängen, wie Bildern, gefüllt. appendText fügt einen Text zum RichText Item hinzu. Das Form-Objekt hat einige getter Methoden, um allgemeine Eigenschaften auszulesen. Neben Namen und Aliasnamen wird in dem Agenten mit isSubform ausgelesen, ob es sich um eine Teilmaske (Subform) handelt. Hier zeigt sich das Konzept der Klassen und Typen von Note-Objekten. Ist eine Note aus der Klasse FORM (Tabelle 2-2), so entscheidet sich über das Vorhandensein eines speziellen internen Items, ob die Note vom Typ SUBFORM ist (Tabelle 2-1, Punkt 5).
2.2.2 Notes Document und Notes Form
47
import lotus.domino.*; import java.util.Vector; import java.io.*; public class ShowFormJavaAgent extends AgentBase { public void NotesMain() { try { Session s = getSession(); Database database = s.getAgentContext().getCurrentDatabase(); Document document = database.createDocument(); document.replaceItemValue("Form", "FO_Report"); document.replaceItemValue("F_Titel", "Masken der Datenbank"); RichTextItem rt = document.createRichTextItem("Body"); Vector forms = database.getForms(); rt.appendText("Masken in \"" + database.getTitle() + "\":"); for (int i = 0; i < forms.size(); i++) { Form form = (Form) forms.elementAt(i); rt.appendText("\n\nMaskenname : " + form.getName()); Vector alias = form.getAliases(); rt.appendText("\n\tAlias "); for (int j = 0; j < alias.size(); j++) { rt.appendText("\n\t \" \"\t: " + alias.elementAt(j)); } rt.appendText("\n\tGeschützte LeserFelder\t: " + form.isProtectReaders()); rt.appendText("\n\tGesch. AutorF.: "+form.isProtectUsers()); rt.appendText("\n\tIst Teilmaske\t: " + form.isSubForm()); Vector users = form.getFormUsers(); rt.appendText("\n\tBenutzer, die Dokumente mit d. Maske "+ "\n\terstellen dürfen:"); for (int j = 0; j < users.size(); j++) { rt.appendText("\n\t\t: " + users.elementAt(j)); } Vector readers = form.getReaders(); if (readers.size() != 0) { rt.appendText("\n\tBenutzer, die Dokumente lesen dürfen, die mit dieser Maske \n\terstellt wurden:"); for (int j = 0; j < readers.size(); j++) { rt.appendText("\n\t\t: " + readers.elementAt(j)); } } Vector fields = form.getFields(); rt.appendText("\n\tFelder"); for (int j = 0; j < fields.size(); j++) { String fName = (String) fields.elementAt(j); int typ = form.getFieldType(fName); String typS = ""; switch (typ) { case Item.AUTHORS : typS = "Autor"; break; case Item.DATETIMES : typS = "Datum-Uhrzeit"; break; case Item.NAMES : typS = "Namen"; break; case Item.NUMBERS : typS = "Zahl"; break; case Item.RICHTEXT : typS = "Richtext"; break; case Item.READERS : typS = "Leser"; break; case Item.TEXT : typS = "Text oder Text - Liste"; break; default : typS = "anderer Typ";} rt.appendText("\n: " + fName + " " + typS + " - " + typ); } } document.save(true, false); } catch (Exception e) { e.printStackTrace(); } } }
Listing 2-2 Form-Objekte und -Eigenschaften anzeigen
48
2 Notes @ Domino
Beachten Sie: Default-Rechte, die über eine Maske vergeben werden können, werden nur dann in einem Dokument gespeichert, wenn es a) neu angelegt wird und b) interaktiv über eine Maske angelegt wird. Das Setzen der Default-Rechte ist eine Leistung des Notes Clients und muss bei der Java-Programmierung im Backend manuell durchgeführt werden.
Werden Dokumente interaktiv durch die Verwendung einer Maske erstellt, so können über die Maskeneigenschaften Default-Berechtigungen vergeben werden. Diese können über getFormUsers und getReaders in Form ausgelesen werden. Beachten Sie, dass diese Default-Rechte nur dann vergeben werden, wenn ein Dokument mit einer Maske NEU erstellt wird. Die Namen aller in einer Maske verwendeten Felder können über getFields als Vector geladen werden. Felder legen fest, welchen Datentyp das korrespondierende Item erhalten soll. Diese Vorgabe kann über getFieldType als Integer ausgelesen werden. Zur Vereinfachung gibt es im Paket lotus.domino.* Konstanten, mit denen die Typen beschrieben werden können. Das neu erstellte Dokument muss nachdem die gewünschten Informationen in dem Dokument abgelegt wurden, durch den Aufruf von save gespeichert werden. Der Agent AG_ShowForms erstellt eine Übersicht über die in einer Datenbank befindlichen Masken und deren Felder. Eine Sonderstellung nimmt das Profildokument ein, also ein Dokument mit dem Typ PROFILEDOCUMENT. Ein Profildokument wird mit der Methode public Document getProfileDocument(String profile, String profilekey) throws NotesException erzeugt oder geöffnet. Der Parameter profile ist der
Name der Maske, mit der das Profildokument erstellt wurde bzw. angezeigt werden soll, also vergleichbar mit dem Feld „Form“. Der Parameter profilekey ist ein Schlüssel, unter dem das Profil gespeichert werden soll, so können z.B. benutzerspezifische Profile angelegt werden. Der Parameter profilekey darf null sein. Existiert ein Profildokument mit den gegebenen Parametern beim Aufruf von getProfileDocument noch nicht, wird es automatisch angelegt und gespeichert (!), d.h. hier ist Vorsicht geboten, falls die Erstellung von Profildokumenten unter bestimmten Bedingungen unerwünscht ist.
2.2.3 Item und Feld
49
Profile unterliegen einem speziellen Caching-Mechanismus und werden im Hauptspeicher des Servers zwischengespeichert. Dadurch sind sie sehr viel performanter als normale Dokumente, haben aber auch einige Nachteile, die in Kapitel 7.2 diskutiert werden. Wichtig für das Verständnis des Zusammenhangs zwischen Masken und Dokumenten ist die Kenntnis der Eigenschaften und Unterschiede von Feldern und Items.
2.2.3
Item und Feld
Das Speicherkonzept von Domino sieht die Organisation der Daten in losen Schemata innerhalb von Dokumenten vor. Die einzelnen Teilinformationen werden in so genannten Items im Dokument gespeichert. Im Gegensatz zu einer SQL-Datenbank ist ein Dokument (vgl. Datensatz im SQL) nicht an ein festes Schema (vgl. Tabelle im SQL) gebunden. Jedes Dokument kann seine eigenen Items unabhängig von anderen Dokumenten enthalten. Die Organisation der Daten und die Zuordnung der Daten (Dokumente) zueinander wird in Domino über Ansichten (vgl. Views im SQL) erreicht. Items sind die Container, in denen die Daten innerhalb eines Dokuments gespeichert werden. Sie können von verschiedenen Typen sein. Felder sind diejenigen Elemente einer Maske, die in der Ausprägung verschiedener Eingabehilfen dem Benutzer die Eingabe von Daten ermöglichen, aber auch zur Anzeige berechneter Informationen dienen. Items, die sich in einem Dokument befinden, werden nicht automatisch durch Felder in einer Maske sichtbar. Sie sind aber dennoch im Dokument vorhanden und können programmatisch verarbeitet werden. Um Items in einer Maske anzuzeigen, muss der Maske ein Feld mit gleichem Namen (oder einer Berechnung, die z.B. den Wert eines Items anzeigt) oder mit einem berechneten Text, der den Wert des Items lädt und anzeigt, hinzugefügt werden. Auch Felder haben verschiedene Ausprägungen mit unterschiedlichen Typen. Die Datentypen der Items und der Felder können als lose miteinander verknüpft betrachtet werden. Wie bereits im vorigen Kapitel aufgezeigt, werden Daten, deren Typ zwischen Feld und Item abweicht, sofern möglich, konvertiert. Unstimmigkeiten zwischen Feld- und Itemtypen können zu Fehlern führen und müssen stets bei der Programmierung im Auge behalten werden. In Tabelle 2-3 sind wichtige Datentypen von Items und Feldern gelistet. Zusätzlich wird aufgezeigt, durch welche Arten von Items und Feldern diese Datentypen erzeugt werden können. Beispiel A: In eine Maske wird ein Feld mit Typ Text eingefügt. Zusätzlich wird die Feld-Eigenschaft „Mehrere Werte zulassen“ angekreuzt. Füllt ein Benutzer ein solches Feld aus, werden alle eingegebenen Werte, die z.B. durch ein Semikolon (welches Trennzeichen verwendet wird, hängt wiederum von Feldeigenschaften ab) vom Benutzer getrennt wurden, im gleichnamigen Item als Textliste gespeichert.
50
2 Notes @ Domino
Tabelle 2-3 Wichtige Datentypen und Arten von Items und Feldern Datentyp des Items
(wird nicht als Designelement in Masken verwendet)
NOTEREFS
NoteLink_List
DocLink Reference list (Verwendung im Item $Links)
(wird nicht als Designelement in Masken verwendet)
NOTELINKS
Number Range
Zahl, Zahlliste
Zahl, Zahlliste
Zahl
NUMBERS
Time Range
Datum/Uhrzeit, Datum/UhrzeitListe
Datum/Uhrzeit, Datum/UhrzeitListe
Datum-, Zeitwähler oder Datum/Zeit-Eingabefeld
DATETIMES
Composite
RichText, RichText light, Embedded Object
RichText, RichText light
RichText, RichText light
RICHTEXT, EMBEDDEDOBJECT, ATTACHMENT und andere
MimePart
MimePart
(wird nicht als Designelement in Masken verwendet)
MIME_PART
(*) kann nicht als Textliste auftreten
(**) Item.getType(): Name der Konstanten in Item
2.2.3 Item und Feld
51
Beispiel B: In eine Maske wird ein Feld mit Typ Radiobutton eingefügt. Wählt der Benutzer einen Button aus, wird der dahinter (über Feldeigenschaften) verborgene TextWert im gleichnamigen Item als Text gespeichert. Andere Werte als Text können nicht über einen Radiobutton übergeben werden. Beispiel C: In eine Maske wird ein Feld mit Typ Autoren eingefügt. Zusätzlich wird die FeldEigenschaft „Mehrere Werte zulassen“ angekreuzt. Füllt ein Benutzer ein solches Feld aus, werden alle eingegebenen Werte, die z.B. durch ein Semikolon (welches Trennzeichen verwendet wird, hängt wiederum von Feldeigenschaften ab) vom Benutzer getrennt wurden, im gleichnamigen Item als Textliste gespeichert. Da das Feld ein Autorenfeld war, wird im Item ein spezielles Flag gesetzt. Über Java kann dieses Flag mit setAuthors und isAuthors gelesen und gesetzt werden. RichText (und RichText light) ist ein etwas komplexerer Feld- und Itemtyp. RichTextItems sind Container, die wiederum Objekte enthalten können. Das wichtigste Objekt innerhalb eines RichText-Items ist der Paragraph. RichText-Items können eingebettete Dateien, Grafiken etc. aufnehmen. Diese werden nicht im RichText-Item selbst gespeichert, sondern in einem separaten Item mit Datentyp Composite als embedded Object gespeichert und über einen Zeiger verlinkt. Embedded Objects, die über eine Maske in ein RichText-Feld eingegeben werden, werden im Item $File gespeichert. In Kapitel 2.2.2 und Listing 2-2 wurde aufgezeigt, wie mit einfachen Mitteln Informationen über Masken angezeigt werden können. Ebenso lassen sich die Items, die in einem Dokument gespeichert sind, extrahieren und anzeigen (Listing 2-3). Der Agent befindet sich in der Buch-Datenbank djbuch.nsf und heißt AG_ShowItems. Er kann über das Menü „Actions“ -> „Tools“ -> „2. Items anzeigen“ gestartet werden. Die Aufgabe dieses Agenten ist es, über ein vorgegebenes, durch den Benutzer in der Ansicht „Kapitel 2\Dokumente“ markiertes Dokument Informationen über die Items dieses Dokuments einzusammeln und in einem so genannten Antwortdokument abzuspeichern. Antwortdokumente sind normale Dokumente, die aber über das Item $Ref ein Mutter-Dokument referenzieren. In Ansichten lassen sich solche Dokumente kategorisiert darstellen, so dass deutlich wird, zu welchem Mutter-Dokument (auch Parent- oder Hauptdokument) das Antwortdokument gehört. Ursprünglich waren Antwortdokumente in Diskussionen für Antworten auf Beiträge entwickelt worden (daher der Name Antwortdokument). Antwortdokumente wurden daher in Ansichten in einer für Diskussionsforen typischen, kategorisierten Form angezeigt. Das Konzept der Antwortdokumente ist jedoch in Domino erweitert worden und wird in der Regel allgemein gültig für die Referenzierung zwischen Mutter- und Tochter-Dokumenten verwendet. Dieses Konzept hat viele Vorteile, aber auch einige Nachteile, die in Kapitel 7.3f. diskutiert werden.
52
2 Notes @ Domino
import lotus.domino.*; import java.util.Vector; import java.util.Enumeration; public class ShowItemsJavaAgent extends AgentBase { public void NotesMain() { try { Session session = getSession(); Database db = session.getAgentContext().getCurrentDatabase(); Document doc = session.getAgentContext().getDocumentContext(); if (doc != null) { Document responsedoc = db.createDocument(); responsedoc.replaceItemValue("Form", "FO_Report"); RichTextItem rt = responsedoc.createRichTextItem("Body"); responsedoc.replaceItemValue("F_Titel","Items von Dok. \"" + doc.getItemValueString("F_Titel") + "\""); responsedoc.makeResponse(doc); Enumeration items = doc.getItems().elements(); while (items.hasMoreElements()) { Item item = (Item) items.nextElement(); String type; switch (item.getType()) { case Item.TEXT : type = "Text o. Textliste"; break; case Item.READERS : type = "Leser"; break; case Item.AUTHORS : type = "Autoren"; break; case Item.NAMES : type = "Namen"; break; case Item.NOTEREFS : type = "Referenz auf das übergeordnete Dokument"; break; case Item.NOTELINKS : type = "Link auf Datenbank, eine Ansicht oder ein Dokument"; break; case Item.NUMBERS : type = "Zahl / Zahlliste"; break; case Item.DATETIMES : type = "Datum/Uhrzeit o. Bereich/Liste von Datum/Uhrzeit"; break; case Item.RICHTEXT : type = "RichText"; break; case Item.EMBEDDEDOBJECT : type = "Eingebettetes Object"; break; case Item.ATTACHMENT : type = "Dateianhang"; break; case Item.HTML : type = "HTML-Quelltext"; break; default : type = "Anderer Typ"; } rt.appendText("\n" + item.getName() + "\n" + type); rt.appendText("\n" + item.getValueLength() + " bytes"); rt.appendText("\nLast modified " + item.getLastModified().getLocalTime()); if (item.getText().length() == 0) { rt.appendText("\n[kein darstellbarer Text]\n"); } else { rt.appendText("\n" + item.getText(80) + "\n"); } } responsedoc.save(true, false); } } catch (Exception e) { e.printStackTrace(); } } }
Listing 2-3 Items eines Dokuments anzeigen
2.2.3 Item und Feld
53
Die Informationen, die in diesem Agenten gesammelt werden, sollen in einem neuen Dokument gespeichert werden. Dieses neue Dokument soll eine Referenz zu dem ursprünglichen Dokument erhalten. Die Methode makeResponse wird auf dem neuen Dokument ausgeführt und erhält als Parameter das Mutter-Dokument, das referenziert werden soll. Durch Anwenden dieser Methode wird ein Item $Ref im Antwortdokument angelegt. Dieses Item hat den Datentyp NoteRef_List und enthält die so genannte UniversalID des Dokuments (s. auch Kap. 2.5). getItems liefert einen Vector von Item-Objekten.
Items und Felder: Item werden die einzelnen Datenobjekte innerhalb eines Dokumentes genannt. Ein Dokument kann eine beliebig große (nur durch einige Speicherlimits begrenzte) Anzahl von Items enthalten. Items enthalten Daten unterschiedlichster Datentypen. Felder heißen die korrespondierenden Objekte in einer Notes-Maske. Ein Feld dient dazu, das Item gleichen Namens anzuzeigen oder zu erzeugen. Ein Feld dient der Anzeige von Items, oder Ein Feld dient der Eingabe von Daten. Diese vom Benutzer eingegebenen Daten werden im gleichnamigen Item gespeichert. Ein Feld kann einerseits verschiedene Erscheinungsformen (Textfeld, Listbox, Aufklappmenü, Radiobox, etc.) haben. Andrerseits kann ein Feld verschiedene Datentypen haben. Diese Datentypen werden bei der Eingabe vom Notes Client validiert. Ein Item, das entstand, weil Daten durch ein Feld im Dokument gespeichert wurden, hat den gleichen Datentyp wie das Feld. Ein Feld, das Daten eines Items anderen Typs anzeigen soll, versucht diese Daten zu konvertieren. Felder und Items können mehrere Werte gleichzeitig enthalten. Ein Feld mit mehreren Werten zeigt diese unterschiedlich, je nach Eigenschaft des Feldes an (z.B. durch Semikolon oder durch neue Zeilen getrennt).
54
2 Notes @ Domino Der Typ, der via getType erhalten wird, beschreibt das Item bereits sehr detailliert, so dass nicht extra noch separate Methoden zu Rate gezogen werden müssen, um das Item genauer zu analysieren. So liefern z.B. isAuthor oder isReader Informationen, ob es sich bei einem Item um Autoren- oder Leseritems handelt. Die im Code gelisteten Konstanten beschreiben nicht das ganze Spektrum. Es stehen noch weitere Typen zur Verfügung, die aber aus Sicht der Programmierung nicht relevant sind und durch Domino intern verwendet werden bzw. nur über das C++ API angesprochen werden sollten. Neben Name und Größe besitzt jedes Item Angaben über den Zeitpunkt der letzten Änderung. Da nicht jedes Item Text-Informationen oder durch Text repräsentierbare Informationen besitzt, wird in dem Agenten überprüft, ob getText Ergebnisse liefert. Ein eventueller Datei-Anhang muss aus dem RichText (s. Kap. 11.1ff.) mit speziellen Methoden extrahiert werden.
Grundsätzlich werden Items mit der Methode getFirstItem () gelesen, wobei ein Objekt der Klasse Item zurückgegeben wird. Den Wert oder die Werte, sofern es sich um ein Listen-Item handelt, eines Items können mit der Methode getItemValue (String itemName) gelesen werden. Hierbei wird immer ein Vector mit einem oder mehreren Elementen der zum Datentyp des Items passenden Klasse zurückgegeben. Einzelwerte werden mit den Methoden getItemValueString (String itemName), getItemValueInteger (String itemName) oder getItemValueDouble (String itemName) gelesen, wobei diese Methoden null, 0 oder 0.0 zurückgeben, wenn das entsprechende Item nicht vorhanden ist. Geschrieben werden Items mit der Methode replaceItemValue (Object o), wobei der Datentyp des geschriebenen Items von der Klasse von o abhängt (siehe auch Beispiel aus Listing 2-4). Alternativ kann die Methode appendItemValue verwendet werden, die jedoch ein weiteres Item mit gleichem Namen erzeugt. Die Verwendung dieser Methode ist eher nicht zu empfehlen. Durch die konsequente Einführung von XML für Domino und die Abbildung der Domino-Objekte durch DXL ergibt sich für das Verständnis von Dokumenten, Masken, Items und Feldern die Möglichkeit, diese Objekte auch als XML darzustellen und zu interpretieren. In der Buch-Datenbank djbuch.nsf finden sich zwei Agenten, die dies verdeutlichen. Öffnen Sie die Datenbank djbuch.nsf und markieren Sie ein beliebiges Dokument in der Ansicht „Alle Dokumente“. Starten Sie im Menü „Actions“ -> „Tools“ den Agenten „3. Dokument als XML“. Der Agent4 erzeugt ein Antwortdokument, das unterhalb des zuvor markierten Dokuments erscheint. Öffnen Sie das Dokument, um den erzeugten XML-Code zu sehen. 4
Dieser Agent kann nur auf einer lokalen Replik auf dem Notes Client gestartet werden. Dies liegt an einer Eigenart des verwendeten Domino XML Exporters (DXL). Dieser startet einen eigenen NotesThread. Da das anzuzeigende Dokument bereits als so genanntes Context-Dokument (das markierte Dokument) vorliegt, kann es nicht gleichzeitig vom DXL Exporter verarbeitet werden. Bei einer lokalen Verarbeitung tritt diese Situation nicht auf.
2.2.4 Datentypen
55
Der Agent „Actions“ -> „Tools“ -> „4. Gesamtes Design als XML“ erzeugt ein neues Dokument mit dem Titel „Alle Designelemente dieser Datenbank als XML“. Dieses Dokument enthält die XML-Darstellung aller Designelemente in der Datenbank. Hier finden Sie zum Beispiel die Maske FO_URLLoader_k2 und die darin befindlichen Felder in der Darstellung als XML. Weiterführende Informationen zu diesem Thema finden Sie in Kapitel 17.7ff.
2.2.4
Datentypen
2.2.4.1
Text und Textliste
Die meisten Felder speichern Ihre Daten in Items vom Datentyp Text oder Textliste. Hierzu gehören Text, Password (*), Dialog list (spezielle in Notes übliche Dialogliste), Radio button (*) (Auswahl einer aus mehreren), Checkbox (Auswahl mehrere aus mehreren), Listbox (Auflistung), Combobox (*) (Aufklappmenü), Time zone (*) (Zeitzonen-Wähler), Color (*) (Farbwähler), wobei die mit (*) markierten Typen keine Textliste sein können. Die Feldtypen Text, Radio button, Checkbox, Combobox, Listbox und Dialog list sind Felder, die direkt Ihren eingegebenen oder ausgewählten Text in dem Item speichern. Die unterschiedlichen Typen dienen lediglich dazu, unterschiedliche Benutzerinterfaces zur Verfügung zu stellen. Diese Typen sind direkt vergleichbar mit den < INPUT> Feldern mit type= Text, Radio, Checkbox und <SELECT> mit size= 1 für ein Aufklappmenü bzw. size > 1 für eine Listbox im HTML. Die Dialogliste ist vergleichbar mit einer Listbox, die jedoch eine eigene Darstellung im Notes Client erhält. Diese Unterscheidung nach Feldtypen ist nur relevant, falls die Daten über ein Feld in das Dokument eingegeben werden sollen und erleichtern dem Benutzer die Eingabe. Items, die als Text über das Backend und eine der Methoden replaceItemValue oder appendItemValue in das Dokument eingefügt wurden, können über sämtliche obigen Felder in einer Maske dargestellt werden. Durch die impliziten Konvertierungsmöglichkeiten in einer Notes-Maske können auch z.B. Items vom Datentyp Zahl als Textfeld angezeigt werden, wobei dies wegen der möglichen Verwechslungen und dadurch entstehenden Fehler nicht empfohlen wird. Time zone, Color und Password sind spezielle Felder mit besonderen GUIs. Über den Typ Time zone kann eine Zeitzone ausgewählt werden, die dann z.B. für die Lokalisierung von Date/Time-Feldern benutzt werden kann. Dieser Typ steht nur als Feld in einer Maske zur Verfügung. Die ausgewählte Zeitzone wird als Text in einem Item gespeichert. Gleiches gilt für den Typ Color, der den RGB-Wert der ausgewählten Farbe als 3x2 Byte-Texte, ähnlich dem HTML, speichert. Felder vom Typ Password sind Felder, die beim Eintippen von Werten Sternchen statt der getippten Buchstaben anzeigen. Dies ist keine (!) Sicherheitsfunktion. Der eingegebene Wert wird im Klartext gespeichert.
56
2 Notes @ Domino
Daten werden in Textfeldern als so genanntes Lotus Multi-Byte Character Set LMBCS [Batutis, LMBCS] abgespeichert. Das LMBCS wurde 1989 entwickelt und hatte somit schon lange vor der Entwicklung von Unicode die Fähigkeit multilinguale Zeichensätze abzubilden. LMBCS hat drei Ziele: Abwärtskompatibilität Keinen Mehrbedarf an Speicherplatz in Standard-Situationen Abbilden aller internationaler Zeichensysteme in einer Struktur Um dies zu erreichen, werden die Zeichen in LMBCS-Gruppen unterteilt. Jede LMBCS-Gruppe bildet ein Standard-Industrie-Encoding ab. Die Gruppe 0 entspricht dem ASCII-Standard. Da durch eine spezielle Technik erreicht wird, dass Zeichen der Gruppe 0 kein extra Byte für die Gruppenzugehörigkeit benötigen, können die ersten beiden Forderungen erreicht werden. Durch die Abbildung der Industrie-StandardTabellen wird die dritte Forderung erreicht. So werden zum Beispiel in Gruppe 01 Zeichen des Encodings aus der Microsoft Codepage 850 oder in Gruppe 14 (hex) die Zeichen aus UTF-16 abgebildet. Da das Handling von Encodings und der LMBCS-Zeichenumwandlung komplett von den Core-Klassen in Domino übernommen wird, muss der Java-Programmierer sich nicht um LMBCS kümmern.
2.2.4.2
Namen, Autoren und Leser
Namens- und auch Autoren- oder Leser-Items sind Text-Items, mit einem zusätzlichen Flag, das Sie als Namensfelder auszeichnet. In Namensfeldern werden Namen von Benutzern, Gruppen oder Servern gespeichert. Domino arbeitet mit einer so genannten hierarchischen Namensstruktur, die der des LDAP-Standards sehr ähnlich ist. Namen können in der abgekürzten Form <>/<>/<>/<> oder in der kanonischen Form CN=< >/OU=<>/O=<>/C= <> dargestellt werden. In beiden Fällen können bis zu vier Hierarchien von Organisationseinheiten optional vorliegen. Das Land ist ebenfalls optional. Intern in den Items speichern Namensfelder den Namen als Text in der kanonischen Form. Der Notes Client konvertiert bei der Eingabe von Namen in Felder einer Maske automatisch zwischen der einfachen und der kanonischen Form der Namen. In Java muss der kanonische Name manuell oder unter Zuhilfenahme der Methoden der Klasse Name umgewandelt werden. Ein Name-Objekt kann über die Session aus einem kanonischen oder einfachen Namen erstellt werden. Beispiele hierzu finden Sie in Kapitel 12.1ff. Leser- oder Autorenfelder sind spezielle Namensfelder. Befindet sich mindestens ein Namensfeld in einem Dokument, so dürfen nur Benutzer, die in diesem Feld ge-
2.2.4.3 Datum / Uhrzeit
57
speichert sind oder die einer Gruppe oder Rolle angehören, deren Name im Feld gelistet ist, das Dokument öffnen. Autorenfelder arbeiten ähnlich. Durch sie wird bestimmt, wer ein Dokument bearbeiten darf.
2.2.4.3
Datum / Uhrzeit
Datums- und Uhrzeit-Items speichern Datums- oder Zeitangaben und Listen oder Bereiche solcher Werte. In der Programmierung in Java können diese Items sehr einfach über java.util.Calendar, lotus.domino.DateTime oder lotus. domino.DateRange erzeugt und per replaceItemValue im Dokument abgelegt werden. Für die verschiedenen Erscheinungsformen gibt es für die Felder in einer NotesMaske sehr praktische Zeit- und Datumswähler. Wichtig ist hierbei, dass solche Felder nur bestimmte Items verarbeiten können, so wird ein Zeitwähler immer ein Item mit einem reinen Zeit-Objekt (ohne Datum) erwarten. Listing 2-4 zeigt einführend den Umgang mit DateTime-Objekten: Ein DateTime-Objekt kann durch createDateTime in Session entweder aus einem java.util.Calendar (oder java.util.Date) oder direkt aus einem String, wie z.B. „12.12.97 16:30:00“ oder „Today“ erzeugt werden. Beachten Sie, dass die Trennzeichen systemabhängig sind, dass also durch die Verwendung des Strings Inkompatibilitäten auftreten können und eine Invalid date Exception geworfen wird, falls der String nicht interpretiert werden kann. Wird ein DateTime-Objekt sowohl mit einer Datums-, als auch mit einer Zeitangabe erzeugt, dann enthält es zusätzlich automatisch die lokal eingestellte Zeitzone. DateTime-Objekte können zusätzlich ein Flag für Sommer- und Winterzeit enthalten. DateTime-Listen werden wie gewohnt über einen Vector und replaceItemValue im Dokument abgelegt. In der Darstellung werden die einzelnen Werte durch ein Trennzeichen, das in den Feldeigenschaften spezifiziert werden kann, separiert. Zeitbereiche (Range) werden über lotus.domino.DateRange aus DateTime-Objekten erzeugt. Date/Time-Ranges können nicht mit einem speziellen Feld angezeigt werden, sondern erscheinen in einem normalen Date/Time-Feld, wobei der Anfangs- und Endwert durch ein Minuszeichen getrennt dargestellt werden. Sollen nur ein Datum oder nur eine Zeit erzeugt werden, kann der jeweils nicht benötigte Teil mit setAnyDate oder setAnyTime „entfernt“ werden. Hierdurch werden auch Zeit-Ranges möglich, für die es einen sehr praktischen Time-Range-Wähler gibt (s. Abb. 2-10). In dieser Abbildung sehen Sie außerdem die Eigenschaften des Dokuments. Im zweiten Tabellenreiter mit dem Dreiecks-Symbol können die Items des Dokuments angezeigt werden. Sehr schön ist der Datentyp „Time/Date List or Range“ erkennbar.
58
2 Notes @ Domino
import java.util.*; import lotus.domino.*; public class CreateTimeItemsJavaAgent extends AgentBase { public void NotesMain() { Document doc = null; try { Session session = getSession(); Database db = session.getAgentContext().getCurrentDatabase(); //Neues Dokument erzeugen doc = db.createDocument(); doc.replaceItemValue ("Form", "FO_TimeExample_k2"); doc.replaceItemValue ("F_Titel", "Beispiel für Zeit Items."); //ZeitObjekt aus Calendar erzeugen Calendar jetzt = Calendar.getInstance(); DateTime dtsecond = session.createDateTime(jetzt); //zeitObjekt als DateTime DateTime dtfirst = session .createDateTime ("Yesterday 11:35:07"); //Date / Time Liste ins Dokument Vector list = new Vector (); list.addElement(dtfirst); list.addElement(dtsecond); doc.replaceItemValue ("F_datetimeList", list); //Date / Time Range ins Dokument DateRange dr = session.createDateRange(dtfirst, dtsecond); doc.replaceItemValue("F_datetimeRange", dr); //Einzelne Zeit und Datums Objekte ins Dokument doc.replaceItemValue ("F_datetime", dtsecond); dtsecond.setAnyDate(); doc.replaceItemValue ("F_time", dtsecond); dtfirst.setAnyTime(); doc.replaceItemValue ("F_date", dtfirst); //Zeit-Range ins Dokument DateTime timeFirst = session.createDateTime ("10:00"); DateTime timeSecond = session.createDateTime ("11:00"); timeFirst.setAnyDate(); timeSecond.setAnyDate(); DateRange tr = session.createDateRange(timeFirst,timeSecond); doc.replaceItemValue("F_timeRange", tr); doc.save (true, false); } catch (Exception e) { e.printStackTrace(); } } }
Listing 2-4 Date / Time Items in einem Dokument erzeugen
Felder in einer Notes-Maske können Datumsangaben vom 1.1.0001 bis zum 31.12.9999 und Zeitangaben mit einer Genauigkeit von einer Sekunde aufnehmen. Intern kann Notes die Zeitangaben zwar genauer und für die Datumsangaben auch Jahre vor Christi speichern, dies ist aber nur von theoretischer Bedeutung, da Items nicht über das Java-API mit diesen erweiterten Werten erzeugt werden können. Zeit-Objekte können entweder mit der Methode getItemValue oder mit der Methode getItemValueDateTimeArray aus dem Dokument gelesen werden. In beiden Fällen wird ein Vector aus Elementen der Klasse lotus.domino. local.DateTime oder lotus.domino.local.DateRange für den lokalen Zu-
2.2.4.4 Zahl und Zahlliste
59
Abb. 2-10 Time-Range-Wähler-Feld
griff und für den Remote Zugriff ein Vector mit Elementen der Klassen lotus.domino.cso.DateTime oder lotus.domino.cso.DateRange zurückgegeben. Die Methode getItemValueDateTimeArray ist neu seit Version 6.5.
2.2.4.4
Zahl und Zahlliste
Numerische Felder und Items sind vergleichbar mit Textfeldern, kennen jedoch nur eine einzige Art eines Eingabefeldes (Radiobuttons etc. erzeugen ja bekanntlich Text). Numerische Items können entweder eine einzelne Zahl oder eine Liste von Zahlen aufnehmen. Erzeugt wird ein Zahl-Item durch doc.replaceItemValue ("F_ZahlItem", new Double (17.123));
Wichtig ist es, ein Objekt zu übergeben, da die Methode replaceItemValue ein Object erwartet. Zur Zeit gibt es keine Signatur, mit der eine Zahl direkt übergeben werden könnte. Die direkte Übergabe einer Konstanten oder Variablen ist daher nicht möglich. Intern werden Zahlen von Domino im IEEE 64-Bit Floating Point Format (ANSI/IEEE Standard 754-1985) gespeichert. Dieses Format besteht aus einem Bit für
60
2 Notes @ Domino
das Vorzeichen, 11 Bit für den Exponenten und 52 Bit für die Mantisse. Das Format erlaubt die Darstellung des Wertes +0, -0, +Unendlich, -Unendlich, NaN (Not a Number – keine Zahl) und positiver und negativer Werte zwischen 4.9 x 10^-307 und 1.8 x 10^+308. Zu beachten ist, dass die Methode replaceItemValue zwar auch Long- oder Integer-Objekte annimmt, diese intern aber immer als Floating Point speichert. Wie immer bei der Speicherung von Floating-Point-Werten in Ihrer binären Repräsentation muss mit Rundungsfehlern bei der Umrechnung gerechnet werden. Natürlich ist auch mit Ungenauigkeiten zu rechnen, wenn mit sehr großen oder sehr kleinen Zahlen gerechnet wird. Zahllisten werden wie Textlisten als Vector aus Number-Objekten gespeichert.
2.2.4.5
Antwortreferenzliste
Antwortreferenzen können nur über spezielle Maskeneigenschaften oder über die Java-Methode makeResponse erzeugt werden. Diese Referenzen werden im Item $Ref gespeichert und bilden den Bezug zwischen Tochter- und Mutterdokument bei der Erzeugung von Antwortdokumenten, wie bereits in 2.2.3 im Zusammenhang mit Listing 2-3 beschrieben.
Listing 5-6 Gesichertes Servlet mit SSO zwischen WAS und Domino
218
5 Java-Web-Anwendungen @ Domino
wieder abgemeldet werden kann. Prinzipiell kann dies natürlich auch durch eine eigene Programmierung erreicht werden, die einfach das Cookie mit dem LtpaToken löscht. 11 Vor- und Nachteile des SSO über Ltpa Token zwischen WebSphere und Domino Sollen Domino und WebSphere per SSO miteinander verbunden werden, ist das Ltpa Token sicher eine der sehr guten Möglichkeiten, dies einfach, sicher und schnell umzusetzen. Vorteile dieser Vorgehensweise sind: • Nahtlose und problemlose Kommunikation zwischen dem Domino-LDAP und WebSphere • Einfaches Setup über die WebSphere-Verwaltungskonsole • Weitreichende und granuläre Sicherheitskonfiguration • Zuordnung von Benutzern zu Berechtigungsklassen • Kein Programmierungsüberhang Nachteile dieser Vorgehensweise sind: • Nicht alle der von WebSphere angebotenen Einstellungen sind J2EE-Standard, daher nicht portierbar • Keine Autorisierung auf Methodenebene, wie bei JAAS • Im Wesentlichen optimiert für Web-Anwendungen (Login Servlet) • LDAP kann zum Performance-Engpass werden, wenn viele Anmeldeoperationen zu erwarten sind Zum vorletzten Punkt sei noch gesagt, dass natürlich auch eine vom Browser unabhängige Java-Applikation einen HTTP Request an den WebSphere Server zur Authentifizierung machen kann, um ein LTPA Token zu beziehen, sofern eine TCP/IP-Verbindung zwischen Anwendung und Server besteht. Da es zur Zeit keine Unterstützung von JAAS durch Domino gibt, kann alternativ noch das CORBA Security API über den LoginHelper genutzt werden, um eine Autorisierung auf Methoden-Ebene zu erreichen und gleichzeitig einen Security Context zur Verfügung zu haben, mit dem gegen Domino authentifiziert werden kann. Fazit: Sieht man von der fehlenden Portierbarkeit ab, ist das LtpaToken die Technik der Wahl, wenn Web-Anwendungen mit Domino per SSO verbunden werden sollen.
5.3.5.1 Setup von WebSphere und Domino für SSO via LtpaToken
219
12 Troubleshooting •
•
•
• •
• •
•
• •
Wenn Sie die Reihenfolge für den Setup der Sicherheit in WSAD nicht einhalten oder wenn die Verbindung zum Domino-LDAP nicht korrekt eingerichtet ist, kann es passieren, dass Sie sich aus der Verwaltungskonsole „aussperren“. In diesem Fall besteht die Möglichkeit, den Server und die Serverkonfiguration (genauer gesagt, das in WSAD angelegte Objekt) in der Perspektive „Server“ zu löschen. WSAD entfernt dann die zugehörigen XML-Konfigurationen und Sie können einen neuen unkonfigurierten Server in Ihrer IDE-Umgebung anlegen. Bei Kommunikationsproblemen mit dem LDAP-Server überprüfen Sie diesen mit einem LDAP-Browser, dies erleichtert auch das Verständnis für die von Domino angebotene Struktur der Directory-Daten. In der Datei notes.ini kann der Parameter LDAPDEBUG=n (n=1..7) gesetzt werden, um (nach einem Neustart) Debug-Informationen anzuzeigen. Die Werte werden wie folgt interpretiert: 1=Query-Informationen, 2=Result-Informationen, 3=1 und 2, 4=Authentifizierungsinformationen, 5=1 und 4, 6=2 und 4, 7=Alle Informationen (n=8..15 – Erweiterte Debug-Informationen). Bedenken Sie die unterschiedlichen Namenskonventionen in Domino und LDAP. Da Domino nicht nach Groß- und Kleinschreibung unterscheidet, ist es sinnvoll, diese für die Authentifizierung per LDAP in der entsprechenden Einstellung in der Verwaltungskonsole ebenfalls abzuschalten. Im Gegensatz zu anderen LDAP-Servern ist für Domino die Angabe einer BASE DN nicht erforderlich. Das SSO ist an die in der Konfiguration gewählte Domäne gebunden. Für erste Tests lassen Sie am besten dieses Feld leer und beziehen Sie sich in allen Einstellungen auf „localhost“. Alternativ können Sie in Ihrem Hostfile einen voll qualifizierten Domänen-Namen vergeben und müssen sich dann in allen Einstellungen auf diesen Namen beziehen. Wenn Sie dies konsequent für LDAP-Realm, Domino-Server-Host, DIIOP-Server-Host und WAS-Server-Host machen, vermeiden Sie Anmeldefehler. Versuchen Sie, die Anwendung über verschiedene URL, über die IP über „localhost“, über „127.0.0.1“ und optional über den Hostnamen anzusprechen, wenn Sie sich unsicher sind, ob Ihr Domänen-Setup korrekt ist. Vergessen Sie nicht, DIIOP für den Domino-Server zu aktivieren und für die gewünschte Domäne einzurichten. Gleiches gilt für den Domino-HTTP, wenn Sie createSession ohne Portnummer verwenden.
220
5 Java-Web-Anwendungen @ Domino
5.3.6
Connection Pooling – Object Request Broker
Bisher wurden in den Beispielen für jeden Life Cycle einer Anwendung oder eines Servlets eine DIIOP-Verbindung geöffnet und eine Domino-Session aufgebaut und wieder geschlossen. Dies erfordert für jeden Aufbau die Bereitstellung eines TCP Sockets, um die DIIOP-Verbindung aufzubauen. Einerseits entsteht hierdurch ein Überhang an (Zeit-) Kosten für den Aufbau der Verbindung, da diese Anforderung immerhin bis ganz auf Systemebene durchgreift und zum anderen werden hierdurch übermäßig Ressourcen für die einzelnen Verbindungen beansprucht. Gerade für Servlets ist es jedoch typisch, dass viele kurze Sitzungen geöffnet werden, da ja jede Benutzerinteraktion den erneuten Aufruf eines Servlets oder einer JSPSeite erfordert – im Gegensatz zu einer Standalone-Anwendung, die durchaus eine Session öffnen und dann für die gesamte Benutzungsdauer der Anwendung offen halten kann. Für das Servlet wäre es also vorteilhaft eine DIIOP-Verbindung über mehrere Aufrufe offen halten zu können, damit die Verbindungen nicht zu kurz (um noch effizient zu sein) werden, und für eine Anwendung ist zu berücksichtigen, dass die Verbindung nicht zu lange geöffnet bleibt, damit nicht eine Domino-Session serverseitig geschlossen wird (s. auch Kap. 5.3.4). Domino bietet die Möglichkeit über entsprechende CORBA API einen Object Request Broker zu beziehen, der über mehrere Domino-Sessions verwendet werden kann. Bezogen wird ein neuer ORB über die NotesFactory mit broker = NotesFactory.createORB(),
dieser kann dann zum Erzeugen einer Notes-Session z.B. über session = NotesFactory.createSession (String host, ORB orb, HttpServletRequest req);
oder session = NotesFactory.createSession (String host, ORB orb, String ltpaToken);
verwendet werden. Jede Session, die denselben ORB benutzt, nutzt somit dieselbe DIIOP-Verbindung. So einfach und geradeheraus dieser Ansatz ist, so komplex ist das eigentliche Geschehen im Hintergrund: •
Jede Notes-Session beinhaltet Autorisierungs- und Authentifizierungsinformationen. Daher kann eine Session über einen ORB nur mit einer eindeutigen Authentifizierung erstellt werden, wobei hierfür alle vier Signaturen, die im Kapitel über SSO beschrieben wurden, zulässig sind.
Für eine anonyme Session genügt es nicht, den HttpServletRequest eines „unangemeldeten“ Besuchers zu übermitteln. Hierfür muss ein gültiges LtpaToken einer anonymen Anmeldung aus createSession (String host, "","") verwendet werden. Wird ein HttpServletRequest ohne ein gültiges LtpaToken übermittelt, wird eine NotesException mit der Meldung „not implemented“ geworfen. Die Authentifizierungsinformationen werden benötigt, da Domino serverseitig die verschiedenen Domino-Objekte, wie Datenbanken, Views oder Dokumente, im Benutzerkontext behandelt. Über einen ORB kann ein Pool von Notes-Sessions aufgebaut werden. Je nach Anwendung und verwendeter Hardware wird die Anzahl der handhabbaren Sessions je Broker variieren. Überlasten Sie eine ORB nicht mit zu vielen Sessions. Der ORB wird statisch angelegt, um ihn über mehrere Sessions nutzen zu können. In der Multithreaded-Umgebung der Servlets muss unbedingt für eine korrekte Synchronisierung beim Erstellen neuer Sessions und ORBs geachtet werden.
Listing 5-7 zeigt ein Servlet mit einem einfachen Connection Pool, wobei berücksichtigt wird, dass nur angemeldete Benutzer dieses Servlet benutzen können und natürlich über einen statischen ORB mehrere Notes-Sessions gebündelt werden. Der erste wichtige Schritt im Servlet ist, dass überprüft wird , ob der Benutzer angemeldet ist, also ein LtpaToken im HttpServletRequest vorliegt, das einfach als Cookie aus dem Request extrahiert werden kann . Vorausgesetzt wird hier natürlich, dass SSO am Server aktiviert ist und über Cookies verwaltet wird (unter WebSphere wäre auch ein Rewriting von URLs zur Session-Verfolgung denkbar, dies ist aber für diesen Ansatz nachteilig). Ein Manko an dieser Vorgehensweise ist, dass im Beispiel nicht überprüft wird, ob das Token überhaupt noch am Server gültig ist. Dies soll in einem späteren Beispiel noch gelöst werden. Wird festgestellt, dass noch kein Token vorliegt, so muss dem Benutzer eine Login-Seite angezeigt werden. Im Beispiel wird davon ausgegangen, dass das Servlet auf einem Domino-Server zum Einsatz kommt, so dass ein Login über die URL „/ names.nsf?login“ angezeigt werden kann. Damit nach einem erfolgreichen Login das Servlet angezeigt werden kann, muss der Login-Seite noch über den URLParameter RedirectTo die gewünschte Seite übermittelt werden. Im Beispiel finden Sie allgemein gültigen Code , der auf jeder beliebigen Seite zum Einsatz kommen kann und nach einem Login zu dieser Seite zurückkehrt. Da die Seite names.nsf nach einem erfolgten Login nur ein GET ausführt, ist diese Technik nur für Seiten einsetzbar, die über GET erreicht werden sollen und können. URL-Parameter der ursprünglichen Seite werden aber mit übermittelt und durch den URLEncoder codiert, damit Sonderzeichen korrekt übermittelt werden. Selbstverständlich kann in der hier verwendeten Methode doRedirect auch ein Ansatz verwendet werden, der ein Verfahren verwendet, dass mit einem eingesetzten Application Server – unabhängig von Domino – ein Login durchführt, allerdings
muss dieses Verfahren mit der SSO-Technik LTPA von Domino-kompatibel sein, damit ein entsprechender HttpServletRequest bzw. ein LTPA Token zur Verfügung steht. Der im Beispiel aus Kapitel 5.3.5.1 vorgestellte Weg ist hierfür geeignet.
5.3.6 Connection Pooling – Object Request Broker
223
Für den Bezug des ORBs wurde zunächst eine statische Klassenvariable broker deklariert , damit diese allen Instanzen des Servlets zur Verfügung steht. In der Methode getBroker wird dann überprüft, ob der ORB bereits initialisiert wurde oder ob bereits die maximale Anzahl an Sessions, die über diesen Broker verwaltet werden sollen, erreicht ist . Ist der Broker noch null oder sind bereits (in unserem Fall) 10 Sessions in diesem Broker offen, so wird von der NotesFactory ein neuer ORB angefordert und von dieser erstellt35 . Beim Bezug des Brokers ist es extrem wichtig, dass immer nur ein Thread zur Zeit überprüft, ob ein Broker benötigt wird und diesen erstellt, sonst kann es in Race Conditions zu Problemen kommen. Hierfür muss die Java-Funktionalität „synchronized“ verwendet werden. Da es sich beim broker um eine statische Klassenvariable handelt, die auch null sein kann, ist es nicht möglich lediglich die Variable broker zu synchronisieren, sondern es muss die statische Klassen-Methode getBroker synchronisiert werden. Synchronisierung von statischen Methoden hat zur Folge, dass als zu synchronisierendes Objekt die gesamte Klasse angenommen wird. Dies ist aus Performance-Gesichtspunkten nicht optimal. Daher wird in einem späteren Beispiel eine Möglichkeit vorgestellt dieses Dilemma aufzulösen, indem eine eigene Klasse für den Connection Pool vorgestellt wird. Wird nun createSession mit Hostname, Broker und dem HttpServletRequest (mit dem gültigen Anmeldetoken) aufgerufen , wird innerhalb des Brokers eine Session für den aktuellen Benutzer erstellt. Mit dieser Notes-Session können Sie nun wie gewohnt arbeiten, Datenbanken öffnen und mit den Domino-Objekten arbeiten. Sollen alle Verbindungen mit den Rechten eines bestimmten Funktionsusers hergestellt werden, so steht die in diesem Beispiel verwendete Technik, die Benutzerauthentifizierung über einen Redirect auf eine Login-Seite herzustellen, nicht zur Verfügung. Insgesamt kann also festgestellt werden, dass noch einige offene Fragen im bisher gezeigten Ansatz bestehen: • • •
Ein eventuell ungültig gewordenes LTPA Token wird nicht erkannt. Es fehlt eine alternative Anmeldung für einen generellen Funktionsuser Die gewählte Art des synchronize ist nicht optimal
Die nächsten beiden Beispiele setzen sich mit diesen Fragestellungen auseinander, zunächst werden die ersten beiden Punkte näher untersucht. Um zu erreichen, dass eine
35
Die Architektur von Java und seiner Garbage Collection macht übrigens den Life Cycle der ORBs einfach zu handhaben. Wird beim Öffnen der elften Session ein neuer ORB erstellt, wird zwar die statische Variable broker nun auf das neu erstellte ORB-Objekt verweisen, aber der vorige ORB wird noch so lange „am Leben“ bleiben, wie offene Notes-Sessions, die diesen ORB als Argument im createSession übergeben haben, bestehen. Erst wenn keine Session mehr diesen ORB als Argument benutzt, wird es keinen Verweis mehr auf das alte ORB-Objekt geben und es kann von der GC aufgeräumt werden. Folgerichtig kann ohne Bedenken einfach die statische Variable neu zugewiesen werden, ohne dass die ORB-Objekte ausdrücklich verwaltet werden müssten.
224
5 Java-Web-Anwendungen @ Domino
Anmeldung generell für einen Funktionsuser erfolgt, kann initial einmalig mit den explizit verwendeten Daten für Benutzername und Passwort ein LtpaToken generiert werden und dieses für alle folgenden Authentifizierungen verwendet werden. Dieses Token wird, ähnlich wie im vorigen Beispiel schon der Broker, in einer statischen Variable vorgehalten und über eine Methode getToken geliefert, die bei jedem Aufruf überprüft, ob schon ein Token vorliegt, oder ob ein neues bezogen werden muss. Dem Token selbst ist nicht anzusehen, ob die dahinter geführte session noch gültig ist. Dies wird an einer anderen Stelle überprüft. Das Beispiel aus Listing 5-8 zeigt eine überarbeitete Version des Connection Pooling Servlets, das abgelaufene LtpaTokens handhaben kann und auf der Anmeldung eines Funktionsusers basiert. Um den Ablauf eines Tokens erkennen zu können, wurde der Bezug der eigentlichen Notes-Session in die Methode getSession ausgelagert. Dort wird die session zunächst wie im vorigen Beispiel mit Hostname, Broker und LtpaToken bezogen . Ist das über die private Methode getToken gelieferte LtpaToken ungültig, so wird Domino eine NotesException werfen, die abgefangen wird, damit erneut ein Verbindungsversuch unternommen werden kann . Hierfür wurde die Methode getToken mit einem boolean-Parameter enforce versehen, über den erzwungen werden kann, dass ein neues und gültiges Token erzeugt wird. Für die Erstellung der Session wird der Broker wie im vorigen Beispiel bezogen, die Methode getToken in diesem Beispiel extrahiert das Token jedoch nicht aus dem Request, sondern erzeugt eine Notes-Session auf dem klassischen Wege über DIIOP, aber ohne ORB . Diese session liefert ein gültiges LtpaToken , das in der statischen Variablen token vorgehalten wird, um für weitere Verbindungen verwendet zu werden. Auch diese Methode muss aus den oben genannten Gründen synchronisiert werden, um eindeutige Session Token beziehen zu können und in der Multithreaded-Umgebung des Servlets nicht in Racing Conditions zu geraten. Die Behandlung der Frage der Synchronisation bedarf etwas mehr Aufwand und erfordert eine eigene Klasse ConnectionPool, die als Objekt instanziert und synchronisiert werden kann. Die Idee ist, eine Klasse zu erstellen, die die komplette Abwicklung des Connection Pools übernimmt, den Broker verwaltet, auf Anfrage Notes-Sessions liefert und sich um die Synchronisation kümmert. Im Einsatz (Listing 5-9) wird diese über die eingebaute Factory-Methode getConnectionPool als Objekt bezogen und stellt damit die benötigten Methoden zur Verfügung, so kann z.B. über verschiedene getter-Methoden der Status über Token , die Anzahl der insgesamt geöffneten Sessions und der aktuellen Anzahl der Sessions im zur Zeit gültigen Broker abgefragt werden. Der eigentliche Pool wird über die Factory bezogen, die dafür verantwortlich ist, dass nur so viele Pools im Umlauf sind, wie nötig und sinnvoll sind, damit ein Pool auch tatsächlich mehreren Benutzern (jeder Benutzer, der das Servlet über den Browser öffnet, kommt ja als eigener Thread) zur Verfügung steht und liefert der Anwendung über getSession eine neue Notes-Session, die aber von der Anwendung selbst ausdrücklich recycled werden muss. Die Klasse ConnectionPool liefert hierfür die statische Utilities-Klasse recycleSession, die zur Vereinfachung das Exception Handling übernimmt.
Listing 5-8 Connection Pool mit Funktionsuser für die Anmeldung
Ein kleiner Zusatzeffekt der Kapselung des Pools in einer Klasse ist, dass er sogar als final deklariert werden kann, da er nur ein einziges Mal zugewiesen werden muss und dann auf Dauer für alle Threads der gesamten Anwendung Gültigkeit behalten soll und behält (es sei denn er wird von der GC aufgeräumt, dann würde er aber bei der nächsten Benutzung neu instanziert) und somit außerdem vom Compiler noch weiter optimiert werden kann.
Listing 5-10 zeigt den Entwurf der Klasse ConnectionPool. Der Pool selbst besitzt die Methoden getBroker und getToken als private Methoden, um intern diese Objekte beziehen zu können, und getSession als öffentliche Methode, über die die Anwendungen neue Sessions beziehen. Diese Methoden sind dem ursprünglichen Entwurf ähnlich, unterscheiden sich aber in wichtigen Punkten. Zunächst ist wichtig, dass die Felder für Broker und Token nicht mehr als static deklariert sind, sondern, da der Pool instanziert werden soll, als Instanzvariablen der Klasse deklariert werden (in Listing 5-10). Die Factory-Methode getConnectionPool ist dafür verantwortlich, so viele Pools wie nötig und sinnvoll in Umlauf zu bringen. Im Beispiel wurde diese Logik stark vereinfacht und durch einen Singleton ersetzt, so dass immer nur ein einziger Pool im Umlauf ist, was für viele Anwendungen sogar ausreichen wird. Die Klasse erhält nur einen privaten Konstruktor , da ein Pool nur über die Factory-Methode bezogen werden soll. Mit dem Parameter host wird bisher nur intern der Host festgelegt, zu dem der Pool die Verbindung aufbauen soll. Sollen verschiedene ConnectionPools zu verschiedenen Hosts ermöglicht werden, muss die Factory dies verwalten. Hier sind natürlich zusätzlich Varianten denkbar, die z.B. den anzumeldenden Benutzer einbeziehen. Die Methode getSession unterscheidet sich nur dadurch vom vorigen Beispiel, dass sie nicht mehr statisch deklariert ist. Dies wird notwendig, da broker und token keine Klassen- sondern Instanzvariablen sind. Der Bezug der Notes-Session als solches bleibt unverändert. Die Methoden getBroker und getToken sind aus demselben Grund nicht statisch deklariert, unterscheiden sich aber in der Synchronisation. Die Metho-
Listing 5-10 ConnectionPool-Klasse für Connection Pooling mit verbesserter Synchronisation
228
5 Java-Web-Anwendungen @ Domino
de getBroker selbst muss nicht mehr synchronisiert werden, es genügt, den CodeTeil, der den Broker prüft und bei Bedarf neu bezieht, in einem synchronizedBlock zu kapseln . Synchronisiert wird das Objekt this, also die aktuelle Instanz der Klasse ConnectionPool. Der hierdurch erreichte Fortschritt ist, dass nicht mehr die gesamte Methode synchronisiert werden muss und insbesondere, dass nur noch der ConnectionPool durch die Synchronisation gelocked wird und nicht mehr die gesamte Klasse des Servlets, wie im Beispiel zuvor, denn dort war getBroker sowohl synchronisiert als auch statisch und synchronisierte Klassen-Methoden (statische Methoden) verwenden das Klassen-Objekt als zu synchronisierendes Objekt. Die Methode getToken ist weiterhin synchronisiert , verwendet aber als Instanz-Methode implizit ebenfalls das Objekt this für das Locking, mit den gleichen Vorteilen wie eben skizziert. Diese verbesserte Variante des Connection Pools bietet also einschließlich der zuvor gemachten Verbesserungen folgende Vorteile: • • • •
Ein eventuell ungültig gewordenes LTPA Token wird erkannt. Bequeme Nutzung als Objekt und einfacher Bezug einer Session durch getSession Optimierte Synchronisation Einsatz mehrerer Instanzen des Connection Pools denkbar
Selbstverständlich kann durch Kombination des ersten (Listing 5-7) und letzten Beispiels (Listing 5-10) ein ConnectionPool realisiert werden, der nicht mit einem grundsätzlichen Funktionsuser Verbindungen herstellt, sondern der die Verbindungen mit dem aktuell angemeldeten User der HTTP Session herstellt. Auch ist es möglich, die Anmeldung anstatt mit einem bestimmten Benutzer durch Übergabe leerer Strings als anonyme DIIOP- und Notes-Session herzustellen. Auch dies lässt sich wiederum kombinieren und ermöglicht, HTTP-Benutzer, die keine Anmeldung haben, mit einem Token einer derart anonym hergestellten Verbindung anzumelden und alle angemeldeten HTTP-User mit ihrem Token anzumelden, nachdem sie auf eine Login-Seite redirected wurden. Auch das zuletzt vorgelegte Beispiel lässt sich noch weiter verbessern. Bisher wird jeder ORB verworfen, wenn er die vorgegebene Anzahl von Sessions verarbeitet hat. Durch geschicktes Management der offenen Sessions, insbesondere durch eine Registratur, in der vermerkt ist, welche Sessions offen sind und welche bereits wieder geschlossen sind, lässt sich ein ORB weit öfter wiederverwenden, als dies im vorgegebenen Beispiel der Fall ist. Allerdings ist hierbei der Aufwand für das Management gegen die Kosten für das jeweils erneute Erstellen eines ORB abzuwägen. Connection Pooling für Domino-Verbindungen lässt sich also beim Einsatz von DIIOP gut realisieren, muss aber mit hoher Aufmerksamkeit verwendet werden. Bedenken Sie, dass beim Connection Pooling sowohl Ihre eingesetzte Hardware als auch das Betriebssystem bei der Bereitstellung des TCP Stacks wesentlich involviert sind. Überlasten Sie die TCP-Verbindungen nicht durch einen zu großen Pool. Am besten
5.3.7 Die NotesFactory – Überblick
229
ist es, wenn Sie die Größe des Pools für Ihre Anwendung durch geeignete Stresstests ermitteln und optimieren. Bedenken Sie auch, dass die Dauer der einzelnen NotesSessions eine wichtige Rolle bei der Ermittlung der optimalen Parameter spielt.
5.3.7
Die NotesFactory – Überblick
Die NotesFactory sieht verschiedene Kombinationen der vorgestellten Techniken vom einfachen Login über SSO und SSL bis hin zur Verwendung von ORBs vor. Da für einige Signaturen bestimmte Parameter als String oder Object deklariert sind und je nach Wert dieser Parameter ein unterschiedliches Verhalten an den Tag legen, sind im Folgenden die Signaturen als Aufruf mit den tatsächlichen Werten beschrieben. Die jeweiligen Parameter müssen wie folgt deklariert sein: String userString; String passwortString; String hostString; String ior; org.omg.CORBA.ORB orb; String [] args; String tokenString; org.omg.SecurityLevel2.Credentials credentials; HttpServletRequest httpServletRequest; java.util.Properties props;
Alle Methoden werfen eine NotesException im Fehlerfall. Die hierfür notwendigen Signaturen stehen wie folgt zur Verfügung. Lokale Verbindung bei Benutzung der lokalen Notes-User-ID session = NotesFactory.createSession(); session = NotesFactory.createSession ((String) null, (String) null, (String) null)); session = NotesFactory.createSession ((String) null, (String) null, passwortString);
Für diese Zugriffe muss eine lokale Notes-Installation vorliegen. Angemeldet wird der Benutzer der lokalen Notes-ID. Benutzt diese ID ein Passwort, muss die dritte Signatur verwendet werden. NotesThread wird verwendet. session = NotesFactory.createSessionWithFullAccess(); session = NotesFactory.createSessionWithFullAccess(passwortString);
Wie oben, jedoch unter Umgehung von Leser- oder Autorenfeldern.
Für diese Anmeldung muss ebenfalls eine lokale Notes-Installation vorliegen. NotesThread wird verwendet. Angemeldet wird entweder anonym (erste Signatur) oder der genannte Benutzer, der im Domino Directory bekannt sein muss. Erzeugt wird eine so genannte Internet-Session, für die besondere Anmelderechte in der ACL festgelegt werden. Nur im Kontext des Servers gültig. DIIOP-Verbindung über Benutzername und Passwort session = NotesFactory.createSession(hostString); session = NotesFactory.createSession(hostString,"",""); session = NotesFactory.createSession(hostString, userString, passwortString); session = NotesFactory.createSession(hostString, args, userString, passwortString); session = NotesFactory.createSession(hostString, orb, userString, passwortString);
DIIOP wird verwendet. NotesThread wird nicht verwendet. host darf nicht null oder leer sein und kann den DIIOP-Port enthalten, wenn kein HTTP-Task zur Verfügung steht.
DIIOP wird verwendet. NotesThread wird nicht verwendet. Für die Verwendung von Credentials oder null als zweitem Parameter wird WebSphere benötigt. session = NotesFactory.createSession(hostString, orb, tokenString); session = NotesFactory.createSession(hostString, orb, credentials); session = NotesFactory.createSession(hostString, orb, (Credentials) null); session = NotesFactory.createSession(hostString, orb, httpServletRequest);
ebenso unter Verwendung eines Object Request Brokers, session args, session args, session args, session args,
Die Signaturen mit den Parametern userString, passwortString und args sind neu seit Domino 6.5. Bezug eines ORB orb = NotesFactory.createORB(); orb = NotesFactory.createORB(args); orb = NotesFactory.createORB(props);
5.4
Troubleshooting
Beim Setup von Java-basierten Domino-Anwendungen gibt es einige Fallstricke, auf die in den vorangegangenen Kapiteln hingewiesen wurden. Diese sollen an dieser Stelle nochmals kompakt zusammengefasst werden und als Kurzreferenz bei SetupProblemen dienen. Zusätzlich werden noch mögliche Exceptions und deren Bedeutung aufgelistet36. Netzwerk, Tasks • •
36
Netzwerkverbindung – Überprüfen Sie, ob der DIIOP-Server korrekt gestartet wurde und ob er über das Netzwerk zu erreichen ist. Falls Sie den IOR über den HTTP beziehen, gilt Gleiches für den HTTP-Task von Domino. Teilweise entnommen aus [Domino Exceptions]
5.4 Troubleshooting • • • • •
233
Ist der DIIOP-Task (und der HTTP-Task) an die Domain / IP gebunden, zu der Sie verbinden? Laufen DIIOP (und HTTP) unter den Ports, zu denen Sie verbinden (Achtung: Linux verwendet in der Regel andere Ports). Netstat (netstat -a) liefert den Zustand der offenen Verbindungen auf dem Server. Telnet kann genutzt werden, um vom Client die Verbindung auf einem bestimmten Port zu prüfen (telnet host port). Typische Exceptions für fehlerhafte Netzwerkverbindungen, nicht erhaltenes IOR oder nicht korrekt oder auf dem falschen Port gestartete Tasks oder ein Verbindungsversuch zu einem nicht erreichbaren Host sind: NotesException: Could not get IOR from Domino Server java.net.ConnectException: Connection refused java.net.ConnectException: Operation timed out java.net.UnknownHostException NotesException: Could not open Notes session: org.omg.CORBA.COMM_FAILURE
Sicherheit • •
•
Akzeptiert der HTTP-Task anonyme Verbindungen? Meldet ein Web-Agent „Agent done“? Dies ist ein Hinweis, auf eine eventuell nicht korrekt abgefangene Exception (untersuchen Sie die Datenbank log.nsf oder die Serverkonsole). Eventuell konnte der Agent nicht geladen werden, weil er entweder nicht korrekt compiliert wurde oder weil ihm Bibliotheken fehlen. Eventuell stimmen die Ausführungsberechtigungen für den Web-Agenten nicht. Überprüfen Sie Delegationen („run on behalf of“) und Signatur. Überprüfen Sie, ob – der ausführende Benutzer die benötigten Rechte in der ACL der Datenbank hat (Delegationen und Signaturen berücksichtigen oder bei Servlets Anmeldung überprüfen). – die ACL nicht den Internetzugriff einschränkt (erweiterte Einstellungen) – insbesondere bei DIIOP-Verbindungen und lokalen Internet-Sessions. – die benötigten Rechte im Serverdokument eingetragen sind (Abschnitt Security -> Server Access) – der Benutzer die benötigten Rechte für Delegationen hat (Abschnitt Security -> Programmability Restrictions) – der Benutzer auch im Adressbuch vorhanden ist und dort ein InternetPasswort vergeben ist. – der Benutzer nicht durch die programmatischen Rechte in der Ausführung bestimmter Operationen eingeschränkt ist (z.B. File I/O)
oder Benutzer nicht vorhanden oder falsches Passwort) NotesException: Server access denied (Benutzer hat keine Rechte im
Abschnitt Server Access oder anonyme Verbindungen sind nicht erlaubt.) Single Sign On • • • •
•
Netzwerkverbindung zwischen WAS und Domino prüfen LDAP-Task auf Domino auf korrekte Ausführung und Setup prüfen (DebugEinstellungen verwenden!) Wurde der Ltpa Key korrekt zwischen WAS und Domino ausgetauscht? Wurde die Anwendung, für die eine Authentifizierung stattfinden soll, unter derjenigen Domain im Browser aufgerufen, die im SSO Setup in WAS und Domino als LDAP-Realm eingetragen ist? Sind die verwendeten Benutzer im Domino-Adressbuch?
Setup •
• •
Das Setup der Classpath-Variablen in Domino ist nicht ganz einfach. An folgenden Stellen kann ein Classpath konfiguriert werden: – Für Servlet-Klassen gibt es im Serverdokument einen Eintrag. Trennung der Einträge durch Semikolon. – Für Klassen, die entweder von Servlets oder von Domino-Java-Agenten verwendet werden, müssen diese in der Datei notes.ini im Feld JavaUserClasses eingetragen werden. Einträge werden für Windows-Systeme mit Semikolon, für Unix-Systeme durch Doppelpunkt getrennt. Dieses Feld darf nicht mehr als 255 Zeichen haben. – JavaUserClasses kann alternativ (!) im der Datenbank names.nsf in einem Konfigurationsdokument in der Ansicht „Configuration“ --> „Configurations“ konfiguriert werden. Diese Einstellung überschreibt (!) Änderungen in der Datei notes.ini. – Jar-Dateien können in der Domino JVM, ebenso wie auch sonst z.B. für die Sun JVM üblich, als so genannte installed extention installiert werden. Hierfür werden die Jar-Dateien in den lib/ext-Pfad der JVM innerhalb des JavaHome-Verzeichnisses gelegt und von der JVM automatisch von dort geladen. Für eine Standardinstallation unter Windows ist dies z.B. C:\Lotus\Domino\jvm\lib\ext. Servlet-Klassen, die sich nicht im Default Package befinden, müssen in der Datei servlets.properties konfiguriert werden. Die für die lokale Verwendung vorgesehenen Signaturen müssen NotesThread oder die statischen Klassen sinitThread und stermThread verwenden. DIIOPVerbindungen dürfen NotesThread nicht verwenden.
5.4 Troubleshooting • • • •
235
Bei der Verwendung von NotesThread: Sind die Notes-Binaries im Path und die Notes.jar im Classpath? Bei der Verwendung von DIIOP-Verbindungen: Ist die NCSO.jar (R5.x, R6.x) / Notes.jar (R7.x) im Classpath? Einige Signaturen verlangen null-Parameter. Sind diese korrekt als (String) null gecasted? Typische Fehlermeldungen sind: NotesException: User username is not a server (Verwendung der lokalen Internet-Session nicht im Server-Kontext) Reference to createSession is ambiguous (Fehlender Cast auf String eines nullParameters) UnsatisfiedLinkError: NCreateSession (Fehlende Verwendung von Notes Thread) UnsatisfiedLinkError: Can't find library nlsxbe (nlsxbe.dll) in java.library.path (Binaries nicht im PATH)
Gültigkeitsbereiche Domino lädt mehrere Instanzen der JVM, bzw. des ClassLoaders, je nachdem, an welcher Stelle Java-Code verwendet wird. Dies kann zu unerwarteten Ergebnissen und zu SecurityExceptions führen, wenn JarDateien an den verschiedenen möglichen Stellen gleichzeitig angemeldet werden.
• • • • •
• •
Insbesondere sind folgende Eigenschaften zu beachten: Servlets werden in einem eigenen ClassLoader (im HTTP Thread von Domino) geladen. Klassen, die in JavaUserClasses aufgeführt werden, werden über den ClassLoader des Servers geladen. Klassen in einer Jar-Datei, die sowohl in JavaUserClasses, als auch im Servlet Classpath geführt werden, werden in einem gemeinsamen Kontext geladen. Java-basierte Agenten im Domino Designer werden in einem eigenen ClassLoader (Agent Manager) geladen. Klassen, die nur im Servlet Classpath geführt werden, können (während der Entwicklung im laufenden Betrieb auf Dateisystemebene) ausgetauscht werden und werden nach einem Neustart des HTTP neu geladen. Klassen, die in JavaUserClasses geführt werden, bedürfen eines Server-Neustarts, um neu geladen zu werden, nachdem sie ausgetauscht wurden. Klassen, die nur im Designer ausgetauscht werden, werden immer exklusiv neu geladen, wenn der Agent, der diese Klassen verwendet, gestartet wird. Hierfür ist also kein Neustart eines Tasks notwendig.
236 •
•
•
•
5.5
5 Java-Web-Anwendungen @ Domino Ist eine Jar-Datei im Domino Designer in einer Bibliothek (oder in einem Agenten) geladen und durch Java-Code im Designer referenziert und liegt sie gleichzeitig im Dateisystem und wird über die notes.ini-Einstellung JavaUserClasses geladen, so wird der JavaAgent auf die Jar-Datei aus dem File System zurückgreifen, auch wenn sich die Jar-Datei in einer Bibliothek ändert. Der Vorteil dieser Vorgehensweise ist, dass die Klassen nicht für jeden Agentenstart neu geladen werden müssen. Umgekehrt kann z.B. ein Servlet nicht auf eine Jar-Datei in einer Bibliothek im Domino Designer zugreifen. Wird eine Jar-Datei sowohl in JavaUserClasses, als auch im Servlet ClassPath geführt, sind diese Klassen von Agenten im Domino Designer und von Servlets erreichbar. Alternativ können Servlet-Klassen von vorne herein ausschließlich in JavaUserClasses geführt werden. Statische Variablen in Klassen, die lediglich in JavaCode, der im Domino Designer codiert wurde, geführt werden, stehen jeweils nur einer Instanz eines Agenten zur Verfügung und werden nach Beendigung des Agenten wieder aufgeräumt. Im Klartext bedeutet dies, dass sämtliche Variablen (insbesondere die statischen) bei einem Agentenstart erzeugt und danach wieder komplett entfernt werden. Für jeden Start eines Agenten wird also praktisch ein kompletter ClassLoader hochgefahren und wieder beendet. Dies ist bei Performanceüberlegungen zu berücksichtigen. Gleichzeitig stehen statische Variablen, die in Klassen erzeugt wurden, die in JavaUserClasses geführt werden, sowohl Servlets, als auch Java-Agenten aus dem Domino Designer persistent zur Verfügung. Empfehlung: Wenn Sie sowohl Servlets als auch Java-Agenten einsetzen, dann sollten Sie alle Jar-Dateien nur in JavaUserClasses anmelden. Kopieren Sie die Klassen zusätzlich in eine Bibliothek im Domino Designer, die Sie in allen Agenten referenzieren, damit Agenten bei einem erneuten Speichern des Designdokuments auch außerhalb des Serverkontextes compiliert werden können.
Zusammenfassung
Domino-Web-Anwendungen können auf vielfältige Weise realisiert werden. Im Mittelpunkt dieses Kapitels standen Domino-Web-Agenten und Servlets im Domino Servlet Manager, aber auch die Remote-Anbindung von Domino-Servern über DIIOP. Die Notes-Session wurde als Zentrum jeder Domino-Anwendung herauskristallisiert und es wurde aufgezeigt, wie sich die verschiedenen Arten der möglichen Verbindungen und Aufbau von Sessions verhalten und welche Besonderheiten jeweils zu beachten sind. Mit der Einführung ins Single Sign On wurde bereits aufgezeigt, wie Domino-Anwendungen extern auf Basis eines Application Servers wie WebSphere realisiert werden können und durch SSO dennoch mit einer gemeinsamen Anmelde-Realm arbeiten können.
5.5 Zusammenfassung
237
Mit dem Connection Pooling wurden fortgeschrittene Techniken vorgestellt, die es dem Java-Programmierer ermöglichen, Systemressourcen sparsam einzusetzen, indem mehrere Verbindungen über einen TCP Socket gebündelt werden. Im Bereich der Web-Anwendungen wurde noch nicht auf JSP eingegangen, was im Kapitel über die Domtags Tag Library nachgeholt werden wird. Nach diesen weitreichenden Grundlagen geht es nun daran, auf Basis der Notes-Session die Domino-Objekte genauer kennenzulernen, deren Einsatz und Objekt- und Speicherstruktur kennenzulernen.
6
6 Domino-Objekt- und Speichermodell
In diesem Kapitel: Die Hierarchie der Domino-Objekte Übersicht über die Domino-Java-Objekte Bezug von Objekten Basisklassen Sonderklassen AgentBase und AppletBase NotesException
240
6 Domino-Objekt- und Speichermodell
Domino unterscheidet sich wesentlich von vielen Datenbankservern – die zugrunde liegende Datenstuktur aus Dokument und Item, dargestellt durch Masken und Ansichten, wurde bereits ausführlich in den Kapiteln 2.2 bis 2.4 erläutert. Im folgenden Kapitel soll nun die Java-Sicht auf die Datenstruktur und das Objektmodell näher beleuchtet werden, aber auch auf erweiterte Techniken, die bisher noch nicht vertieft wurden, eingegangen werden.
6.1
Objekt- und Datenstruktur
Die Objektstruktur der Domino-Objekte und deren Speicherrepräsentation ist streng hierarchisch geordnet und ebenso sind es die Java-Repräsentationen. Rufen Sie sich den Überblick aus Abb. 2-14 in Kapitel 2.4 ins Gedächtnis. Dort wird die Hierarchisierung der Domino-Objekte dargestellt, wobei zwei wichtige Gruppen von Abhängigkeiten herauszustellen sind: die Beziehung zwischen der Session, der Datenbank, dem View und dem Dokument einerseits und dem Aufbau eines Dokuments als („lose“) Sammlung von Items andrerseits. Ein Document lässt sich auf einem der folgenden Wege beziehen: • • • • •
Die Datenbank selbst ist das Speicherobjekt für alle Domino-Elemente und wird in der Notes Storage Facility NSF, bzw. optional seit Domino R7 in einer DB2-Datenbank im NSFDB2-Format gespeichert. Dokumente können entweder direkt aus der Datenbank über getDocumentByID oder getDocumentByUNID (Kapitel 7.1.4.1) bezogen werden, oder indirekt über einen View (Kapitel 10), eine ViewEntryCollection oder eine DocumentCollection (Kapitel 10.7f.) referenziert werden, wobei DocumentCollection und ViewEntryCollection über verschiedene Mechanismen der Suche (Kapitel 15) generiert werden können. Das Document selbst besteht (neben diversen Eigenschaften, die ein Dokument haben kann) aus Items, RichTextItems oder Dateianhängen, so genannten EmbeddedObjects (Dateianhänge, Bilder, OLE-Objekte). • • •
Ein Item enthält die eigentlichen Informationen des Dokuments. Ein Dokument kann beliebig viele Items haben37. Ein Item wiederum enthält Daten unterschiedlichster DatenTypen, vom String bis hin zur Repräsentation von Objekten beliebiger Klassen über die Mechanismen von Serializable. Ein Item hat einen Namen und einen Wert, wobei es sowohl mehrere Items mit gleichem Namen38 geben kann, als auch ein Item mehrere Werte haben kann. • • • • •
Item -> Object (Objekt einer beliebigen Klasse, die Serializable imple-
•
Item -> Vector aus Integer, Double, String, DateTime, DateTimeRange oder Object
mentiert) mentiert) als Byte Array
Ein RichTextItem wiederum nimmt eine Sonderstellung im Dokument ein, da es wiederum, wie bereits in Kapitel 2.2.4.8 beschrieben, alle Elemente enthalten kann, die eine Maske enthalten kann. Programmatisch erreichen Sie jedoch nicht alle diese Objekte, sondern bis Version 5 nur Text, RichTextStyle (für die Manipulation von Fontattributen und Tabulatoren), Links und EmbeddedObjects und seit Version 6.5 auch RichTextNavigator, RichTextRange, und RichTextTable, die erweiterte Möglichkeiten zur programmatischen Gestaltung von RichText liefern.
z.B. Eingebettete Ansichten oder Applets. Basis und Grundlage für den Bezug aller Objekte ist die Notes-Session, die, wie in den vorigen beiden Kapiteln ausführlich besprochen, erzeugt wird. Für das Verständnis und die fehlerfreie Verwendung der Java-Klassen für Domino ist es wichtig, diese hierarchische Struktur einzuhalten.
37 38
Nur beschränkt durch die Speicherlimits (s. Kap. 20.1). Es wird dringend geraten, dies zu vermeiden, da sich solche Items recht schwer handhaben lassen.
242
6 Domino-Objekt- und Speichermodell
Ein Domino-Java-Objekt, also eine Datenbank oder ein Dokument, das über eine Session, die lokal bzw. auf dem Server an einen NotesThread gebunden ist, ist immer nur in diesem Thread und in dieser Session gültig, d.h. die sich hieraus ergebende Objektabhängigkeit ist immer beim Auf- und Abbau der Objekte zu beachten. Eine Datenbankreferenz, also ein Java-Objekt der Klasse Database, und alle über dieses bezogenen Domino-Java-Objekte, also z.B. ein über diese Database bezogenes Dokument oder Ansicht, ist immer nur dort (in der Session und dem Thread) und so lange (bis zum Recycling der Database) gültig, wie die Database gültig ist. In Multithreaded-Java-Anwendungen muss daher im Normalfall (s. jedoch Kap. 13.3.7) für jeden Java Thread eine eigene Notes-Session, bzw. und/oder NotesThread geöffnet werden. Beachten Sie, dass diese zwingende Hierarchie sich auf die Java-Objekte, aber nicht etwa auf die Notes Storage Facility, also die eigentliche Notes-Datenbank bezieht. Selbstverständlich kann ein Dokument, eine Ansicht oder eine Datenbank von verschiedenen Threads geöffnet werden, aber die jeweiligen Java-Objekte müssen jeweils innerhalb ihres NotesThreads unabhängig voneinander instanziert werden und können nicht (als Java-Objekt) zwischen den Threads geteilt werden. Umgekehrt spiegelt sich dieses Verhalten in den Methoden der Domino-Java-Objekte wieder. So hat das Item eine Methode getParent, die das Document liefert, über das das Java-Objekt Item bezogen wurde und in dem sich das Item befindet. Ein Document besitzt die Methode getParentDatabase, die das Java-Objekt Database liefert, über das das Dokument bezogen wurde und das die Datenbank referenziert, in der sich das Dokument befindet. Falls ein Dokument über einen View bezogen wurde, lierfert die Methode getParentView die Ansicht, über die das Dokument bezogen wurde. Folgerichtig liefert getParent in View ebenfalls das Domino-Java-Objekt der Datenbank, in der sich die Ansicht befindet. Schließlich wird über getParent in Database die Session geliefert, über die das Java-Objekt der Klasse Database instanziert wurde. Umgekehrt folgt automatisch, da eine Session immer an einen bestimmten, die Session aufbauenden Benutzer gebunden ist, dass auch alle abhängigen Objekte diese Benutzerinformationen tragen (über den Bezug zur Session). Anders formuliert: Jedes Java-Objekt für Domino ist benutzerabhängig! Dieser Hierarchie unterliegen alle Domino-Java-Klassen. Welche Klassen stehen nun zur Verfügung? Am wichtigsten sind sicherlich die Klassen Session, Database, View und Document. Ein Session-Objekt wird wie bereits besprochen bezogen. Aus der Session lassen sich nun (fast) alle benötigten Objekte direkt oder indirekt ableiten.
6.1.1 • •
Objekte, die über Session bezogen werden können
Database – über getDatabase Einfache unabhängige Stil- und Eigenschafts-Objekte
6.1.2 Objekte, die über Database bezogen werden können
•
•
•
Über die Session können Stil-, Zeit- und einige Objekte, die Eigenschaften repräsentieren, bezogen werden. Hierzu gehört zum Beispiel das Objekt Registration, das Eigenschaften und Methoden rund um die User-ID liefert. Oder auch Notes-DateTime-Objekte und andere Objekte, die alle unabhängig von anderen Objekten erzeugt werden können (s. Kap. 12.2). Objekte, die komplexe Service-Anwendungen repräsentieren Domino stellt auch im Java-Umfeld neben den einfachen Domino-Objekten, Objekte zur Verfügung, die Anwendungen repräsentieren, mit denen komplexe Aufgaben erfüllt werden können. Hierzu gehört zum Beispiel der DXLExporter und DXLImporter, mit denen alle Domino-Objekte in XML konvertiert werden können oder z.B. die Klasse Log, mit der spezielle Log-Datenbanken mit Logging-Informationen gefüllt werden können. Ausführen verschiedener Befehle Die Session stellt etliche Befehle zur Verfügung, die von keinen dritten Objekten abhängen. Hierzu gehört z.B. das Evaluieren von @Formeln oder auch das Ausführen von Serverkonsolenbefehlen. Verschiedene Eigenschaften In der Session werden verschiedenste Variablen, insbesondere zum Server (mit dem die Session verbunden ist) und zum Benutzer zur Verfügung gestellt. Hierzu gehört zum Beispiel der Name des angemeldeten Benutzers.
6.1.2 •
•
•
Objekte, die über Database bezogen werden können
Document und DocumentCollection Die Database kann Dokumente direkt über deren IDs oder indirekt über verschiedene Methoden zur Suche liefern. Außerdem stellt sie verschiedene DocumentCollections zur Verfügung, die z.B. alle oder alle unverarbeiteten Dokumente repräsentieren. Objekte, die aus Designelementen abgeleitet werden In einer Notes-Datenbank sind verschiedenste Designelemente definiert, deren Java-Repräsentation direkt aus dem Database-Objekt bezogen werden können. Hierzu gehören die Java-Objekte Agent, Form, Outline und View. Zusätzlich liefert NoteCollection alle Designelemente der Datenbank. Datenbankeigenschaften Über das Datenbankobjekt können Java-Objekte bezogen werden, die Eigenschaften wie Sicherheitseinstellungen in ACL oder Eigenschaften der Replikation in Replication repräsentieren.
6.1.3 •
243
Objekte, die über View bezogen werden können
Document und DocumentCollection Auch über den View können Dokumente direkt oder indirekt über Suchfunktionen bezogen werden. Ergebnis einer Suche kann ein einzelnes Document oder eine DocumentCollection sein.
244 •
6 Domino-Objekt- und Speichermodell Objekte des View Der View selbst liefert verschiedene Objekte, mit denen der View beschrieben oder durchsucht werden kann. ViewColumn liefert eine Spalte des Views, ein ViewEntry beschreibt eine Zeile eines Views. Eine ViewEntryCollection ist eine Collection von ViewEntry. Über einen ViewNavigator können insbesondere kategorisierte Views durchsucht werden.
6.1.4 • • •
Objekte in Document
Daten Item und RichTextItem Binär-Objekte EmbeddedObject und MimeEntity Eigenschaften DateTime-Informationen über Erstell- oder Änderungsdatum, aber auch über Ersteller und Revisionen des Dokuments.
6.1.5
Domino-Objektklassen
Tabelle 6-1 zeigt alle für den Programmierer zur Verfügung stehenden Klassen des Domino-Java-API in einer Übersicht. Ein vollständiges Methodenverzeichnis befindet sich im Index, jeweils unter dem Namen der zugehörigen Klasse. In der Tabelle wird zunächst zwischen zwei grundsätzlichen Arten von Klassen unterschieden und zwar zwischen Basisklassen und einigen besonderen Hilfklassen auf der einen Seite (Markierung in der ersten Spalte „B“ der Tabelle) und den eigentlichen Domino-Objekten auf der anderen Seite. Die Klassen der zweiten Gruppe erweitern alle die Basisklasse Base (Markierung in der zweiten Spalte „E“) und können alle direkt oder indirekt entsprechend dem oben beschriebenen Schema aus der Session bezogen werden. Die eigentlichen Domino-Objektklassen erweitern Base und tragen daher in der Tabelle in der zweiten Spalte „E“ eine entsprechende Markierung. Für diese Klassen bietet die Tabelle eine Übersicht, wie sie jeweils bezogen werden können und welche Objekte (auf direktem Wege) aus diesen Klassen erzeugt oder bezogen werden können. Trägt eine Klasse in der Tabelle eine Markierung in einer der Spalten mit den Titeln „S“, „V“, „DB“ oder „D“, wird hierdurch gekennzeichnet, dass Objekte dieser Klassen jeweils aus einer Session, einem View, einer Database oder einem Document erzeugt oder bezogen werden können. Hierdurch erhalten Sie einen guten Überblick über die Objekthierarchie der Domino-Objekte. In der Spalte „Zusätzlich zu beziehen über“ wird für jede der Klassen vermerkt, ob es noch weitere Klassen gibt, über die die Klasse bezogen oder erzeugt werden kann. In der letzten Spalte mit der Überschrift „Liefert Objekte der Klassen (auch optional)“ werden die Klassen von Objekten gelistet, die aus einer Klasse erzeugt oder bezogen werden können.
6.1.5 Domino-Objektklassen
245
Tabelle 6-1 Übersicht über die Domino-Klassen Domino-Klasse
B E
ACL
x
ACLEntry
x
AdministrationProcess Agent
x
AgentBase
S V D D Zusätzlich zu be- Liefert Objekte der KlasB ziehen über sen (auch optional) • ACL • •
Tabelle 6-1 Übersicht über die Domino-Klassen Domino-Klasse
B E
S V D D Zusätzlich zu be- Liefert Objekte der KlasB ziehen über sen (auch optional) ViewEntryCollection, ViewNavigator
Document
ViewEntry
x
•
ViewEntryCollection ViewNavigator
x
•
ViewEntry
x
•
ViewEntry
XSLTResultTarget
x
B E S V DB D
Basisklasse oder Sonderaufgabe Erweitert Base Enthalten in oder zu beziehen über Session Enthalten in oder zu beziehen über View Enthalten in oder zu beziehen über Database Enthalten in oder zu beziehen über Document
(1)
Erweitert Item
Die Tabelle wird für das Beispiel „Database“ wie folgt gelesen: In der Zeile „Database“ findet sich eine Markierung in der Spalte „E“, d.h. die Klasse Database erweitert die Basisklasse Base. Die Spalte „S“ trägt eine Markierung und kennzeichnet, dass ein Objekt der Klasse Database über eine Session bezogen werden kann (z.B. über getDatabase in Session). Die Spalte „Zusätzlich zu beziehen über“ enthält die Einträge „AgentContext“ und „DbDirectory“, d.h. ein Objekt der Klasse Database kann auch über eine dieser beiden Klassen erzeugt werden, z.B. über getCurrentDatabase in AgentContext oder openDatabaseByReplicaID oder openDatabase in DbDirectory. Die letzte Spalte liefert nun Informationen über die Objekte, die (direkt) über ein Database-Objekt erhalten werden können, dies sind ACL, Agent, DateTime, Document, DocumentCollection, Form, Outline, Replication und View, so kann z.B. ein Objekt der Klasse DocumentCollection über getAllDocuments bezogen werden, wobei eine derartige DocumentCollection alle Dokumente der Datenbank enthält. Session, Database, View und Dokument sind die wichtigen Objekte in der Domino-Objekthierarchie. Nachdem der Session (und dem eng mit der Session verbundenen NotesThread) bereits die Kapitel 4 und 5 gewidmet waren, erhalten nun die Klassen Database, View und Document in diesem Kapitel ihren eigenen Platz, wobei zunächst noch kurz auf die eingangs erwähnten und in der Tabelle mit „B“ markierten Base- und Sonderklassen eingegangen wird.
248
6 Domino-Objekt- und Speichermodell
6.2
Basis- und Sonderklassen
Die Basis- und Sonderklassen der Domino-Objektstruktur stehen ausserhalb der Domino-Objekthierarchie. Im Einzelnen sind dies folgende Klassen, mit den Aufgaben: •
•
•
•
•
•
•
AgentBase Die AgentBase ist die Basisklasse aller Java-Agenten, die als Agent im Domino Designer entwickelt werden. Sie stellt den Agenten den so genannten AgentContext zur Verfügung über den Sicherheits- und Kontextinformationen bezogen werden können. Hierzu gehört der Name des aktuellen Benutzers der Session, der effektive Benutzer unter dessen Rechten die Anwendung ausgeführt wird und der so genannte DocumentContext, der das Kontextdokument liefert. AppletBase und NotesAppletContext Die AppletBase ist die Basisklasse aller Domino-Java-Applets und erweitert java.applet.Applet. Jedes Applet, das Domino-Objekte verwendet, muss die AppletBase erweitern. Die AppletBase stellt Methoden zum Bezug einer Notes-Session zur Verfügung und kann einen NotesAppletContext zur Verfügung stellen. Der NotesAppletContext ist nur gültig, wenn das Applet innerhalb eines Domino-Designelements im NotesClient angezeigt wird und liefert Server, Datenbank und das Dokument, in dem sich das Applet befindet, jeweils sofern verfügbar. Nähere Informationen finden Sie in Kapitel 4.9. Base Die Klasse Base ist die Basisklasse aller regulären Domino-Objekte. Alle diese Objekte erweitern Base. Die Klasse Base selbst sollte nicht direkt benutzt werden. JAppletBase Die Klasse JAppletBase hat die gleiche Funktion wie AppletBase, erweitert selbst aber javax.swing.JApplet. Sie stellt die gleichen Methoden wie AppletBase zur Verfügung und wird bei der Entwicklung von Swing-basierten Domino-Applets verwendet. NotesError Die Klasse NotesError definiert Konstanten, die die Fehlernummern aus NotesException beschreiben. NotesException Die Klasse NotesException erweitert java.lang.Exception und wird für das Exception Handling in Notes verwendet. Notes-Methoden werfen NotesExceptions, sofern sie überhaupt eine Exception werfen. NotesFactory Über die Klasse NotesFactory kann die Notes-Session bezogen werden. Sie bietet zusätzlich unterstützende Methoden zum Bezug des IOR und für die Verwendung des Connection Poolings. Nähere Informationen finden Sie in Kapitel 5.2.3ff. und einen Überblick in Kapitel 5.3.7.
6.2.1 AgentBase •
249
NotesThread NotesThread erweitert java.lang.Thread und ist die Basis lokaler DominoSessions. NotesThread wird immer dann benötigt, wenn Notes-Sessions auf
•
Basis einer lokalen Notes- oder Domino-Installation aufgebaut werden sollen. Nähere Informationen finden Sie in Kapitel 4.3 ff. XSLTResultTarget Seit Version 5 bietet Domino reichhaltige Funktionalitäten zur Verarbeitung von XML und XSL. Mit transformXML in EmbeddedObject, Item, MIMEEntity kann XML über ein XSL Stylesheet transformiert werden. Das Ergebnis ist ein XSLTResultTarget. Diese Klasse ist ein Wrapper der Klasse com.lotus.xsl. XSLTResultTarget, die wiederum org.apache.xalan.xslt.XSLTResultTarget erweitert. Der Wrapper und das Extending sind absolut geradlinig ohne Nebenwirkungen implementiert, so dass die Konstruktoren und Methoden äquivalent zu denen aus org.apache.xalan.xslt.XSLTResultTarget verwendet werden können. Seit Domino R7 ist XSLTResultTarget eine eingenständige Klasse und basiert (intern) im Wesentlichen auf javax.xml.transform, da seit Java 1.4.x die XML- und XSL-Klassen Bestandteil des Standard-JRE sind39.
6.2.1
AgentBase
AgentBase ist deklariert als public class AgentBase extends NotesThread, wobei NotesThread seinerseits java.lang.Thread erweitert, hat den Constructor AgentBase()und enthält die Methoden: •
void dbgMsg(String s)
Wurde das Debugging für einen Agenten über setDebug (true) aktiviert, so können mit dbgMsg40 Nachrichten auf die Serverkonsole bzw. die JavaDebugKonsole im NotesClient geschrieben werden. dbgMsg gibt seine Meldungen auf System.out aus. •
•
• 39
40
void dbgMsg(String s, PrintStream printstream) dbgMsg (String, PrintStream) arbeitet wie dbgMsg (String), gibt jedoch seine Meldungen auf den PrintStream aus uns stellt den Meldungen den String "dbg: " voran. void dbgMsg(String s, PrintWriter printwriter) Wie dbgMsg (String, PrintStream), jedoch Ausgabe auf einen PrintWriter PrintWriter getAgentOutput() Dementsprechend wurden aus der Domino 7 Core-Installation auch die Bibliotheken xml4j.jar und lotusxsl.jar entfernt. Dies hat leider Auswirkungen auf bestehende Installationen für den Fall, dass Methoden aus xml4j direkt verwendet wurden, da nun die neuen Parser verwendet werden müssen. Die Methoden dbgMsg (String), dbgMsg (String, PrintStream), dbgMsg (String, PrintWriter) und setDebug (boolean) sind nicht von Lotus dokumentiert und können daher unangekündigten Änderungen unterliegen.
250
6 Domino-Objekt- und Speichermodell Jeder Agent hat einen Ausgabe-Stream. Für WebAgenten ist dies die Ausgabe auf den Browser, für Agenen im NotesClient die JavaDebugConsole, für Server-basierte Agenten die Serverkonsole.
•
OutputStream getAgentOutputStream()
Neu seit Domino R7. Liefert den Agenten-Ausgabe-Stream als java.io. OutputStream. Vgl. getAgentOutput(). •
•
Session getSession() Die Session, in der der Agent geöffnet wurde. Dies ist entweder die Session des aktiven Benutzers oder die Session des Servers. boolean isRestricted()
Domino-Java-Agenten, die im Domino Designer erstellt werden, haben (seit Version 6) im Kontext-Menü die Einstellung „Set runtime security level (1 = most secure)“. Hier kann für Benutzer mit uneingeschränkten Rechten festgelegt werden, ob der Agent dennoch mit eingeschränkten Rechten arbeitet. isRestricted ()41 liefert true, falls diese Einstellung den Wert 1 hat. Für die Werte 2 und 3 („allow restricted operations“ und „allow restricted operations with full administrative rights“) liefert die Methode false. Nähere Informationen finden Sie in Kapitel 4.16. •
void NotesMain()
Die Methode NotesMain ist der Einstiegspunkt eines Domino-Java-Agenten (s. Kap. 4.10). •
final void runNotes()
Interne Methode des Java-Agenten, die letztendlich NotesMain aufruft. Sollte nicht direkt verwendet werden. •
void setDebug(boolean flag)
Legt fest, ob dbgMsg seine Ausgaben tatsächlich auf System.out liefern soll. • •
void setTrace(boolean flag) Wrapper für Runtime.getRuntime().traceMethodCalls(boolean) final void startup(AgentInfo agentinfo) Interner Wrapper. Ruft letztendlich java.lang.Thread.start() auf. Sollte
nicht direkt verwendet werden.
6.2.2
AppletBase und JAppletBase
AppletBase ist deklariert als public class AppletBase extends java.applet.Applet implements DominoAppletBase und hat den Construktor public AppletBase(). JAppletBase ist definiert als public class JAppletBase extends JApplet implements DominoAppletBase und hat den Construktor public JAppletBase(). Das Interface DominoAppletBase ist Basis für AppletBase und JAppletBase und deklariert die in beiden Basisklassen für Domino-Applets benötigten Methoden. Sowohl AppletBase als auch JAppletBase implementieren alle
41
Die Methode isRestricted is nicht von Lotus dokumentiert.
6.2.2 AppletBase und JAppletBase
251
Methoden von DominoAppletBase und überschreiben zusätzlich die Methoden init, start, stop und destroy von java.applet.Applet, bzw. von javax.swing.JApplet AppletBase und JAppletBase haben folgende Methoden: • •
•
void closeSession(Session session) Schließt eine geöffnete Notes-Session. final void destroy() Überschreibt java.applet.Applet.destroy() und ruft (letztendlich) notesAppletDestroy() auf. Soll nicht direkt verwendet werden. Stattdessen soll in einer Anwendung notesAppletDestroy verwendet werden. NotesAppletContext getContext(Session session) Liefert den AppletContext, der Informationen über die Notes-Session liefert
(s.o.) • • •
•
Session getSession() Liefert die Notes-Session (s. Kap. 4.9). Session getSession(String s, String s1) Liefert die Notes-Session (s. Kap. 4.9). final void init() Überschreibt java.applet.Applet.init() und ruft notesAppletInit() auf. Soll nicht direkt verwendet werden. Stattdessen soll in einer Anwendung notesAppletInit verwendet werden. boolean isNotesLocal() Liefert die Information, ob die zugrunde liegende Notes-Session lokal aufge-
baut wurde. Dies ist (in der Regel) der Fall, wenn das Applet im NotesClient geöffnet wurde (s. Kap. 4.9). • •
Soll Aufräumarbeiten erledigen, wenn es vom eigentlichen Applet überschrieben wird. Die grundsätzliche Verwendungsweise ist entsprechend der Methode destroy in Applet vorgesehen. •
void notesAppletInit()
Soll Initialisierungsarbeiten erledigen, wenn es vom eigentlichen Applet überschrieben wird. Die grundsätzliche Verwendungsweise ist entsprechend der Methode init in Applet vorgesehen. •
void notesAppletStart()
Soll Arbeiten bei jedem Start des Applets erledigen, wenn es vom eigentlichen Applet überschrieben wird. Die grundsätzliche Verwendungsweise ist entsprechend der Methode start in Applet vorgesehen. •
void notesAppletStop()
Soll Arbeiten bei jedem Anhalten des Applets erledigen, wenn es vom eigentlichen Applet überschrieben wird. Die grundsätzliche Verwendungsweise ist entsprechend der Methode stop in Applet vorgesehen. • •
6 Domino-Objekt- und Speichermodell Siehe getSession (String, String). final void start() Überschreibt java.applet.Applet.start() und ruft notesAppletStart() auf. Soll nicht direkt verwendet werden. Stattdessen soll in einer Anwendung notesAppletStart verwendet werden. final void stop() Überschreibt java.applet.Applet.stop() und ruft notesAppletStop() auf. Soll nicht direkt verwendet werden. Stattdessen soll in einer Anwendung notesAppletStop verwendet werden.
6.2.3
NotesException und NotesError
Die NotesException ist deklariert als public final class NotesException extends org.omg.CORBA.UserException. Sie ist die einzige Exception, die innerhalb der Domino-Java-Objekte verwendet wird und speichert daher zusätzlich zu den Fehlertexten spezielle für Notes-spezifische Fehlernummern, die im Feld id in NotesException gespeichert werden. Mögliche Werte für id werden über Konstanten in NotesError definiert. Dort findet sich auch die Methode String getErrorString(int i) mit der sich die Fehler IDs in den zugehörigen Text umwandeln lassen. In Version 5 von Lotus Domino wurde über printStackTrace in NotesException lediglich der Trace, jedoch nicht die Fehlernummer ausgegeben. Daher musste zusätzlich immer noch die ID in (z.B.) den catch Clauses mit ausgegeben werden, um eine vollständige Anzeige des Fehlers zu erhalten. Seit Version 6 wird die Notes-Fehlernummer automatisch bei Verwendung von printStackTrace ausgegeben. Zusätzlich steht seit Version 6 ein Feld mit einer internen Exception zur Verfügung, falls eine solche vorliegt. Ansonsten ist dieses Feld null. NotesException hat drei Construktoren, die vom Programmierer normal verwendet und mit throw new NotesException geworfen werden können: •
NotesException()
NotesException, bei der die internen Felder id, internal und text (für die Message aus toString) null bzw. 0 sind. •
NotesException(int i, String s)
Neue NotesException mit Fehlernummer i und Text s. •
NotesException(int i, String s, Throwable throwable)
Neue NotesException mit Fehlernummer i, Text s und interner Exception throwable. Ungewöhnlich ist, dass die internen Felder id für die Fehlernummer, text für die Exception Message und internal für die interne Exception als Public fields deklariert sind und direkt ohne getter angesprochen werden können. Ist e eine NotesException, werden diese Parameter über e.id als int, e.text als String und e.internal als Throwable angesprochen. Die Methode to-
6.3 Zusammenfassung
253
String liefert den Messagetext text oder die id, falls keine Message vorliegt und
optional zusätzlich dieselben Informationen über die interne Fehlermeldung, sofern eine vorliegt.
6.3
Zusammenfassung
Domino-Java-Objekte sind streng hierarchisch angelegt. Einerseits erweitern sämtliche Domino-Java-Objekte die Basisklasse Base. Andrerseits sind alle Objekte von einer Domino-Session abhängig. Bei der Erzeugung von Objekten wird daher immer direkt oder indirekt von einer Session ausgegangen. Hieraus hervorgehend können dann Datenbanken, Ansichten, aber auch Masken und Agenten geöffnet werden. Die Dokumente der Datenbanken stehen entweder als Container von Item-Objekten zur Verfügung oder werden selbst in DocumentCollections gesammelt, während Resultsets gleichermaßen in ViewEntryCollections abgebildet werden können. In den folgenden Kapiteln werden die Zusammenhänge zwischen diesen Objekten und die Arbeit mit ihnen und den Daten- und Speicherobjekten ausführlich diskutiert. Weitere wichtige Objekte und Methoden von Ansichtenindizes über Agenten, von Datenbanken und Designelementen bis hin zum Document- und Designlocking und der komplexen Verarbeitung von RichText werden dann in jeweils eigenen Kapiteln die beteiligten Objekte und Methoden auch anhand von Beispielen analysiert. Während dieses Kapitel nur eine Einführung und Übersicht bietet, wird mit den folgenden Kapiteln das gesamte Rüstzeug zur Verfügung stehen, das als Basis benötigt wird, um komplexe Domino- und Notes-Anwendungen in Java zu programmieren.
7
7 Document
In diesem Kapitel: Document und Item als Abbild der Datenstruktur von Domino Dokumenten-Lifecycle Profil- und Antwortdokumente Dokumenthierarchien Encryption und Signing Attachments und OLE-Objekte Document Locking
256
7 Document
7.1
Document und Item
Immer wieder zeigt sich, dass Datenbankprogrammierer, kommen sie das erste Mal mit Domino-Java-Objekten in Berührung, Einstiegshürden überwinden müssen, die die Objektstruktur mit sich bringt. Dies liegt an der Struktur der losen Schemata, auf denen das lotus.domino. Document basiert. Letzendlich ist das Document eine leere, flexible Hülle, in der Werte oder gar Objekte beliebig in Anzahl und Art gespeichert werden können. Daten sind nicht, wie z.B. in SQL-Datenbanken, daran gebunden, über Ansichten oder Tabellenabfragen bezogen zu werden (obwohl dies auch möglich ist) und unterliegen auch nicht der Datenstruktur des zwingenden Schemas z.B. einer Tabelle. Andrerseits wird (in der Regel) dennoch ein konkretes und konsistentes Schema erreicht, indem Dokumente, sofern sie durch einen Benutzer interaktiv angelegt werden, auf der Basis von Masken erstellt werden, die ein erweiterbares Schema erzeugen. Am ehesten lassen sich Dokumente noch mit JavaBeans vergleichen. Statt der getter- und setter-Methoden verwendet das Document die Methoden getItemValue und replaceItemValue (und andere), über die Name-Value-Paare, ähnlich einem Hashtable, in den Items (also den Speichereinheiten für einzelne Werte) gespeichert werden können. Items sind am ehesten mit den Feldern einer JavaBean vergleichbar. Der Unterschied zwischen einem Dokument und einer JavaBean besteht darin, dass ein Document nicht programmatisch erweitert werden muss, wenn neue Werte oder Werte eines bisher noch nicht verwendeten Datentyps gespeichert werden sollen, sondern es können jederzeit einem Dokument weitere Items hinzugefügt werden. Interessant ist in diesem Zusammenhang eine Entwicklung an einer anderen Stelle der Java-Programmierung. In der Projektentwicklung für die Datenbankschicht relationaler Datenbanken erfreuen sich im Java-Umfeld O/R Frameworks zum Mapping zwischen der relationalen Datenbankschicht und der objektorientierten JavaSchicht zunehmender Verbreitung42. Auch wenn sich die Konzepte von Database, View und Document nicht direkt mit solchen Frameworks vergleichen lassen, so ist doch die Idee der objektorientierten Datenbankverarbeitung sehr ähnlich, so dass sich ein Programmierer, der im O/R Mapping Erfahrungen mitbringt, sicher leicht in die Domino-Java-Klassen einarbeiten wird. Ein Document ist aber mehr als nur ein Container einfacher Datentypen. Das Konzept der Embedded Objects und des RichTextes, in denen Dateianhänge bis hin zu OLE-Objekten gespeichert werden können, geht weit über das der JavaBean hinaus und erweitert das Document zu einem universellen Container für fast jeden denkbaren Datentyp. Das Konzept der Items ist übrigens dem Konzept des Node in XML-Dokumenten sehr ähnlich, daher können Notes-Dokumente nahtlos in XML überführt werden (s. Kap. 17.7). Es bestehen folgende Möglichkeiten ein Dokument zu beziehen oder zu erzeugen: 42
Das zur Zeit verbreitetste Framework in diesem Bereich aus der OpenSource-Gemeinde ist sicherlich Hibernate [Hibernate, URL]. Als einführende Literatur sei das Hibernate „In Action“ Buch aus dem Mannings Verlag empfohlen. [Hibernate, In Action]
7.1.1 Mit Items arbeiten
• •
• •
257
Direkt über die Datenbank über die Universal ID oder NoteID des Dokuments, bzw. durch Neuerstellen eines Dokuments (s. Kap. 9) Über einen View der Datenbank, indem entweder über eine Suche ein einzelnes Dokument oder mehrere über eine DocumentCollection oder eine ViewEntryCollection bezogen werden (s. Kap. 10 und 15). Über ein Newsletter-Objekt, das Verweise zu Dokumenten sammelt, um diese per E-Mail versenden zu können. Für einige Sonderfälle direkt aus Objekten (z.B. das Kontextdokument in DocumentContext)
Grundsätzlich gibt es vier Arten von Dokumenten, die aber alle über das Javaobjekt Document angesprochen werden: „Normale“ Dokumente, die als Datencontainer in der Datenbank dienen, Profildokumente, die eine spezielle Speicherbehandlung erfahren und dort zeitsparend (aber „sticky“) verwaltet werden (s. Kap. 7.2), Antwortdokumente, die sich dadurch auszeichnen, dass sie immer von einem übergeordneten Dokument abhängen und einen Querverweis zu diesem Parent-Document tragen (s. Kap. 7.3)und schließlich Konfliktdokumente, die beim Speichern oder bei der Replikation entstehen, wenn entweder zwei Benutzer gleichzeitig ein Dokument öffnen, verändern und dann zu speichern versuchen, oder bei der Replikation, wenn ein Dokument auf zwei verschiedenen Repliken verändert wurde und diese beiden Repliken nun synchronisiert werden müssen (s. Kap. 2.3 ff). Um Speicherkonflikte zu vermeiden gibt es seit Domino 6 den Mechanismus des Document Locking, durch den Dokumente exklusiv für einen Benutzer oder Prozess gesperrt werden können (s. Kap. 7.7).
7.1.1
Mit Items arbeiten
Items sind die Container innerhalb eines Document, in denen die eigentlichen Daten gespeichert werden. Sie sind vergleichbar mit Nodes in einem XML-Dokument. Während im Notes Client durch den Benutzer die Daten in Felder eingegeben und dann in Items gespeichert werden, sind die Items als solche unabhängig von einer Maske und können über die Java-Backend-Programmierung immer gelesen oder geschrieben werden. Für Items wird einerseits zwischen verschiedenen Datentypen unterschieden und andrerseits, ob ein Item einen diskreten Wert enthält oder ob es sich um ein MultiValue Item handelt, das mehrere Werte eines solchen Datentyps enthält. Bei MultiValue Items wird in Domino auch von Listen gesprochen. Multi-Value Items werden als Vector (der entsprechenden Datentypen) ausgelesen. Items des Datentyps DateTime machen hier eine Ausnahme, da sie immer als Vector gespeichert und gelesen werden. Items können entweder als Java-Objekt behandelt werden, indem ein einzelnes Item über getFirstItem (String name) in Document bezogen wird oder alle Items eines Document über getItems () bezogen werden, oder ihr Wert kann direkt über verschiedene Methoden getItemValue... () in Document gelesen
258
7 Document
werden. Sollte es in einem Document mehrere Items gleichen Namens geben, liefert getFirstItem nur das erste Item und getItemValue... nur den Wert des ersten Items diesen Namens. Ein Item kann nicht ohne das Document, in dem es sich befindet, existieren, d.h. die Klasse Item kann auch instanziert werden und innerhalb eines Dokuments neu erstellt werden. Folgerichtig hat Item die Methode getParent (), die das beinhaltende Document liefert. Neu erstellt wird ein Item innerhalb eines Document durch Item replaceItemValue(String name, Object value) in Document oder durch Item appendItemValue(String name, Object value), Item appendItemValue(String name, int value) oder Item appendItemValue(String name, double value), wobei alle ein neues Item im Document anlegen und als Item-Objekt zurückgeben, sofern es noch nicht existiert, replaceItemValue jedoch ein möglicherweise vorhandenes Item gleichen Namens ersetzt, wogegen appendItemValue ein weiteres Item gleichen Namens anlegt. Dies wird nicht empfohlen, da von mehreren Items gleichen Namens, wie eingangs erwähnt, nur über getFirstItem das erste aus einem Dokument gelesen werden kann und folglich zusätzliche Items nur schwer handhabbar sind. Ein anderer Weg, ein Item neu zu erstellen, ist die Methode Item copyItemToDocument (Document targetDoc) in Item, wodurch ein Item in ein Zieldokument kopiert wird und Item copyItem(Item sourceItem) in Document, wodurch ein Item in das aufrufende Document kopiert wird. Beide Methoden können einen zusätzlichen String-Parameter haben, durch den der Name des neuen Items spezifiziert wird. Die Methode void copyAllItems(Document targetDocument, boolean replaceItemIfExists) in Document kopiert alle Items des Documents, über das diese Methode aufgerufen wird, in das im Parameter targetDocument spezifizierte Zieldokument. Hat replaceItemIfExists den Wert true, werden bereits vorhandene Items im Zieldokument überschrieben, andernfalls werden die Werte an die vorhandenen Items angehängt. Wird mit der Methode copyItemtoDocument ein RichTextItem kopiert, werden EmbeddedObjects oder Anhänge im Item nicht mitkopiert. Soll der Wert eines einzelnen Items für viele Dokumente gleichzeitig geändert werden, so sei hier als Vorgriff auf Kapitel 10.7 darauf hingewiesen, dass mit der Methode void stampAll(String name, Object value) in DocumentCollection und ViewEntryCollection der Wert des Items mit dem Namen name für alle Dokumente der Collection durch den Wert value ersetzt wird, wie es durch replaceItemValue erreicht würde. Diese Methode ist einem Loop über alle Dokumente aus Performancegründen vorzuziehen. Die so veränderten Dokumente müssen nach der Operation nicht gespeichert werden, dies geschieht implizit in stampAll. Änderungen in bereits instanzierten Document-Objekten müssen vor der Verwendung von stampAll bereits gespeichert sein, um Versions- und Speicherkonflikte zu vermeiden. Umgekehrt werden solche bereits geladenen Java-Objekte durch stampAll nicht aktualisiert und müssen neu geladen werden.
7.1.2 Datentypen handhaben 7.1.2
259
Datentypen handhaben
Der einfachste und direkteste Weg, Daten, Items und Dokumente zu handhaben sind die Methoden getItemValue... (String name) in Document. Diese Methoden sind äquivalent zu entsprechenden getValue... () Methoden in Item, die in Tabelle 7-1 dargestellt werden. Beim Beziehen von Werten von Items, egal ob aus dem Document oder dem Item, ist es wichtig, den Datentyp der zu lesenden Daten zu kennen, sonst werden unerwartete Ergebnisse geliefert oder Exceptions geworfen (s.u.). Wird mit Items gearbeitet, steht die Methode getType () zur Verfügung, die die Art der Daten liefert. Die wichtigsten Typen sind TEXT, NUMBERS, DATETIMES, NAMES, READERS, AUTHORS, RICHTEXT, NOTEREFS, NOTELINKS, ATTACHMENT, OTHEROBJECT, EMBEDDEDOBJECT (s. Tabelle 2-3 in Kapitel 2.2.3), wobei diese Typen durch
gleichnamige Konstanten in Item repräsentiert werden. Beim Lesen (s. Tabelle 7-1) und Schreiben (s. Tabelle 7-2) von Item-Werten sind einige Besonderheiten zu beachten (die Nummern beziehen sich auf die beiden Tabellen). 1
2
3
4
5
Für Multi-Value Items können mehrere Werte als Vector bezogen werden. Enhält ein Item nur einen Wert, liefern die Methoden getItemValue und getValues einen Vector mit nur einem Element. Rückgabewerte im Vector values können Elemente der Typen Double, String oder DateTime sein. Das Konzept der CustomData Items erlaubt es, Objekte einer beliebigen Klasse, die Serializable implementiert, abzuspeichern. Die Handhabung ist einfach, es können solche Objekte direkt mit replaceItemValueCustomData (21) geschrieben und mit getItemValueCustomData wieder gelesen werden. Es empfiehlt sich beim Schreiben von CustomData-Elementen einen Typnamen zu spezifizieren (22), am besten wird hier der Name der Klasse des CustomDataElements verwendet. Wird versucht, ein Element über getItemValueCustomData (String, String) zu laden, das nicht dem spezifizierten Typ entspricht, wird eine entsprechende NotesException („Supplied Data type name does not match stored CustomData type“) geworfen. Zur Verwendung über externe APIs können solche Daten mit getItemValueCustomDataBytes (String name, String className) auch als Byte Array geschrieben werden. Datumsangaben können in Notes entweder als diskrete Datums- (und Uhrzeit-) Angaben gespeichert werden oder als DateTimeArray (getItemValueDateTimeArray (String name)) zur Definition von Zeitspannen. Üblich ist Letzteres. Datentypen kontrollieren Dringend überwachen müssen Sie die Datentypen bei der Verwendung der Methoden getItemValueInteger, getItemValueDouble und getItemValueString, denn diese werfen keine (!) Exception, wenn versucht wird, ein Item eines anderen Datentyps zu lesen.
260
7 Document
Tabelle 7-1 Methoden zum Lesen von Item-Werten in Item und Document
Zahlen werden in der Regel als Double gespeichert, beim Lesen über getItemValueDouble werden so auch Integer-Werte in Double umgewandelt. Grundsätzlich können Zahlen auch als Integer gespeichert werden, beim Lesen von Double-Werten über getItemValueInteger werden sie buchhalterisch gerundet. Der in Notes am einfachsten zu handhabende (und am häufigsten verwendete) Datentyp ist String und wird über getItemValueString gelesen. getFirstItem liefert nur ein Item, auch wenn es mehrere gleichen Namens gibt. Für getFirstItem gibt es kein Äquivalent in Item.
7.1.2 Datentypen handhaben
261
Tabelle 7-2 Methoden zum Schreiben von Item-Werten in Item und Document
10 getItems liefert alle Items eines Dokuments, auch eventuell solche mit gleichem Namen. 11 getSize in Document liefert die Gesamtgröße des Dokuments, die Größe von Items kann über getValueLength in Item berechnet werden. 12 Im Gegensatz zum Dokument kann im Item auch ein diskreter DateTime-Wert gehandhabt werden.
262
7 Document
13 getText liefert den Wert des Items als String, sofern er sich in einen solchen umwandeln lässt. RichTextItems können auch über getText angezeigt werden, werden jedoch um sämtliche Nicht-Text-Elemente reduziert. 14 getText (int max) arbeitet wie getText (), jedoch wird das Ergebnis auf maximal max Zeichen limitiert. 15 abstractText (int maxlen, boolean dropvowels,boolean userdict) ist ursprünglich dazu gedacht, RichText Items in Text umzuwandeln, kann aber auch auf normale (Text-) Items angewendet werden. maxlen gibt die maximale Anzahl von zurückgegebenen Zeichen an, dropvowels spezifiziert, ob Vokale aus dem Text entfernt werden sollen und userdict spezifiziert, ob Synonyme aus der Datei NOTEABBR.TXT angewendet werden sollen. Diese Datei enthält ein Replacement pro Zeile, wobei jede Zeile zunächst das neu einzufügende Wort, dann ein Leerzeichen, dann das zu ersetzende Wort enthalten muss. Es gelten spezielle Regeln für die Umwandlung in Uppercase, die Sie der Notes-Hilfe unter den Stichworten @Abstract und abstractText entnehmen können. Neu einzufügende Wörter müssen kürzer sein als zu ersetzende Wörter. 16 getValueLength liefert den internen Speicherbedarf für das Item in Bytes, einschließlich des von Notes benötigten Überhangs. 17 getType liefert eine int-Konstante (s.o.), die den Datentyp beschreibt. 18 containsValue ist für die Auswertung von Multi-Value Items gedacht und überprüft, ob eine Text-, oder Zahl- oder DateTime Liste den spezifizierten Wert enthält. 19 toString liefert (wider Erwarten) den Namen des Items, der auch über getName () erhalten werden kann. 20 Das Schreiben von Werten ist unterschiedlich in Document und Item realisiert. In Document wird der Methode replaceitemValue ein Parameterpaar aus name und value übergeben, wobei value ein Object ist, für das Werte instanceof Integer, Double, String, DateTime oder Item oder Vector aus Intger, Double, String oder DateTime übergeben werden können. In Item müssen diese Werte diskret über setValueDouble, setValueString usw. bzw., falls mehrere Werte gesetzt werden sollen, über setValues gesetzt werden. 21, 22, 23 – siehe 2,3,4 24 In Document können mit appendItemValue(String name) leere Items erzeugt werden, hasItem (28) liefert dann true, getItemValue liefert dann jedoch einen leeren Vector. getItemValueString liefert allerdings nicht etwa null, sondern einen leeren String. 25, 26, 27 Werte vom Typ Integer, Double, int, double, String oder DateTime können zu einem Multi-Value Item ergänzt werden, indem Werte hinzugefügt werden. Über Vektoren (27) können zeitgleich mehrere Werte angehängt werden. In Item gibt es hierfür nur Entsprechungen für Text und Textlisten (27). 28 Die Methode hasItem (String name) zeigt an, ob es ein Item (oder mehrere) dieses Namens im Document gibt. 29 removeItem in Document entfernt ALLE Items des angegebenen Namens. Doppelte Items mit gleichem Namen sind alle betroffen. 30 remove in Item entfernt nur das aktuelle Item. Doppelte Items mit gleichem Namen werden nicht gelöscht.
7.1.2 Datentypen handhaben
263
31 recycle räumt die C++ Backend-Repräsentation des Items auf. Für Version 5.x ist es wichtig, auch Items zu recyclen. In Version 6 gibt es verschiedene Versionen, in denen ein Recycle auch Fehler verursachen kann. In Version 6.5.2 z.B. recycelt, das recycle eines Items auch das gesamte Parent Document (s. Kap. 14). Es ist wichtig, sich immer ins Gedächtnis zu rufen, dass das Item nicht ohne „sein“ Document existieren kann. Wenn also z.B. ein Item über remove in Item entfernt wird, bezieht sich dies nicht etwa auf ein „alleinstehendes“ Item-Objekt, sondern entfernt endgültig das Item aus dem Document-Objekt, über das das Item bezogen wurde. Allerdings wird dieses Löschen des Items erst in der Datenbank manifestiert, wenn das Document über save gespeichert wird. Hier zeigt sich eine weitere wichtige Technik für die Verwendung von Items. Wird nämlich ein Document über eine der oben beschriebenen Methoden manipuliert, sind diese Änderungen so lange „virtuell“, bis ein save aufgerufen wird. Dies kann man sich zu Nutze machen, wenn Operationen auf einem Dokument ausgeführt werden müssen, die nicht in der Datenbank manifestiert werden dürfen, aber für einen bestimmten Zweck benötigt werden. Hierzu gehört die Verarbeitung von Items, von denen es mehrere gleichen Namens in einem Document gibt: doc.replaceItemValue(itemName,"Wert 1"); doc.appendItemValue(itemName, "Wert 2"); System.out.println ("Liefert \"Wert 1\": " + doc.getItemValue (itemName)); Item item = doc.getFirstItem(itemName); item.remove(); System.out.println ("Liefert \"Wert 2\": " + doc.getItemValue (itemName));
Mit diesem Code können also nach und nach Items (gleichen Namens) aus dem Document entfernt und weiterverarbeitet werden. Die Items müssen zwar über remove entfernt werden, um referenziert werden zu können, dies bleibt aber ohne Auswirkungen auf die Datenbank, solange kein save folgt. In jedem Fall gilt, dass die Verarbeitung am leichtesten (und die Anwendung am robustesten) ist, wenn jederzeit sauber kontrolliert wird, welche Items welchen Datentyp haben. Werden Daten über Benutzer und den Notes Client eingegeben, muss in den dort verwendeten Masken kontrolliert werden, welche Felder welchen Datentyp erzeugen. Einen Sonderfall nehemen Items mit dem Namen „Received“ ein, die in Dokumenten zu finden sind, die empfangene Internet-Mail-Nachrichten (in der eigenen Mail-In Datenbank) repräsentieren und deren Werte seit Domino R6.5 durch getReceivedItemText als Vector gelesen werden können. Dringend überwachen müssen Sie die Datentypen bei der Verwendung der Methoden getItemValueInteger, getItemValueDouble und getItemValueString, denn diese werfen keine (!) Exception, wenn versucht wird, ein Item eines anderen Datentyps zu lesen. getItemValueInteger liefert zum Beispiel immer 0, wenn kein Item des spezifizierten Namens gefunden wird oder das gefundene Item
264
7 Document
Best Practice Kontrollieren Sie, welche der Items welchen Datentyp haben und verwenden Sie nur die passenden Methoden. Ziehen Sie die Entwicklung eigener Methoden in Erwägung, die z.B. bei der Rückgabe von 0, 0.0 oder null über instanceof in getItemValue feststellen, um welchen Datentyp es sich tatsächlich handelt. Verwenden Sie getItemValue zum Bezug der Vectoren, wenn Sie keine Informationen über die zu lesenden Items haben. Verwenden Sie alternativ getFirstitem, getType und getValues, getValueString etc., wenn Sie keine Informationen über die zu lesenden Items haben.
einen anderen Datentyp hat. Dies stellt natürlich ein großes Risiko dar, da nie entschieden werden kann, ob tatsächlich ein Item mit dem diskreten Wert 0, oder aber ein Fehler vorliegt. Tabelle 7-3 zeigt das Verhalten der verschiedenen Methoden getItemValue..., falls versucht wird, Items zu lesen, deren Datentyp nicht zur Methode passt, und zeigt klar die Risiken bei der Rückgabe von 0, 0.0 oder null auf. Wenn keine Informationen vorliegen, welchen Datentyp die Items haben, ist es hilfreich, mit getItemValue immer nur die Vectoren zu beziehen. Dies erfordert aber, dann im Nachhinein entweder auftretende Exceptions in Kauf zu nehmen oder ein Exception Handling zu implementieren (z.B. falls Double erwartet wurde und bei dem dann notwendigen Cast Exceptions auftreten, da z.B. ein String geliefert wurde). Eine Alternative kann sein, eine Hilfsklasse zu implementieren, die zwar äquivalente Methoden für getItemValueString etc. vorhält, aber für den Fall der Rückgabe von 0, 0.0 oder null über getItemValue den Datentyp überprüft und falls dieser nicht zur aufgerufenen Methode passt, eine Exception wirft. Ein dritter Weg ist die konsequente Verwendung von Items und der Ermittlung des Datentyps über getType.
Bedenken Sie Dokumente können von Benutzern im Notes Client in Masken geöffnet werden. Enthalten Masken Berechnungen, so können im Backend über Java gesetzte Werte (unerwartet) verändert werden. Notes-Masken führen eine automatische Typen-Konvertierung durch, die zu unerwarteten Ergebnissen führen kann, wenn über Java in einem Item Daten eines anderen Datentyps als in der Maske vorgesehen gespeichert werden.
265
7.1.2 Datentypen handhaben
Tabelle 7-3 Verhalten der getItemValue-Methoden beim Lesen nicht erwarteter Datentypen (lokale Session) Datentyp des Items Integer
Methode getItemValueInteger
Integer
Double
gerundeter 0 Wert als Integer als Double 0.0
getItemValueDouble Wert Double getItemValueString null getItemValue Vector mit Wert als Double (!!) getItemValue(6) DateTimeArray getItemValue(1) CustomData
getItemValueCustomDataBytes
(2)
String
DateTime CustomData
Custom- Item Datanicht Bytes vorhanden
0
0
0
0
0.0
0.0
0.0
0.0
null Vector mit Wert als Double
String Vector aus String
null null Vector aus (7) DateTime
null (7)
null Leerer Vector
(6)
(6)
(6)
(4)
(1)
(1)
(3)
(5)
(2)
String (!!)
Vector aus (6) DateTime (1) Objekt der abgelegten Custom Klasse (2) ObjektStream als Byte Array
Objekt null als Byte Array
(1) NotesException: Supplied Data type name does not match stored CustomData type oder java.io.StreamCorruptedException: InputStream does not contain a serialized object (2) NotesException: Supplied Data type name does not match stored CustomData type oder sinnloses Ergebnis (3) java.io.StreamCorruptedException: InputStream does not contain a serialized object (4) NotesException: Item value is not a date type (5) java.lang.NullPointerException (6) NotesException: Item value is not a date type (7) NotesException: Unknown value type to convert Eine weniger elegante, aber dennoch des Öfteren (in der gelebten Praxis) anzutreffende Strategie ist es, grundsätzlich alle Werte in Strings oder Vectoren aus Strings abzuspeichern (bei der Eingabe über Notes-Masken) und dann im Einzelfall über entsprechende Parser etc. die Werte auszulesen, natürlich mit der gesamten Problematik der Konvertierung und des unnötigen Speicherüberhangs. Die Methoden verhalten sich übrigens äquivalent, sofern es sich um Multi-Value Items handelt, wobei die Methoden getItemValueInteger, getItemValueDouble und getItemValueString nur jeweils den ersten Wert des Items zurückgeben.
266
7 Document
Tabelle 7-4 Verhalten der getItemValue-Methoden beim Lesen nicht erwarteter Datentypen (DIIOP-Session) Datentyp des Items Integer
Methode getItemValueInteger
Integer
Double
0 Integer Anteil des Wertes als Double 0.0
getItemValueDouble Wert Double getItemValueString "" getItemValue Vector mit Wert als Double (!!) getItemValueDateTi- (6) meArray getItemValueCus(1) tomData getItemValueCustomDataBytes
(2)
String
DateTime CustomData
Custom266 Item Dataicht Bytes vorhanden
0
(8)
(8)
0
0.0
(8)
(8)
0.0
"" Vector mit Wert als Double
String Vector aus String
"" (8) Vector aus (8) DateTime
(8) (8)
null Leerer Vector
(6)
(6)
(8)
(1)
(1)
Leerer Vector Undefiniertes Object
(2)
String (!!)
Vector aus (8) DateTime (1) Objekt der abgelegten Custom Klasse (2) ObjektStream als Byte Array
(3)
Objekt null als Byte Array
(1) NotesException: Supplied Data type name does not match stored CustomData type oder java.io.StreamCorruptedException: InputStream does not contain a serialized object (2) NotesException: Supplied Data type name does not match stored CustomData type oder sinnloses Ergebnis (3) java.io.StreamCorruptedException: InputStream does not contain a serialized object (4) NotesException: Item value is not a date type (5) java.lang.NullPointerException (6) NotesException: Item value is not a date type (8) NotesException: Invalid object type for method argument Die Unterschiede zu Tabelle Tabelle 7-3 sind fettgedruckt. Fazit: Planen Sie Ihre Datentypen gründlich und verhindern Sie Fehler, die aus der Rückgabe von Null-Werten entstehen können. Zur weiteren Verdeutlichung des Verhaltens wurde die Klasse GetItemReturnValues im package djbuch.kapitel_07 beigelegt, die insgesamt vier Demonstrationen beinhaltet. Demo 1 erzeugt ein leeres Dokument und wendet die Methoden getItemValue... auf nicht vorhandene Items an.
7.1.2 Datentypen handhaben
267
Unterschiedliche Rückgabewerte für lokale und DIIOP-Sessions Je nachdem, ob die getItemValue Methoden in einer lokalen oder einer IIOP Session verwendet werden, geben sie unterschiedliche Werte zurück, wenn sie auf unerwartete Datentypen angewendet werden. Tabelle 7-3 und Tabelle 7-4 zeigen diese Werte für lokale und IIOP-Sessions. Insbesondere die unterschiedliche Rundung von getItemValueInteger und die Rückgabe von leeren Strings statt null bei getItemValueString sind bemerkenswert. Sind mehrere Items gleichen Namens in einem Document gespeichert, ist nicht gewährleistet, dass Domino diese Werte für lokale und DIIOP-Sessions in der gleichen Reihenfolge behandelt. Dies kann bedeuten, dass je nach Session-Typ verschiedene (!) Werte (mal das eine, mal das andere Item des doppelt verwendeten Namens) zurückgegeben werden.
Demo 2 erzeugt für jeden Datentyp ein Item und wendet auf jedes Item alle getItemValue Methoden an.
Demo 3 arbeitet wie Demo 2, erzeugt jedoch Vectoren aller Datentypen. Diese drei Demonstrationen zeigen das Verhalten der Methoden in showBehaviorGetItem an. Dort wird versucht ein Item mit allen zur Verfügung stehenden Methoden zu lesen und anzuzeigen. Demo 4 zeigt den obigen Code zum Lesen von mehreren Items mit gleichem Namen. Ein letzter wichtiger Punkt ist noch erwähnenswert. Items, die über die beschriebenen Java-Methoden manipuliert werden, unterliegen (selbstverständlich aber nur zunächst) ausschließlich diesen vom Programmierer erwünschten Veränderungen. Da Items im Notes Client in Notes-Masken geladen werden können, muss immer bedacht werden, dass ursprünglich sauber gesetzte Datentypen in den Masken verändert werden können. Speichern Sie zum Beispiel ein Number Item (Integer oder Double) in einem Dokument und ein Benutzer öffnet ein solches Dokument in einer Maske, die ein Feld gleichen Namens besitzt, das aber den Datentyp Text hat, so wird der NumberWert automatisch (durch die Maske) in Text umgewandelt. Speichert der Benutzer das Dokument, so wird aus Ihrem Number Item ein Text Item. Dies kann unangenehme Nebenwirkungen haben und sollte immer bedacht werden. Gleiches gilt für automatische Berechnungen, die in Masken durchgeführt werden könn(t)en.
268
7 Document
Im umgekehrten Fall kann es erwünscht sein, die in einer Maske hinterlegten automatischen Berechnungen, Programmierungen oder Validierungen und Eingabeumsetzungen auf ein im Backend durch Java manipuliertes Dokument anzuwenden. Dies kann über die Methode computeWithForm in Document erreicht werden.
7.1.3
Item Properties und weitere Methoden in Item
Neben den beschriebenen Methoden stehen noch verschiedene setter- und getter-Methoden in Item zur Verfügung, mit denen Eigenschaften der Items verändert werden, die nicht direkt über Methoden im Document gesetzt werden können. Soll für Items im Document einer dieser Werte gesetzt oder gelesen werden muss natürlich immer der Weg über das Item eingeschlagen werden und die Verwendung der getItemValue-Methoden in Document erübrigt sich. •
Wird ein Text-Item mit dieser Eigenschaft versehen und das Dokument gespeichert, so können das Dokument nur noch Benutzer verändern, deren Benutzername(n) in diesem Feld auftauchen. Alternativ kann im so erzeugten Autorenfeld auch eine auf dem Server bekannte Gruppe, in der sich der Benutzer befindet, eingetragen werden. Eine weitere Möglichkeit besteht darin, in der ACL eine Rolle anzulegen und dort den gewünschten Benutzern diese Rolle zuzuweisen. Werden diesen Benutzern dann lediglich Autoren- (nicht Editoren-) Rechte in der ACL zugewiesen, jedoch erzwungen, dass jedes Dokument, das von solchen Autoren bearbeitet werden soll, ein Autorenfeld hat, in dem diese Rolle eingetragen ist, dann können diese Autoren solche Dokumente verändern, auch wenn sie nicht von ihnen selbst angelegt wurden. Wenn Rollen in Autoren-, Namens- oder Leserfelder eingetragen werden, müssen diese mit eckigen Klammern umschlossen werden, z.B.: doc.replaceItemValue ("allowedAuthors", "[authorRole]"); Item item = doc.getFirstItem ("allowedAuthors"); item.setAuthors (true); doc.save ();
Sollen mehrere Rollen oder Benutzer gleichzeitig eingetragen werden, müssen Sie einen Vector verwenden: Vector authors = new Vector(); authors.add("CN=Vorname Nachname/O=DJBUCH"); authors.add("[Sachbearbeiter]"); Item i = doc.replaceItemValue("Authors",authors); i.setAuthors(true); doc.save();
7.1.3 Item Properties und weitere Methoden in Item
269
Leser- und Autorenfelder richtig einsetzen Verwenden Sie immer die Langform für Einträge in Leser- und Autorenfeldern. Auch wenn Domino es erlaubt, an einigen Stellen die Bezeichner „CN=“ oder „O=“ usw. wegzulassen, ist es zwingend erforderlich, dass Sie Benutzer, die Sie in Leser- oder Autorenfelder eintragen, dort in der ausführlichen Form listen. Andernfalls werden die Benutzer die erwarteten Rechte nicht erhalten. Autorenfelder greifen nur für Benutzer bis maximal Autor Level. Alle Benutzer mit EditorRechten oder höher dürfen ohnehin die Dokumente anderer Benutzer verändern, können also nicht über ein Autorenfeld eingeschränkt werden.
Analog zu setAuthors und isAuthors zu verwenden. Liegt ein derartiges ReaderItem (auch Leserfeld) in einem Document vor, dann können nur noch Benutzer dieses Document lesen, die im Leserfeld eingetragen sind. Beachten Sie, dass durch unaufmerksame Verarbeitung von Reader Items Dokumente unerreichbar werden können, wenn es keinen Benutzer gibt, der im Leserfeld gelistet ist. Erst seit Version 6 gibt es mit der Methode createSessionWithFullAcces in NotesFactory eine Möglichkeit solche Dokumente wiederzuerlangen, denn für einen Benutzer einer durch createSessionWithFullAcces geöffneten Session gelten keine Leser- oder Autorenfelder. •
void setNames(boolean flag) throws NotesException boolean isNames() throws NotesException Durch setNames wird ein Item zu einem Namensfeld. Dies hat keine besondere
Bedeutung, sondern kennzeichnet lediglich, dass in einem solchen Item nur Notes-Namen gespeichert werden sollen. •
Notes-Dokumente können durch symmetrische Schlüssel verschlüsselt werden. Bei der Verschlüsselung auf Dokumentenebene werden nur Items verschlüsselt, die das Flag isEncrypted () == true tragen (s. Kap. 7.5). •
void setProtected(boolean flag) throws NotesException boolean isProtected() throws NotesException Items, für die isProtected () == true gilt, können auch bei der Neuanlage
von Dokumenten nur von Benutzern bearbeitet werden, die als Editor in der ACL eingetragen sind. Diese Eigenschaft kommt in der Regel nur in der Verwendung von Notes-Masken über die Eigenschaft „Must have at least Editor access to use“ zum Einsatz. •
7 Document Items, für die isSaveToDisk () == false gilt, werden bei einem save in Document nicht mit in die Datenbank gespeichert. Default ist isSaveToDisk() == true. void setSigned(boolean flag) throws NotesException boolean isSigned() throws NotesException Wird diese Eigenschaft gesetzt, werden Dokumente, wenn sie über die Notes-eigenen Mail-Methoden (send in Document) verschickt werden und das Dokument signiert wird, signiert (s. Kap. 9.7).
•
•
void setSummary(boolean flag) throws NotesException boolean isSummary() throws NotesException Items, für die isSummary() == false gilt, können nicht in Ansichten-Spalten angezeigt werden. Default ist true. DateTime getLastModified() throws NotesException
Liefert das letzte Änderungsdatum des Items. •
String getName() throws NotesException getName liefert den Namen des Items, der z.B. beim Erstellen über replaceItemValue (String name, Object value) im Parameter name verwendet
•
Document getParent() throws NotesException getParent liefert das Document, über das das Item geladen oder erzeugt wur-
wurde.
de. Folgende Methoden stehen zusätzlich in Item zur Verfügung: •
org.xml.sax.InputSource getInputSource() throws NotesException Liefert den Inhalt eines Items als InputSource für die weitere Verarbeitung.
Diese Methode erzeugt eine temporäre Datei, die (erst) beim Recycling wieder gelöscht wird. •
java.io.InputStream getInputStream() throws NotesException Liefert den Inhalt des Items als java.io.InputStream-Repräsentation. Diese
•
java.io.Reader getReader() throws NotesException Liefert den Inhalt des Items als java.io.Reader. Diese Methode erzeugt eine
Methode erzeugt eine temporäre Datei, die beim Recycling wieder gelöscht wird.
temporäre Datei, die (erst) beim Recycling wieder gelöscht wird.
Seit Version 5 kann Domino Multipart Mime Messages verarbeiten, die auch ihre Repräsentation in den Java-Methoden gefunden haben. Typischerweise enthält eine Mail, die über Domino in die Mail-Datenbank des Benutzers geschickt wurde, ein Body-Feld, in dem sich MIME Entities befinden. MIME Entities können auch dazu genutzt werden, um HTML-E-Mails über Java in Domino zu versenden.
7.1.4 Mit Document arbeiten – Lifecycle eines Dokuments •
Enthält ein Item gültiges XML, so kann es mit dieser Methode in ein XML dom Document umgewandelt werden. Ist der Parameter validate true, so wird die Überprüfung der DTD erzwungen. •
Wendet das XSL StyleSheet xslStyleSheet auf das XML an, das in dem Item gespeichert ist. xslStyleSheet darf vom Typ org.xml.sax.InputSource, lotus.domino.Item, lotus.domino.EmbeddedObject oder lotus.domino.MIMEEntity sein.
7.1.4
Mit Document arbeiten – Lifecycle eines Dokuments
Dokumente in Domino und das Document als Repräsentation in Java sind der Basiscontainer für alle in Domino gehandhabten Objekte. Sie haben bereits in Kapitel 2.2.1 das Konzept der Domino-Note kennengelernt. Das Document ist die DominoNote, wobei sich nicht jede Note über ein Document handhaben lässt. Ein Document hat in der Regel einen typischen Lifcycle: Es wird erstellt oder z.B. über einen Suchmechanismus selektiert, es wird verändert oder kopiert, es wird gespeichert oder gelöscht und natürlich zum Schluss recycelt. Ohne eine Ansicht oder eine Suchanfrage lassen sich Dokumente direkt aus der Datenbank oder dem AgentContext beziehen, entweder einzeln über die UNID oder als vordefinierte Collection, z.B. aller Dokumente.
Ist die Note ID eines Dokuments bekannt, so kann in Database mit getDocumentByID das Dokument mit dieser ID geöffnet werden. Gibt es das spezifizierte Dokument nicht, wird null zurückgegeben. 43 44
45
Neue Signatur: Ab Domino Version 7 wirft diese Methode nur noch eine NotesException. Seit Domino R7 ist XSLTResultTarget eine eingenständige Klasse und basiert im Wesentlichen auf javax.xml.transform, da seit Java 1.4.x die XML- und XSL-Klassen Bestandteil des Standard-JRE sind. Dementsprechend wurden aus der Domino 7 Core Version auch die Bibliotheken xml4j.jar und lotusxsl.jar entfernt. Dies hat leider Auswirkungen auf bestehende Installationen, da nun die neuen Parser verwendet werden müssen. Neue Signatur: Ab Domino Version 7 wirft diese Methode nur noch eine NotesException.
272
7 Document Ist noteID keine Hexadezimalzahl oder "" oder null, wird eine NotesException geworfen. Die NoteID kann in Document über String getNoteID() bezogen werden. Sie ist nur pro Replik eindeutig. Ein primary Key, der über alle Repliken einer Datenbank eindeutig ist, liefert String getUniversalID(). Die NoteID ist eine achtstellige Hexadezimalzahl, die als String übergeben wird. Führende Nullen müssen nicht zwingend mit angegeben werden. Manchmal, z.B. innerhalb der @Formelsprache, wird der Note ID die Buchstabenfolge „NT“ vorangestellt. Für getDocumentByID muss diese entfernt werden.
Ist die Universal ID eines Dokuments bekannt, so kann in Database mit getDocumentByUNID ein Dokument direkt geöffnet werden. Gibt es das spezifizierte Dokument nicht oder ist der Parameter universalID entweder keine gültige 32stellige Hexadezimalzahl (als String) oder leer oder null, wird eine NotesException geworfen. •
Der Name dieser Methode ist leicht irreführend oder zumindest nicht selbsterklärend. getDocumentByURL lädt kein Notes-Dokument, sondern diejenige Web-Seite, die in url spezifiziert ist. Das Ergebnis dieser Methode ist recht unflexibel zu handhaben und wird von Domino eigentlich nur vorgehalten, um im Notes Client Webseiten anzeigen zu können. Der geladene HTML-Code wird in einem neuen Notes-Document im Item Body, das als RichText Item angelegt wird, abgespeichert. Zusätzlich werden in diesem Document Items für Header und Metainformationen angelegt. Das Item Form erhält den Wert „HTMLForm“, d.h. die Default-Maske für dieses Dokument heißt „HTMLForm“. Dies so erzeugte Notes-Document wird als Ergebnis der Methode zurückgegeben. Wird ein solches Dokument gespeichert, so bleibt zunächst neben den Metainformationen lediglich der unveränderte HTML-Code im Body Item. Wird dieses Dokument aber im Notes Client geöffnet, so versucht Domino den HTML-Code auszuwerten und zu rendern. Images werden vom ursprünglichen Server (erst jetzt) downgeloaded. Falls dies nicht mehr möglich ist, wird ein entsprechender Platzhalter hinterlegt. Wird das Dokument nun vom Anwender erneut gespeichert, so wird der HTMLCode in RichText umgewandelt, Links in Hotspots konvertiert und die geladenen Bilder als PICT-Ressourcen in den RichText eingebettet. Dies funktioniert natürlich nur, wenn im Designer der Datenbank, in dem das Dokument gespeichert wurde, eine Maske mit Namen HTMLForm vorliegt, die ein RichText-Feld mit Namen HTMLForm hat. Kann eine URL nicht geöffnet werden, wird keine Exception geworfen, sondern ein Document zurückgegeben, mit dem entsprechenden Status im Item „Status“, z.B. 503, wenn eine URL nicht aufgelöst werden kann.
7.1.4.2 Dokumente selektieren über Methoden in Document
273
Wird ein nicht unterstütztes Protokoll oder eine nicht gültige URL übergeben, so wird ebenso verfahren. Je nach Fall wird im Feld Title und/oder Body eine Fehlermeldung hinterlegt. Übrigens kann diesem RichText-Feld die Eigenschaft „Storage - Store contents as HTML and Mime“ mitgegeben werden (im Domino Designer in den erweiterten Feldeigenschaften). Dann findet aber ebenfalls eine Umwandlung statt (wie dies immer der Fall ist, ist diese Eigenschaft gesetzt). Domino wandelt dann den RichText-Inhalt in gültiges MIME um, erzeugt die benötigten MIME Parts und erzeugt in optional auch mehreren Body Items eine Multipart MIME Message. Bilder werden hierdurch z.B. in jeweils einem eigenen MIME Part gespeichert. •
licher Parameter: reloadOnlyIfModified – falls enforceReload gesetzt ist, wird nur dann
neu geladen, falls der Webserver meldet, dass die Seite verändert wurde. saveUrlsInListItem – falls dieser Parameter auf true gesetzt wird, speichert Domino alle gefundenen Links im Item URLLinks[n], wobei n mit 1 beginnt
und immer jeweils ein neues Item mit aufsteigender Endziffer angelegt wird, wenn ein Item droht, größer als 64 KByte zu werden. Die Verwendung dieses Parameters kann die Verarbeitung verlangsamen. charset – spezifiziert das für die Konvertierung zu verwendende Character Set. httpUserName und httpUserPassword erlauben das Login mit einer HTTP Base Authentication. proxyUserName und proxyuserPassword erlauben die Authentifizierung an einem Proxyserver. returnImmediately – für interne Verwendung. Wird dieser Parameter auf true gesetzt, wird kein Document Object zurückgegeben, sondern die Seite im Hintergrund geladen.
7.1.4.2 •
Dokumente selektieren über Methoden in Document
String getURL() throws NotesException String getNotesURL() throws NotesException (Neu seit Domino R6.5)
Diese beiden Methoden liefern die so genannte Notes-URL (Abb. 7-1) eines Notes-Objekts. Sie wird wie jede URL aufgebaut und besteht aus Protokollbezeichnung („notes“), einem Eintrag für den Server und einer URL mit der Datenbank und Objekt spezifiziert werden. z.B.: a) notes:///__C1256EEE002C549F.nsf/0 /9BE8F2EAD563EC66C1256FC70044B02C?OpenDocument.
274
7 Document b) notes://www.djbuch.de/djbuch/djbuch.nsf/$all /9BE8F2EAD563EC66C1256FC70044B02C?OpenDocument
c) notes://java@djbuch/djbuch/djbuch.nsf/V_alleNachVorname /Thomas?OpenDocument
Für ein Dokument besteht diese URL aus der Datenbank-ReplikID, der zwei Unterstriche „_“ vorangestellt und die Endung „.nsf“ nachgestellt werden, dem Namen des zu verwendenden Views, wobei als Default-View das Zeichen Null „0“ verwendet werden kann, und entweder dem Index des Dokuments innerhalb des View, wie er sich in der ersten Spalte des Views wiederfindet, oder, wie in diesem Beispiel, die Universal ID des Dokuments, gefolgt von einer Aktion, die für dieses Beispiel „OpenDocument“ oder „EditDocument“ heißen kann. Beachten Sie, dass die Methode getURL() je nach Kontext unterschiedliche Ergebnisse liefert. In einer lokalen Session liefert diese Methode die Notes-URL, in einer DIIOP-Session die HTTP URL. Beachten Sie, dass diese Art von URLs nicht (!) kompatibel mit der Methode getDocumentByURL in Database ist. •
String getHttpURL() throws NotesException Neu seit Domino R6.5. Die Methode getHttpURL liefert eine URL, mit der ein
Domino-Objekt im Browser angezeigt werden kann. Für ein Dokument ist dies eine URL in der Art des Beispiels b aus dem vorigen Absatz, wobei das Protokoll von notes auf http geändert wird. Allerdings liefert getHttpURL nur die URL, wenn diese Methode im HTTP-Kontext aufgerufen wird, sonst liefert sie nur einen leeren String. Der HTTP-Kontext steht z.B für periodische Agenten auf einem Server zur Verfügung, ebenfalls bei der Verwendung von DIIOP oder beim Einsatz von Webagenten. Es genügt nicht, einen Agenten zwar in einer Datenbank auf einem Server zu starten, die Session aber lokal (z.B. über den Notes Client oder sinitThread) aufzubauen.
Abb. 7-1 Notes URL
7.1.4.3 Dokumente selektieren über vordefinierte DocumentCollections
275
Anmerkung: Die Methoden getURL, getNotesURL und getHttpURL stehen für die Objekte Agent, Database, Document, Form, View und Session zur Verfügung. getNotesURL ist für Session nicht vorhanden. Die beiden übrigen Methoden liefern für eine lokale Session den leeren String und für eine DIIOP-Session die HTTP URL des Servers in der Form „http://www.djbuch.de?OpenServer“ Ein ausführliches Beispiel zur Anzeige aller Methoden finden Sie in den Beispielen im Package djbuch.kapitel_07 in der Klasse ShowURLMethods.
7.1.4.3
Dokumente selektieren über vordefinierte DocumentCollections
Steht die Universal ID eines Dokuments nicht zur Verfügung, so werden Dokumente über eine DocumentCollection, ViewEntryCollection oder direkt über einen View selektiert. In Database und AgentContext gibt es vordefinierte DocumentCollections, eigene Collections können über Suchmechanismen (s. Kap. 15) erhalten werden. Alle Dokumente einer Datenbank werden über getAllDocuments() throws NotesException in Database ausgewählt. Das Ergebnis ist eine DocumentCollection, die z.B. über getFirstDocument() throws NotesException und getNextDocument() throws NotesException durchschritten werden kann (s. Kap. 10.8). Seit Domino R7 steht in Database die Methode getModifiedDocuments zur Verfügung, mit der bestimmte Daten- aber auch Designdokumente ausgewählt werden können. Vordefinierte DocumentCollections in Database: •
Liefert alle (Daten-) Dokumente einer Datenbank. •
DocumentCollection getModifiedDocuments() throws NotesException DocumentCollection getModifiedDocuments (DateTime notOlderThan) throws NotesException DocumentCollection getModifiedDocuments (DateTime notOlderThan, int typeOfNote)throws NotesException Neu seit Domino R7. Die Methode getModifiedDocuments stellt eine Beson-
derheit dar, die typisch für Domino R7 ist. Es wird nicht mehr nach Design- oder Datendokumenten unterschieden, sondern alle Note-Objekte werden, wie im ursprünglichen Konzept und in der Datenstruktur vorgesehen, gleich behandelt.
276
7 Document Erst über das zusätzliche Attribut typeofNote wird dann genauer spezifiziert, welche Art von Note-Objekten geladen werden soll. Zulässige Werte für typeOfNote sind vordefinierte Konstanten in Database, die durch bitweises OR („|“) miteinander verknüpft werden können: DBMOD_DOC_FORM, DBMOD_DOC_ VIEW, DBMOD_DOC_ICON, DBMOD_DOC_ACL, DBMOD_DOC_DATA, DBMOD_ DOC_HELP, DBMOD_DOC_AGENT, DBMOD_DOC_SHAREDFIELD, DBMOD_ DOC_REPLFORMULA, DBMOD_DOC_ALL. Zurückgegeben werden alle seit dem Zeitpunkt notOlderThan modifizierten Dokumente (Note-Objekte). Für getModifiedDocuments() werden alle Dokumente der Datenbank zurückgegeben. Dies entspricht db.getModifiedDocuments (db.getCreated()). Zum Zeitpunkt der Ausführung von getModifiedDocuments wird die aktuelle Datenbankzeit in der DocumentCollection gespeichert und kann dort über getUntilTime wieder bezogen werden, um als Parameter für einen erneuten Aufruf von getModifiedDocuments (DateTime) zu dienen, so dass mit je-
dem Aufruf die neuen Dokumente seit dem jeweils letzten Aufruf geladen werden können. Wird ein Java-Agent im Domino Designer erstellt und steht ein AgentContext-Objekt über getDocumentContext() throws NotesException zur Verfügung, liefert getUnprocessedDocuments() throws NotesException (beide in AgentContext) alle von diesem Agenten bisher unverarbeiteten Dokumente, wobei der Verarbeitungsstand über die Methode updateProcessedDoc() throws NotesException nachgehalten werden muss. Die Collection der unverarbeiteten Dokumente kann über verschiedene Suchfunktionen und Signaturen der Methode unprocessedFTSearch und unprocessedSearch eingeschränkt werden, wobei diese ebenso arbeiten wie die in Kapitel 15.5 beschriebenen Suchmechanismen. Der Mechanismus der unverarbeiteten Dokumente wurde bereits in Kapitel 3.4.2 ausführlich erläutert.
7.1.4.4
Neue Dokumente erstellen
Neue Dokumente erstellen Sie entweder über createDocument in Database oder über copyToDatabase(Database targetDatabase) in Document. In beiden Fällen erhalten Sie ein neues Document-Objekt eines noch nicht im Speicher der Datenbank (des NSF) manifestierten Notes-Dokuments. Wird ein Dokument kopiert, entsteht ein exakter Klon des Originals, lediglich die entsprechenden Angaben für Autor, Erstellzeitpunkt etc. werden angepasst. Wird ein Dokument neu erstellt, wird über isNewNote() in Document signalisiert, dass es sich hier um ein noch nicht in der Datenbank vorhandenes Dokument handelt. Nach dem ersten save liefert dieses Flag nur noch false. Hilfreich ist dies, wenn bestimmte Operationen nur beim ersten Erstellen des Dokuments ausgeführt werden sollen. Daher liefert isNewNote() nach einem copyToDatabase auch false, um zu signalisieren, dass es sich hier um ein Dokument handelt, das bereits programmatisch einen Inhalt erhalten hat.
7.1.4.4 Neue Dokumente erstellen
277
Neue Dokumente erhalten bereits mit der Instanzierung über createDocument eine Universal ID, aber noch keine Note ID. Selbstverständlich kann weder über getDocumentByUNID noch über getDocumentByID ein solches Dokument selektiert werden, denn es existiert nur als Speicher-Objekt und kann daher nicht aus der Datenbank ausgelesen werden. Die Universal ID (und die Notes-ID) wird von Domino vergeben und dient als primärer Schlüssel des Dokuments. Die Universal ID kann jedoch auch vom Benutzer nachträglich verändert werden, um eigenen Bedürfnissen Rechnung zu tragen. Mit setUniversalID kann die Universal ID eines Dokuments verändert werden. Geschieht dies bei einem neuen Dokument, wird dieses neue Dokument lediglich mit dieser Universal ID versehen und später unter dieser ID gespeichert. Wird setUniversalID auf ein vorhandenes Dokument angewendet, entsteht ein neues Dokument, d.h. das bereits in der Datenbank gespeicherte Dokument mit der „alten“ Universal ID bleibt bestehen. Bedingung ist, dass die neu gesetzte Universal ID eine 32stellige Hexadezimalzahl ist. Wird eine ID doppelt vergeben, so wird (erst) beim Aufruf von save eine NotesException „Database already contains a document with this ID (UNID)“ geworfen. Bedenken Sie, dass in der Regel Notes-Dokumente auch von Notes-Benutzern, also letztendlich im Notes Client verarbeitet werden. Daher ist es zwingend erforderlich zu klären, welche Maske einem neuen Dokument zugeordnet werden soll und ob es unter Umständen Items gibt, die von dieser Maske erwartet werden. Außerdem ist es eine Überlegung wert, zu prüfen, ob Leser- oder Autorenfelder im Dokument benötigt werden, um die Sicherheit und die konsistenten Zugriffsrechte zu gewährleisten. Wurden durch die Notes-Programmierer bereits Masken und Felder vorgegeben, müssen deren Datentypen korrekt eingehalten werden. Falls es notwendig sein sollte, dass jedes neu erstellte Dokument durch bestimmte Berechnungen, die in einer Notes-Maske festgelegt sind, überarbeitet werden muss, dann könnnen Sie mit der Methode computeWithForm(boolean unused, boolean throwExceptionOnError) throws NotesException eine Anwendung aller Maskenformeln und -berechnungen auf Ihr Dokument erzwingen. Allerdings sollte computeWithForm nur auf bereits gespeicherte Dokumente angewendet werden, da hier teilweise unerwartete Ergebnisse beobachtet wurden, insbesondere scheinen nicht immer alle Items mit in die Berechnung einbezogen zu werden, wenn das Dokument nicht zuvor gespeichert wurde. computeWithForm kann auf zwei Arten benutzt werden. Wird der Parameter throwExceptionOnError als true übergeben, so wirft diese Methode eine NotesException, falls die Berechnungen nicht durchgeführt werden konnten oder, und dies ist der wichtigere Fall, falls eine Feldvalidierungsformel (s. Kap. 3.2) nicht validieren konnte. Ist der Parameter throwExceptionOnError false, dann gibt die Methode true oder false zurück, je nachdem, ob die Berechnung und Validierung erfolgreich war oder nicht. Der Parameter unused wird von der Methode nicht verwendet und scheint nur aus Gründen der Abwärtskompatibilität noch vorzuliegen. Wenn geklärt ist, welche Maske ein Dokument verwendet, so müssen Sie natürlich noch über replaceItemValue ("Form", yourFormName) den Maskenna-
278
7 Document
men im Dokument speichern. Dies sollte auf keinen Fall vergessen werden, auch wenn es nicht zwingend vorgeschrieben ist, dass ein Dokument ein Item Form beinhaltet. Sieht man von der Problematik des Recyclings im Document ab, so kann übrigens ein Document auch ein sehr praktischer (temporärer) Container oder Übergabeparameter in Ihrer Java-Umgebung sein, ohne dass das Document jemals gespeichert werden muss. Die verschiedenen in Kapitel 7.1.1 ff beschriebenen Methoden und Datentypen, mit denen Items ausgestattet sind, bieten einen reichen Werkzeugkasten und machen das Document zu einem universellen Container. Bedenken Sie, dass selbst Dateianhänge (s. Kap. 7.4) in einem Document gespeichert werden können.
7.1.4.5
Speichern von Dokumenten
Um ein Document permanent in der Datenbank zu speichern, genügt der einfache Aufruf der Methode save(), die über einen booleschen Rückgabewert signalisiert, ob der Speichervorgang erfolgreich war. Wie so oft, haben einfache Dinge ihre Tücken und so will auch dieser einfache Vorgang wohlüberlegt sein. Domino ist nicht nur ein Mehr-Benutzer-System, sondern existiert dank der Replikation in verteilten Umgebungen, so dass mehrere Repliken (die man auch als Ausprägungen oder Instanzen bezeichnen könnte) eines einzigen Dokuments auf mehreren Servern vorliegen können (und werden), ohne dass irgendeine Garantie darüber besteht, dass diese Server dauerhaft miteinander verbunden sind. Das beste Beispiel ist die lokale Replik einer Datenbank auf einem Notebook, die vom Benutzer nur ab und zu, wenn er denn Zugang zum Internet (oder seinem Einwähl-Notesserver) hat, mit der Version auf dem Server abgeglichen wird. Hierin ist der Grund zu sehen, dass erst seit Version 6 für Domino ein Document Locking zur Verfügung steht, das aber natürlich immer nur dann aktiv funktionieren kann, wenn zwei Zugriffe, die gegeneinander gelocked werden sollen, auf dem gleichen oder zumindest auf zwei dauerhaft verbundenen Servern stattfinden. Daher wird auch nach Locking und provisorischem Locking unterschieden. Näheres hierzu finden Sie in Kapitel 7.7. Aus diesen Überlegungen folgt, dass beim Speichern eines Dokuments bedacht werden muss, dass möglicherweise ein anderer Benutzer dieses Dokument ebenfalls geöffnet und ebenfalls bereits geändert und gespeichert hat oder noch ändern und speichern wird. Tritt solch ein Konfliktfall auf, sind verschiedene Szenarien denkbar. Der einfachste Fall ist, dass der konkurrierende Benutzer am Notes Client arbeitet und (zeitlich) nach Ihnen speichern will, nachdem Ihre Operationen bereits abgeschlossen sind. Dann wird er auf den Konfliktfall hingewiesen und gefragt, ob er seine Änderungen in einem so genannten Konfliktdokument speichern möchte. Sein Problem :-). Weitere Optionen werden dem Benutzer interaktiv nicht angeboten. Im umgekehrten Fall gibt es in Ihrem Java-Programm natürlich keine Interaktion, daher können Sie der Methode save verschiedene Parameter mitgeben, mit denen Sie das Verhalten im Konfliktfall steuern können und zwar in den Signaturen
7.1.4.5 Speichern von Dokumenten
279
save (boolean enforceSave) und save (boolean enforceSave, boolean makeResponseOnConflict). Der Parameter enforceSave legt fest, ob Ihr
Dokument auch dann gespeichert werden soll, wenn jemand anders das Dokument ebenfalls geöffnet und geändert hat. Im Gegensatz zum vorigen Beispiel kann also programmatisch ein Speichern erzwungen werden, ohne dass ein Konfliktdokument erzeugt wird. Falls enforceSave false ist, kann in der zweiten Signatur mit dem Parameter makeResponseOnConflict festgelegt werden, ob im Konfliktfall ein Konfliktdokument gespeichert werden soll. Ist dieser Parameter false, wird im Konfliktfall Ihr Dokument nicht gespeichert. Konfliktdokumente (s. Abb. 7-2) sind Klone des Originaldokuments, die jedoch zwei zusätzliche Items tragen. Zum einen wird einem solchen Dokument das Item mit Namen $Conflict und Wert "" hinzugefügt, über den ein Konfliktdokument als solches identifiziert wird, und das Item mit Namen $Ref, das einen speziellen Datentyp hat, der einen Link auf ein übergeordnetes Mutterdokument spezifiziert. Der Wert kann jedoch einfach mit getItemValueString gelesen werden, wobei als Ergebnis die Universal ID des Originaldokuments geliefert wird. Ein Konfliktdokument ist also ein Antwortdokument auf das Original. Je nach Bedürfnissen kann es sinnvoll sein, in Ansichten Dokumente, für die gilt „@isavailable ($Conflict)“, in der SELECTFormel auszuschließen, damit der Benutzer nicht mit Speicherkonflikten konfrontiert wird oder im umgekehrten Fall diese bewusst einzuschließen, damit der Benutzer nachträglich entscheiden kann, welche Daten für ihn relevant sind. Keine (!) Kontrolle über Speicherkonflikte haben Sie, wenn der zweite Benutzer und Bearbeiter des betroffenen Dokuments an einem entfernten Server arbeitet. Beide werden zunächst nicht bemerken, dass eine Konfliktsituation vorliegt. Erst bei einer Replikation, bei der die geänderten Daten zwischen den Servern ausgetauscht und die notwendigen Updates auf beiden Seiten durchgeführt werden, fällt diese Situation auf. Es wird dann in jedem Fall ein Konfliktdokument erstellt, das in diesem Fall auch als Replikationskonflikt bezeichnet wird. Domino wird dann entscheiden, welches der Dokumente als Hauptdokument und welches als Antwortdokument gespeichert wird (s. Kap. 2.3.2).
Abb. 7-2 Konfliktdokument (Speicherkonflikt)
280
7 Document
Als Empfehlung und Anhaltspunkt kann Folgendes gesagt werden: Solange in Java im Backend Batchprozesse programmiert werden, hat sich gezeigt, dass es für viele Fälle sinnvoll ist, save (true, false) zu verwenden. Hierbei wird davon ausgegangen, dass der Batchvorgang unerlässliche Wartungsarbeiten vornimmt. Daher ist es sinnvoll, die Wartung in jedem Fall durchzusetzen (enforceSave==true) und keine Datendubletten (makeResponseOnConflict == false) anzulegen, damit nicht später Probleme auftreten, wenn Datensätze doppelt auftauchen. Wenn zusätzlich davon ausgegangen werden kann, dass die Batchvorgänge zu Zeiten durchgeführt werden, die außerhalb der Bürozeiten liegen, werden Sie mit dieser Einstellung gut fahren. Zusätzlich kann die Methode save (boolean enforceSave, boolean makeResponseOnConflict, boolean markAsRead) noch einen dritten boolschen Parameter haben, mit dem festgelegt wird, ob ein durch save gespeichertes Dokument als (für den Benutzer der Session) gelesen markiert werden soll. Wie eingangs erläutert wurde, ist jedes Domino-Objekt auch in Java mit den Rechten des jeweiligen Sessionbenutzers behaftet. Folglich können nur Benutzer Dokumente verändern, die hierfür die entsprechenden Berechtigungen haben. Das erste Mal werden die Berechtigungen überprüft, wenn ein Benutzer versucht, ein Dokument aus der Datenbank in ein Java-Objekt zu laden, also z.B. über getDocumentByUNID. Hierfür muss der Benutzer mindestens Leserechte in der ACL der Datenbank eingeräumt bekommen, wobei nicht vergessen werden darf, dass ein Benutzer, der nicht in einem eventuell vorhandenen Leserfeld gelistet ist, das Dokument nicht sehen darf, d.h. die Methode getDocumentByUNID wird eine NotesException „Invalid universal id“ werfen. Wurde das Dokument erfolgreich in das Java-Document geladen, so kann auch ein Benutzer mit lediglich Leserechten dieses Dokument verändern. Dies geschieht unabhängig von der Datenbank und nur im Java-Objekt. Versucht nun allerdings ein Benutzer ohne Schreibrechte, das Dokument mit save zu speichern, so wird dies mit einer NotesException „Notes error: You are not authorized to perform that operation“ quittiert. Abschließend sei noch darauf hingewiesen, dass save eine teure, also zeitaufwändige Operation ist und nur wohlüberlegt eingesetzt werden sollte. Problemlos kann ein Document während allen Operationen (Ausnahme: computeWithForm) ungespeichert bleiben und erst nach Fertigstellung save aufgerufen werden. Hierdurch werden auch Inkonsistenzen vermieden, die durch Systemausfälle während einer Transaktion aus mehreren Veränderungen an einem Dokument entstehen könnten, würde zwischenzeitlich save mehrfach ausgeführt.
7.1.4.6
Dokumente löschen
Dokumente können ebenso einfach gelöscht werden und – wie könnte es anders sein – dies ist mit ähnlichen Überlegungen behaftet, wie sie beim Speichern eine Rolle spielen.
7.1.4.6 Dokumente löschen
281
Wie beim Speichern kann es auch beim Löschen zu Speicher- oder Replikationskonflikten kommen, wenn der eine Benutzer (oder Batchprozess) ein Dokument löscht und der andere Benutzer ein Dokument verändert und speichert. Folgerichtig kennt die Methode remove (boolean enforceDelete) throws NotesException den Parameter enforceDelete, in dem festgelegt wird, ob die Löschung, die durch remove ausgelöst wird, Vorrang vor anderen Operationen haben soll. Für Batchprozesse gilt hier Ähnliches wie das bereits für save Gesagte. Allerdings müssen Sie sich darüber im Klaren sein, dass, falls ein Benutzer ein Dokument im NotesClient geöffnet hat und dieses Dokument durch remove (true) im Backend gelöscht wird und nun nachträglich der Benutzer dieses Dokument speichert, dieses Dokument neu angelegt wird. Hier gibt es also keine absolute Sicherheit. Vielleicht fragen Sie sich bereits, wie ein gelöschtes Dokument überhaupt noch repliziert werden und folglich Replikationskonflikte verursachen kann, genauer gesagt, kann es vorkommen, dass in bestimmten Replikationssituationen (Löschung und Änderung auf verschiedenen Repliken) ein gelöschtes Dokument wieder sichtbar wird (s. Kap. 2.3ff.). Um auch hier einen sauberen Datenabgleich zu ermöglichen, wurden für Domino-Datenbanken so genannte „Deletionstubs“ eingeführt. Dies sind Dokumente, die bereits gelöscht wurden. Sie sind für alle Anwendungen und Benutzer unsichtbar und können weder über Ansichten noch über datenbankweite Suchfunktionen angesprochen werden, auch wenn sie natürlich über das C++ API angesprochen werden können. Deletionstubs werden dazu verwendet, über alle Repliken einer Datenbank die Information zu übermitteln, dass ein Dokument gelöscht werden muss. Eine Einschränkung ist hier allerdings zu machen. Damit nicht Datenbanken ins Unendliche wachsen, ist in den Datenbankeinstellungen (genauer gesagt, in den Replikationseinstellungen der Datenbank) ein Parameter vorgesehen, der festlegt, wann die Deletionstubs verfallen. In der Regel ist dieser Parameter auf 90 Tage gesetzt, danach dürfen die „untoten“ Dokumente endgültig sterben, natürlich mit reichlich Nebenwirkungen, wenn es eine Replik geben sollte, die länger als diese Zeit nicht repliziert wurde. Wenn Sie das Java-Objekt Document neu instanzieren, dann wird zunächst initial festgestellt, ob es sich bei dem Document um ein Deletionstub handelt. Dies wird nur in absoluten Ausnahmefällen der Fall sein. Die Methode isValid in Document oder ViewEntry zeigt true, falls es sich um kein Deletionstub handelt. Sollen mehrere Dokumente gleichzeitig gelöscht werden, können Sie mit removeAll (boolean enforceDelete) in DocumentCollection und ViewEntryCollection alle Dokumente, die über diese Collections referenziert werden, löschen. Diese Methode ist aus Performancegründen einem einzelnen Löschen der Dokumente über einen Loop vorzuziehen. Sollte nachdem ein Document instanziert wurde ein Dokument durch einen anderen Prozess gelöscht werden, so zeigt zwar isValid weiterhin true (das Dokument war ja ursprünglich nicht gelöscht und gültig, aber dieser Wert wird nicht aktualisiert), aber die Methode isDeleted() spiegelt den aktuellen Stand wider und liefert dann den korrekten Wert true.
282
7 Document
Neben remove existiert seit Version 6 auch die Methode removePermanently(boolean flag) die zum Einsatz kommt, wenn für die Datenbank die so genannte Softdeletion aktiviert war, die Sie mit setOption(Database. DBOPT_SOFTDELETE,true) in Database einschalten können und deren Status mit getOption(Database.DBOPT_SOFTDELETE) überprüft wird. Bei aktiver SoftDeletion verbleiben gelöschte Dokumente nach der Anwendung von remove für einige Stunden (Konfiguration über getUndeleteExpireTime() und setUndeleteExpireTime(int hours)) in der Datenbank, ehe sie endgültig gelöscht werden, können aber in diesem Zustand bereits nicht mehr über Views und Suchen gefunden werden. Um per SoftDeletion gelöschte Dokumente anzuzeigen und dem Benutzer die Möglichkeit zu geben, diese Dokumente wieder „zurückzuholen“, muss in der Datenbank ein spezieller gemeinsamer View eingerichtet werden, der die Eigenschaft „Shared, contains deleted documents“ erhält. Dort muss ein Aktionsbutton zur Verfügung gestellt werden, der z.B. für das Wiederherstellen eines Dokuments die @Formel „@Command([EditRestoreDocument])“ ausführt. Soll vor Ablauf der UndeleteExpireTime ein Dokument endgültig gelöscht werden, kann dies mit removePermanently erreicht werden.
7.2
Profildokumente
Profildokumente wurden in Domino als privilegierte Dokumente konzipiert. Privilegiert bedeutet insbesondere, dass Profildokumente im Hauptspeicher gecached werden und dadurch bei der Verwendung von Profildokumenten erhebliche Zeitgewinne erreicht werden können. Gleichzeitig können Profildokumente nicht wie gewöhnliche Dokumente angelegt und selektiert werden, sondern müssen in Database über getProfileDocument(String baseForm, String key) selektiert oder als DocumentCollection über getProfileDocCollection(String baseForm) geladen werden. Sie können nicht in Views angezeigt oder über die Suchfunktionen (s. Kap. 15) gefunden werden. Ist die Universal ID bekannt, können Profildokumente über getDocumentByUNID geladen werden, über isProfile () wird angezeigt, ob es sich um ein Profildokument handelt. Profildokumente werden ansonsten über das Java-Objekt Document behandelt und erfahren hier keine Sonderbehandlung. Das Konzept der Profildokumente sieht vor, dass diese kategorisiert werden, wobei diese Gruppenaufteilung schlicht über die diesen Dokumenten zugrunde liegende Basismaske erreicht wird, die über den Parameter baseForm beim Erstellen eines Profildokuments gesetzt wird. Diese Maske wird dann auch beim Öffnen im Notes Client verwendet, wenn ein Benutzer das Profildokument ändern oder einsehen möchte. Innerhalb des Clusters der Profildokumente einer bestimmten Maske kann noch ein Schlüssel mit dem zweiten Parameter key hinzugefügt werden, wobei dieser auch null sein darf. Ursprünglich war dieser Key zur Personalisierung vorgesehen und es wurde hier ausschließlich der Benutzername des aktiven Benutzers eingesetzt. Profile ohne diesen zweiten Parameter (mit Wert null) werden daher als allgemeingültig behandelt und auch als Serverprofile bezeichnet. Profildokumente mit diesem Parameter werden auch als Personenprofile bezeichnet. Grundsätzlich steht es
7.2 Profildokumente
283
dem Programmierer natürlich frei, für den Parameter key auch andere Werte anstelle des aktuellen Benutzernamens zu verwenden, gelesen wird dieser Wert über getKey(). Erstellt werden Profildokumente implizit über die Methode getProfileDocument, d.h. es gibt keine eigene Methode zum Erstellen von Profildokumenten und diese Methode hat die Nebenwirkung, dass immer ein neues Profildokument erstellt (und gespeichert) wird, bereits wenn versucht wird, es zu laden, auch wenn es das Profildokument (für diese base-Maske und den Key) noch nicht gibt. Es ist ein häufiger Fehler, zu überprüfen, ob es ein Profildokument mit einem bestimmten Paar von Parametern gibt. Die Anwort bei Verwendung der Methode getProfileDocument ist immer true und erstellt als Nebenwirkung auch noch ein neues Dokument. Um dieses Problem zu umgehen, hat Lotus die Methode getProfileDocCollection eingeführt, mit der der gesamte Cluster von Profilen, die dieselbe Maske verwenden, geladen werden kann. Die zurückgegebene DocumentCollection kann auch leer sein und wird in jedem Fall keine neuen Profildokumente erzeugen. Umgekehrt darf aus der Tatsache, dass Profildokumente automatisch erstellt werden, nicht geschlossen werden, dass Veränderungen, die Sie am Profildokument vorgenommen haben, nicht gespeichert werden müssten. Verwenden Sie auch hier save, um Ihre Änderungen zu manifestieren. Die dem Profil zugrunde liegende Maske wird auch schlicht als Name des Profils bezeichnet und kann über die Methode getNameOfProfile() geladen werden. Der Name der Basismaske wird übrigens nicht automatisch ins Item Form übernommen. Das sollte explizit nachgeholt werden. Verwenden Sie Aliasnamen für den Namen Ihrer Masken, so ist zu bedenken, dass grundsätzlich entweder der Klarname oder der Aliasname für den Parameter baseForm verwendet wird. Entscheiden Sie sich grundsätzlich für eines von beidem, am besten für den Aliasnamen. Falls Sie beides gemischt verwenden, werden beide Profile als voneinander getrennt betrachtet und behandelt und die Verwendung des Aliasnamens als Parameter für die Maske öffnet nicht ein eventuell vorhandenes Profil mit dem Klarnamen als Maske und umgekehrt. Um die Profildokumente schnell und verfügbar zu halten, werden sie im Hauptspeicher gecached. Grundsätzlich können Sie davon ausgehen, dass Veränderungen in der Regel schnell durchgesetzt werden. Allerdings kann ein gelöschtes Profildokument durchaus noch eine Weile im Hauptspeicher verfügbar sein und in der Praxis wurde auch schon beobachtet, dass durch den Aufruf von getProfileDocument ein bereits gelöschtes Profildokument geladen und (mit allen ehemals vorhandenen Items) gespeichert und somit die Löschung aufgehoben wurde. Hier ist also erhöhte Vorsicht geboten. Ein weiterer Unterschied zu „normalen“ Dokumenten liegt in der Replikation. Zu Profildokumenten können keine Replikations- oder Speicherkonflikte entstehen. Es setzt sich in der Regel die letzte Änderung durch, wobei sinngemäß nach den Regeln aus Kapitel 2.3.2 vorgegangen wird, aber keine Konfliktdokumente angelegt werden. Letztendlich ist es sinnvoll, Profildokumente so einzusetzen, wie der Name es bereits nahelegt: Als Profile für Personen oder Server mit häufig benutzten, aber selten geänderten Einstellungen oder Auswahllisten, die lange im Hauptspeicher verbleiben können und sollen.
284
7 Document
7.3
Antwortdokumente
Antwortdokumente haben Sie bereits mehrfach kennengelernt, z.B. als Sonderfall der Speicherkonflikte. Sie zeichnen sich durch die Eigenschaft aus, dass sie nicht alleine bestehen können, sondern immer an ein übergeordnetes Dokument geknüpft sind, dessen Universal ID als Link-Item $Ref im Antwortdokument referenziert wird. Antwortdokumente wurden ursprünglich als das konzipiert, was der Name nahelegt: Als Antwort auf eine Anfrage in einem Diskussionsforum. Grundsätzlich werden Antwortdokumente nach wie vor häufig in Notes-Anwendungen eingesetzt, insbesondere dann, wenn kenntlich gemacht werden soll, dass ein Dokument (Antwortdokument) abhängig von einem übergeordneten Dokument ist. Zum Beispiel könnten für eine Adressdatenbank Firmenanschriften als Hauptdokumente angelegt werden und zur Firma gehörige Kontakte von Mitarbeitern als Antwortdokumente. Hierdurch können zwei Dinge in der Anwendung erreicht werden. Zum einen lassen sich Kombinationen aus Haupt- und Antwortdokumenten leicht in Ansichten durch so genannte Kategorisierung übersichtlich gestaffelt anzeigen und zum anderen wird automatisch erreicht, dass die Antwort immer eine Referenz auf das Hauptdokument hat. Ein Beispiel für die Verwendung von Antwortdokumenten sind die durch einige der Tools der Beispieldatenbank erzeugten Dokumente. Markieren Sie z.B. ein Dokument in einer der Ansichten in djbuch.nsf und starten Sie im Menu „Actions“ -> „Tools“ die Aktion „2. Items anzeigen“ und es wird ein Antwortdokument auf das markierte Dokument erstellt, das eine Kurzbeschreibung aller im markierten Dokument gefundenen Items enthält (s. Abb. 7-3). Da der Ansicht die Eigenschaft „Show response documents in a hierarchy“ mitgegeben wurde, tauchen diese Dokumente automatisch hierarchisch unterhalb des Hauptdokuments auf. Dies ist recht praktisch, jedoch hat die Verwendung von Antwortdokumenten auch einige Nachteile: •
•
•
Antwortdokumente können in einer Ansicht nur zusammen mit ihren Hauptdokumenten angezeigt werden. Enthält die Ansichtsauswahl das Hauptdokument nicht, so wird in keinem Fall das Antwortdokument angezeigt. Was ursprünglich als Funktion gedacht war – schließlich machen Antworten nur Sinn, wenn es auch eine Frage (Hauptdokument) gab –, kann je nachdem, welchen Inhalt die Antwortdokumente haben, auch zum Problem werden, wenn Inhalte der Anwortdokumente auch alleine benötigt werden. Aus dem ersten Punkt folgt direkt, dass große Schwierigkeiten zu erwarten sind, falls ein Hauptdokument gelöscht wird, ohne dass zuvor die Antwortdokumente gelöscht wurden. Hier muss immer für eine saubere Reihenfolge beim Löschen gesorgt werden. Noch problematischer wird es, wenn ein Benutzer ein Dokument ausschneidet und sich dann doch entscheidet dieses Dokument wieder einzufügen. Da Dokumenten, die per Copy-Paste eingefügt werden, eine neue Universal ID von Domino zugeordnet wird, geht die Relation zwischen Hauptdokument und Antwort verloren.
7.3 Antwortdokumente
285
Abb. 7-3 Antwortdokument (Beispiel)
•
Wird ein Antwortdokument per Copy-Paste kopiert und ist beim Einfügen ein Hauptdokument markiert, wird das eingefügte Dokument automatisch zur Antwort dieses Dokuments. Dies kann sowohl ein Vor- als auch ein Nachteil sein, je nachdem, ob dieses Konzept der gewünschten Businesslogik entspricht.
Um Antwortdokumente handzuhaben, gibt es verschiedene Methoden in Document und View, wobei auf letztere in Kapitel 10 ff. eingegangen werden soll. getResponses() in Document liefert eine unsortierte DocumentCollection, mit allen direkt vom Document abhängigen Antwortdokumenten. Sollten Antwortdokumente ihrerseits wieder Antwortdokumente besitzen, so werden diese nicht eingeschlossen. Ob ein Document eine Antwort ist, können Sie mit isResponse() prüfen. Ein Antwortdokument erstellen Sie, indem Sie zunächst ein Dokument myDoc erstellen und für dieses Dokument dann die Methode myDoc.makeResponse(parentDoc) aufrufen, wobei als Parameter das Dokument übergeben werden muss, das zum Hauptdokument für diese Antwort werden soll und hierfür auch wiederum ein Antwortdokument zum Einsatz kommen darf, wodurch so genannte Antworten auf Antworten entstehen. Einen Rückgabewert hat diese Methode nicht, wirft aber eine NotesException, falls Fehler auftreten. Um für ein Antwortdokument die Universal ID des (direkt) übergeordneten Hauptdokuments zu ermitteln, kann die Methode getParentDocumentUNID verwendet werden, die diese UNID als String zurückgibt.
286
7 Document
7.3.1
Antwortreferenzen über Self ID und Parent ID
Antwortdokumente in Domino sind so etwas wie eine rudimentäre Funktion relationaler Verknüpfungen in der Welt nicht-struktierter Daten von Domino. Mit den oben erwähnten Nebenwirkungen lässt sich nicht immer leben und es gibt eine Alternative, die hier kurz angerissen werden soll. Auch wenn es sich hier um kein ausdrücklich Java-spezifisches Problem handelt, werden Sie im Domino-Umfeld früher oder später mit dieser Fragestellung konfrontiert werden. Hierfür müssen folgende Aufgaben gelöst werden: 1 2 3 4 5 6
Relational verknüpfte Dokumente müssen eine Referenz auf das Hauptdokument erhalten. Relational verknüpfte Dokumente müssen zusammen mit ihren Hauptdokumenten selektiert werden können. Relational verknüpfte Dokumente sollen in Ansichten hierarchisch dargestellt werden können. Beim Löschen von Hauptdokumenten sollen Antworten nicht im unreferenzierten Niemandsland verschwinden. Beim Ausschneiden und Einfügen von Hauptdokumenten sollen die Referenzen von Antworten nicht zerstört werden. Hauptdokumente sollen alle (in der gesamten Hierarchie) relational verknüpften Antwortdokumente kennen.
Werden diese Aufgaben gelöst, können Sie einerseits das Konzept der Antwortdokumente auf eine echte Relationalität erweitern ohne andrerseits die Funktionalität der Antwortdokumente zu verlieren. Der erste Schritt für dieses Vorhaben ist zunächst, dass jedes Dokument eine eindeutige ID erhalten soll, die unabhängig von der Universal ID ist und für die Referenzierungen genutzt werden soll: Die SelfID. Erstellen Sie für jedes neue Dokument ein Item F_selfID, in das Sie die Universal ID beim ersten Erstellen des Dokuments als String kopieren. Danach wird dieses Feld unabhängig von der UNID und kann durch Domino nicht mehr (unerwartet) verändert werden. Dokumente, die Hauptdokumente referenzieren sollen, benötigen eine Referenz auf dieses Hauptdokument: Die ParentID. Sie erhalten ein Feld F_parentID, in das die SelfID des gewünschten Hauptdokuments eingetragen wird. Denkbar ist hier sogar eine „n zu m“-Relation, wenn das Antwortdokument im Feld F_parentID eine Liste von Strings enthält. Konzipieren Sie für diesen Fall dieses Feld als Vector. Die Einführung von F_parentID erfüllt Forderung 1. Forderung 2 wird erfüllt, indem dafür gesorgt wird, dass es eine konsistente Eigenschaft von Haupt- und Antwortdokumenten gibt, die es erlaubt, beide mit denselben Auswahlkriterien zu selektieren. In der Regel werden dies die Maskennamen der beiden sein, z.B.: SELECT Form="FO_Haupt":"FO_Antwort" & F_meineBe-
7.3.1 Antwortreferenzen über Self ID und Parent ID
287
dingung=@true. (Beachten Sie, dass der Listenvergleich Form= "FO_Haupt":"FO_Antwort" gleichbedeutend ist mit Form="FO_Haupt" | Form="FO_Antwort"). Der Vollständigkeit halber sei hier darauf hingewiesen, dass
diese Ansichten-Auswahl-Formel auch Antwortdokumente auswählt, die entweder keinen Eintrag in der ParentID haben, oder deren ParentID auf ein nicht vorhandenes Dokument oder ein solches mit Form != "FO_Haupt" verweist. Dies ist entsprechend Forderung 4 erwünscht, insbesondere, wenn die Dokumente im Notes Client angezeigt werden. Sollen für jedes Hauptdokument die zugehörigen Antwortdokumente selektiert werden, können diese über eine Suche mit db.search gefunden werden oder mit dem für Forderung 6 beschriebenen Mechanismus die Referenzen auf die Antwortdokumente im Hauptdokument gehalten werden. Für die Selektion der Haupt- und deren Antwortdokumente im Java Backend ist diese Forderung noch leichter zu erfüllen. In Database gibt es die Methode search, mit der Sie mit einer entsprechenden @Formel die Antwortdokumente für ein bekanntes Hauptdokument hauptDoc auswählen können, wobei für dieses Beispiel als zusätzliche Bedingung angenommen wird, dass nur Dokumente gewünscht werden, die die Maske „FO_dokument_k2“ haben: DocumentCollection dc = db.search( "SELECT Form=\"FO_dokument_k2\" & F_parentID = \"" + hauptDoc.getUniversalID() + "\"");
Forderung 3 erreichen Sie, wenn Sie in jedem Antwortdokument in einem Kategorisierungsitem ein (textliches) Merkmal des Hauptdokumentes (redundant) zusätzlich speichern, z.B. den Titel des Dokuments. Für Hauptdokumente wird dann im Kategorisierungsitem der Titel gespeichert, für Antwortdokumente die Kategorie des Hauptdokuments und der eigene Titel zusammen, durch Backslash „\“ getrennt und für Antwort-von-Antwort-Dokumente wird die wiederum durch den eigenen Titel erweiterte Kategorie des übergeordneten Dokuments gespeichert. In der Ansicht kann dann in einer (üblicherweise der ersten) Spalte das Kategorisierungsitem angezeigt werden. Domino hierarchisiert diese Spalte automatisch am Backslash-Zeichen, wenn für die Spalte die Kategorisierung aktiviert ist. Forderung 4 ergibt sich von allein, da aus Notes-Sicht nur noch Hauptdokumente vorliegen, so dass keine unreferenzierten Antwortdokumente entstehen können. Sollte es Dokumente geben, für die es keine Referenz für das Feld F_selfID gibt, können diese Dokumente problemlos über Suchen etc. selektiert werden. Forderung 5 ergibt sich aus der SelfID, die nachträglich nicht verändert wird. Solange es zu einer ParentID im Antwortdokument ein Dokument mit dieser SelfID gibt, besteht die Referenzialität. Um Forderung 6 zu erfüllen, müssen Sie beim Anlegen von Antworten im zugehörigen Hauptdokument ein Feld F_responses pflegen, in dem Sie einen Vektor mit allen Antworten führen. Dies erfordert aber, dass Sie alle Ansichten mit so genannten Paste- und Deletion-Handlern ausstatten, die überprüfen, wenn Lösch- oder Einfügeoperationen durch den Benutzer stattfinden, so dass diese Referenzen up-to-date gehalten werden können. Zusätzlich müssen alle von Ihnen programmierten Methoden
288
7 Document
beim Erstellen oder Löschen von Dokumenten diese Relationalität berücksichtigen und ebenfalls die Referenzlisten im Hauptdokument pflegen. Dies gilt auch für Operationen, die Sie dem Benutzer über Agenten, Aktionsbuttons oder Aktionen zur Verfügung stellen.
7.4
Attachments
Attachments werden in Domino über das EmbeddedObject abgebildet und können entweder an ein RichTextItem oder an ein Document angehängt werden, wobei letztere Möglichkeit die Ausnahme ist und im Wesentlichen nur aus historischen Gründen noch zur Verfügung steht. Ein EmbeddedObject kann ein Dateianhang, ein OLE-Objekt oder ein OLE-Objectlink sein, wobei die letzteren beiden in ihrer Repräsentation als EmbeddedObject auf OS/2, Unix und Macintosh nicht unterstützt werden. Die verschiedenen Möglichkeiten, Anhänge und OLE-Objekte im Dokument oder im RichText zu finden, führen gerne zu Verwirrung. Eine Übersicht gibt Tabelle 7-5. Ist ein Feld mit „JA“ markiert, bedeutet dies, dass die Methode in der linken Spalte in der Lage ist, Objekte der markierten Spalte zu referenzieren (bearbeiten, lesen, in eine Auswertung einzubeziehen). Entscheidend sind die Spalten zwei und drei oder anders formuliert: EmbeddedObjects sollen aktuell nur noch in RichTextItems gespeichert werden und sollten auch durch die Methoden in RichTextItem bearbeitet bzw. geladen werden, wobei einige Methoden in Document für die Bearbeitung von Attachments in RichTextItems sehr hilfreich sind. Es stehen entsprechende Methoden in RichTextItem und RichTextNavigator (seit Version 6.5) zur Verfügung. Für diejenigen Methoden, die aktuell sind und die Sie auch nutzen sollten, wurden die Einträge durch Fettdruck in der Tabelle hervorgehoben. Für die Bearbeitung von EmbeddedObjects sollen zunächst Dateianhänge untersucht werden. Seit Version 6.5 steht in Domino der so genannte RichTextNavigator zur Verfügung, der mit einem RichTextItem über createNavigator() erstellt werden kann. Im RichTextNavigator können die verschiedenen Objekte, die ein RichTextItem enthalten kann, komfortabel angesprochen werden, Dateianhänge werden über die Konstante RichTextItem.RTELEM_TYPE_FILEATTACHMENT referenziert. Der erste Schritt, um einen in einem RichTextItem vorliegenden Dateianhang zu erhalten, ist, ein entsprechendes EmbeddedObject als Referenz auf diesen Dateianhang zu erhalten: rt = (RichTextItem) doc.getFirstItem("F_yourRichTextItem"); rtn = rt.createNavigator(); eo = (EmbeddedObject) rtn.getFirstElement(RichTextItem.RTELEM_TYPE_FILEATTACHMENT);
7.4 Attachments
289
RichTextNavigator bietet etliche weitere Methoden, mit denen die Objekte eines RichTextItems geladen werden können und mit denen zwischen den einzelnen Objekten navigiert werden kann (s. Kap. 11.1). Um einen Dateianhang an ein RichTextItem anhzuhängen, nutzen Sie embedObject: rt = (RichTextItem) doc.getFirstItem("F_yourRichTextItem"); EmbeddedObject eo = rt.embedObject(EmbeddedObject.EMBED_ATTACHMENT, "", filePath, "yourObjectName");
Das EmbeddedObject ist die Basis für die weiteren Operationen. Um eine Datei ins Dateisystem abzulösen, können sie die Methode extractFile verwenden, die keinen Rückgabewert hat, aber eine NotesException wirft, falls die Operation nicht ausgeführt werden konnte. Denkbare Exceptions sind zum Beispiel „NotesException: Tabelle 7-5 Methoden und Properties mit Zugriff auf EmbeddedObject OLE im Doc
Anhänge aus R3
OLE/ Anmerkung 1 aus R3
JA
Anhänge im Doc -
-
-
-
(3)
JA
-
-
-
-
-
RichTextNavigator über RichTextItem.create Navigator() erstellen (4)
Erstellung Programmatisch Erstellung durch Benutzer
Ist true, wenn ein beliebiger Dateianhang oder ein beliebiges EmbeddedObject vorhanden ist. (6). Nur Attachments.
290
7 Document
Tabelle 7-5 Methoden und Properties mit Zugriff auf EmbeddedObject (1) Dateianhänge in Domino R2 wurden direkt im Dokument gespeichert. (2) Falls eine Maske ein eingebettetes OLE-Objekt enthält und mit Hilfe dieser Maske ein Dokument erstellt wird, hat der Benutzer die Möglichkeit dieses OLE-Objekt zu bearbeiten. Bearbeitet UND speichert der Benutzer dieses Objekt, dann wird die bearbeitete Fassung direkt im Dokument als EmbeddedObject gespeichert. (3) type kann den Wert EmbeddedObject.EMBED_ATTACHMENT oder EmbeddedObject.EMBED_OBJECT haben. Objectlinks mit Type EmbeddedObject. EMBED_OBJECTLINK können nicht in Java erstellt werden. objectName ist optional und wird als interner Name des Objekts verwendet. Class muss immer der leere String "" sein; OLE-Objekte aus einem Speicherobjekt über die Angabe von class statt über die Angabe einer Datei in fileName zu laden, ist in Java nicht möglich. (4) Neben getFirstElement stehen noch weitere Methoden zur Selektion und Navigation zur Verfügung. (5) Der intern verwendete Dateiname muss bekannt sein. Kann z.B. über getName() in EmbeddedObject bezogen werden. Das EmbeddedObject kann wiederum z.B. über getEmbeddedObjects in RichTextItem bezogen werden. (6) Der intern verwendete Dateiname muss bekannt sein. Kann z.B. über getName() in EmbeddedObject bezogen werden. Das EmbeddedObject kann wiederum z.B. über getEmbeddedObjects in Document bezogen werden. (7) Aus allen Items müssen diejenigen mit type == Item.ATTACHMENT oder Item.EMBEDDEDOBJECT herausgesucht werden.
Notes error: Access to data denied“, falls für den Zielpfad der abzulösenden Datei keine Schreibrechte vorliegen oder „NotesException: Notes error: File cannot be created“, falls ein erwartetes Verzeichnis nicht vorliegt. Ein EmbeddedObject hat immer einen type, der mit getType () bezogen werden kann, wobei eine int-Konstante zurückgegeben wird, die die Werte EmbeddedObject.EMBED_OBJECTLINK (1452), EmbeddedObject.EMBED_OBJECT (1453) oder EmbeddedObject.EMBED_ATTACHMENT (1454) annehmen kann. Wie Tabelle 7-5 bereits andeutet, kann eine Referenz auf ein EmbeddedObject auch darüber bezogen werden, dass entweder in RichTextItem alle EmbeddedObjects mit getEmbeddedObjects als Vector geladen werden oder in Document alle OLE EmbeddedObjects über getEmbeddedObjects ebenfalls als Vector bezogen werden. Einzelne EmbeddedObjects können in RichTextItem über getEmbeddedObject geladen werden, sofern der interne (!) Name bekannt ist. In Document besteht zumindest auf Dateianhänge über getAttachment Zugriff, wobei auch hier der interne Name bekannt sein muss. Der interne Name wurde entweder durch embedObject im letzten Parameter gesetzt, oder von Domino vergeben. Um festzustellen, ob es überhaupt irgendwelche EmbeddedObjects im Dokument gibt, kann die Methode hasEmbedded() verwendet werden, die imme true liefert, falls es ein solches Objekt gibt, egal wo es innerhalb des Dokuments gespeichert ist, oder ob es womöglich über eine alte Notes-Version erstellt wurde und nur über Umwege erreichbar ist.
7.4 Attachments
291
Die Liste aller Dateianhänge zu berechnen, ist etwas schwieriger. Leider bietet Domino hierfür keine Java-Methode an, aber es gibt die @Formel @attachmentNames, die genau dieses Ergebnis liefert. In der Session können @Formeln über public Vector evaluate (String atFormula [, Document doc]) evaluiert werden. Rückgabewert ist ein Vector, der nur einen einzelnen Wert enthält, falls der Rückgabewert der @Formel ein diskreter Wert ist; falls der Rückgabewert eine Liste ist (eine anderer Rückgabewert ist nicht zu erwarten), wird der Vector entsprechend mit den Werten gefüllt. Listing 7-1 zeigt einen kleinen Agenten, der die verschiedenen Möglichkeiten, mit Attachments umzugehen, zeigt. Der Agent ist der Beispieldatenbank beigefügt und kann über „Actions“ -> „Kapitel 7“ -> „1. Doc mit Attachment erstellen / Attachment anhängen“ gestartet werden. Sie müssen ein Dokument markieren, bevor Sie den Agenten starten. Falls Sie den Agenten zum ersten Mal aufrufen, so erstellt er ein neues Dokument mit der Maske „FO_dokument_k2“, das in der Ansicht „Kapitel 2“ -> „Dokument“ erscheint. Der Agent versucht in Ihrem lokalen Lotus-Dataverzeichnis die Dateien „image.gif“ und „binary.gif“, die sich immer dort befinden (sollten), zu
import java.util.Vector; import lotus.domino.*; public class AttachmentAgent extends AgentBase { private static final String PATH = "C:\\temp\\"; public void NotesMain() { Document doc = null; RichTextNavigator rtn = null; Database db = null; EmbeddedObject eo = null; RichTextItem rt = null, hiddenRT = null; try { //Vorbereitung Session session = getSession(); AgentContext agentContext = session.getAgentContext(); String base = System.getProperty("java.home"); base = base.substring(0,base.length()-4); db = agentContext.getCurrentDatabase(); doc = agentContext.getDocumentContext(); //Neues Dokument erstellen oder Context Document laden... if (doc != null && !(doc.getItemValueString("F_Titel")) .equals("Demo Dokument mit Attachment")) { doc = db.createDocument ();} doc.replaceItemValue("Form", "FO_dokument_k2"); doc.replaceItemValue("F_Titel", "Demo Dok. mit Attachment"); //RichText Feld 1 -> nicht sichtbar in der Maske rt = (RichTextItem) doc.getFirstItem ("F_attachment"); if (rt == null) { rt = doc.createRichTextItem("F_attachment");} rt.embedObject(EmbeddedObject.EMBED_ATTACHMENT, "", base + "\\data\\image.gif", "demoGif"); //RichText Feld 2 -> sichtbar in der Maske rt = (RichTextItem) doc.getFirstItem("F_richText"); if (rt == null) { rt = doc.createRichTextItem("F_richText");} rt.embedObject(EmbeddedObject.EMBED_ATTACHMENT, "", base + "\\data\\binary.gif", "demoGifVisible");
laden und dem RichText des Dokuments anzuhängen. Der Agent geht von einer Standardinstallation unter Windows aus. Mögliche Dateipfadfehler müssten Sie direkt im Desiger im Agenten anpassen.
7.4 Attachments
293
Wird der Agent nun ein zweites Mal gestartet und dabei ein zuvor durch den Agenten erstelltes Dokument markiert, erstellt er kein neues Dokument, sondern versucht nun das vorhandene Dokument um weitere Anhänge zu ergänzen. Ein derartiges Dokument sehen Sie in Abb. 7-4. Dabei werden folgende Techniken angewendet (Listing 7-1): Nach den üblichen Vorbereitungen eines Domino-Java-Agenten, muss er zunächst unterscheiden, ob der Agent zum ersten Mal gestartet wird, oder ob ein bereits vorhandenes Dokument manipuliert werden soll, was für das Beispiel schlicht am Titel erkannt wird und wobei bei Bedarf in der aktuellen Datenbank ein neues Dokument angelegt wird. Im nächsten Schritt wird eine Referenz auf ein RichTextItem benötigt . Der Agent wird zwei RichTextItems mit Anhängen füllen. Eines, für das es ein korrespon-
Abb. 7-4 Dateianhänge in Richtext und Dokument
294
7 Document
dierendes Feld in der Maske gibt, und eines, für das es kein Feld in der Maske gibt und das daher gewissermaßen unsichtbar bleibt. Hieran können Unterschiede in der Verarbeitung aufgezeigt werden. Mit Hilfe von getFirstItem wird versucht das „versteckte“ RichTextItem zu laden. Ob es neu angelegt werden muss, kann daran erkannt werden, ob getFirstItem null zurückgegeben hat . Vergessen Sie nicht den Cast auf (RichTextItem). Der Dateianhang wird wie eingangs beschrieben mit embedObject angehängt. Diese Methode hat vier Parameter. Der Erste ist eine int-Konstante, die den oben bereits beschriebenen Typ angibt. Der zweite Parameter ist theoretisch dafür vorgesehen, OLE-Objekte über deren Klassennamen zu erzeugen, was aber in Java nicht unterstütz wird und muss für die Erstellung von Dateianhängen immer als leerer String übergeben werden. Der dritte Parameter ist ein String, der den absoluten Pfad zu der anzuhängenden Datei enthält, und schließlich ist der vierte Parameter ein String, der optional den intern zu verwendenden Namen bestimmt. Dieser Parameter ist optional und darf auch als leerer String übergeben werden. Dieser Vorgang wird für das Item F_richText, für das es ein gleichnamiges Feld in der Maske gibt, wiederholt . In den Agenten wurde ein kleines Logging eingebaut, mit dem eine wichtige Tatsache demonstriert wird: RichTextItems sind erst dann Bestandteil des DocumentObjekts, sobald das Document gespeichert wurde. Folgerichtig ergibt der Aufruf von getEmbeddedObjects() in RichTextItem einen, um ein Element zu kleinen, Vector – das aktuell angefügte Item wird nicht angezeigt. Dieser Vector wird erst nach dem save korrekt geladen. Außerdem lässt sich hier deutlich erkennen, dass nur die EmbeddedObjects des RichtTextItems gezählt werden. Das im zweiten RichTextItem angefügte Attachment wird hier nicht geladen. Zusätzlich wird in dieser Logging-Ausgabe noch getEmbeddedObjects() in Document angezeigt, genauer gesagt die Größe dieses Vector. Dieser bleibt so lange leer, wie Sie nicht manuell ein OLE-Objekt in das RichText Feld einfügen, da diese Methode in Document nur OLE-Objekte, jedoch keine Dateianhänge anzeigt. Nachdem der Agent das erste Mal gelaufen ist, sollte im RichText Feld F_richText die Datei binary.gif und folgender Text vorliegen: Vor dem Speichern: 0/0 Nach dem Speichern: 1/true Datei erfolgreich nach C:\temp\binary.gif abgelöst. Datei erfolgreich nach C:\temp\image.gif (interner Name: image.gif) abgelöst. Datei erneut erfolgreich nach C:\temp\_erneut_image.gif abgelöst. Alle Anhänge: [image.gif, binary.gif]
Die Ausgabe zeigt wie erwartet an, dass vor dem Speichern des RichTextItems, obwohl bereits Dateien angehängt wurden, noch keine EmbeddedObjects über getEmbeddedObjects geladen werden können („0/0“) und dass nach dem Speichern das
7.4 Attachments
295
eine Attachment in diesem RichTextItem erkannt wird, nicht jedoch das zweite Attachment im anderen RichTextItem. Has EmbeddedObjects liefert den korrekten Wert true („1/true“). Die folgenden Zeilen geben über das erfolgreiche Ablösen der Anhänge (s.u.) Auskunft. Die letzte Zeile zeigt das Ergebnis des Tricks mit der @Formel zum Bestimmen aller Namen von Dateianhängen. Nach dem zweiten Lauf des Agenten werden erneut die Bilder binary.gif an das sichtbare und image.gif an das „unsichtbare“ RichTextItem angehängt. Auffällig sind zwei Dinge: Eigentlich wäre zu erwarten gewesen, dass die Elemente des RichTextItems F_attachment nicht in der Ansicht des Dokuments hätten auftauchen dürfen, schließlich gilt dies auch für jedes andere Item, das kein korrespondierendes Feld in der Maske hat. Hier macht Domino eine Ausnahme. Dateianhänge werden am unteren Rand der Maske angezeigt (Abb. 7-4), sofern sie nicht in einem RichTextItem sichtbar dargestellt werden können. Der zweite wichtige Unterschied ist die Bezeichnung der jeweils im zweiten Durchlauf angehängten Datei. Während im sichtbaren RichTextFeld die Datei korrekt zweimal als binary.gif angezeigt wird, taucht für den zweiten Eintrag von image.gif ein automatisch generierter Name auf. Dies ist der interne Name des Dateianhangs. Wenn Sie sich Abb. 7-4 ansehen, können Sie in der Liste aller vorhandenen Dateianhänge erkennen, dass auch das zweite binary.gif einen automatisch generierten internen Namen hat. Domino zeigt für „versteckte“ Items den internen und im sichtbaren RichText-Feld den ursprünglichen Dateinamen an. Den internen Namen können Sie immer aus dem referenzierten EmbeddedObject als getName() beziehen und den ursprünglichen Dateinamen als getSource(). Nach dem ersten Upload sind diese Werte übrigens noch identisch, da Domino keine automatischen Namen wegen der Namensdoppelung generieren musste. Für das Ablösen der Anhänge macht dies für Sie übrigens keinen Unterschied. Entweder Sie kennen den Dateinamen und können sie über getEmbeddedObject in RichTextItem oder über getAttachment in Document ablösen oder Sie referenzieren das Objekt auf andere Weise (s.u.), wodurch sich die Kenntnis der Namen erübrigt. Im Folgenden geht es nun darum, die Attachments wieder vom Dokument abzulösen: Im ersten Beispiel wird der bereits skizzierte Weg beschritten. Ein RichTextNavigator liefert diesmal über getLastElement(RichTextItem.RTELEM_TYPE_FILEATTACHMENT) das letzte File-Attachment-Objekt. Rückgabewert von getLastElement (und den anderen get...Element-Methoden) ist übrigens ein Objekt der Klasse Base. Dieser einmalige Ausnahmefall für die explizite Verwendung von Base ist hier unerlässlich, da viele verschiedene Domino-Objekte zurückgegeben werden können, die aber alle Base erweitern. extractFile verwendet in dem Beispiel getName(), das den internen Namen des Objekts liefert, in extractFile als Dateiname. Sollte diese Datei schon im Dateisystem vorliegen, wird sie ohne Fehlermeldung überschrieben. Also Vorsicht!
296
7 Document
Das zweite Beispiel extrahiert zur Abwechslung den Anhang aus dem zweiten RichTextItem. Beachten Sie, dass die beschriebenen Methoden unabhängig davon sind, ob wie in unserem Fall ein Item in der Maske durch ein Feld referenziert und somit bearbeitbar ist oder nicht. Um die Datei direkt aus dem RichTextItem ablösen zu können, werden zunächst alle im Item gefundenen EmbeddedObjects als Vector über getEmbeddedObjects geladen und dann das gewünschte über die Elemente des Vector referenziert (hier das letzte Element im Vector). Um beim Speichern im Dateisystem den ursprünglichen Dateinamen wieder herzustellen wird hier getSource verwendet, der interne Name aus getName wird zur Verwendung im nächsten Beispiel zwischengespeichert. Da nun ein Name eines Attachments bekannt ist, kann im dritten Beispiel über getAttachment in Document der Anhang erneut abgelöst werden. Wichtig ist hier die Verwendung des internen Namens. Alle Ablösevorgänge werden als Text-Log im RichTextItem selbst hinterlegt und dabei noch die verschiedenen Varianten von internem und ursprünglichem Dateinamen angezeigt. Zu guter Letzt wird dann noch die Liste der Namen aller Anhänge ausgegeben. Auch hier werden die internen Namen angezeigt. Da Sie nun die Behandlung von Dateianhängen perfekt beherrschen :-), folgt die Beschreibung des Handlings von OLE-Objekten, die in Dokumenten eingebunden sein können. Beachten Sie, dass diese Art von eingebetteten Objekten nur für Benutzerinteraktionen interessant ist. OLE-Objekte können vom Benutzer durch einen Doppelklick auf das Objekt in den Bearbeitenmodus versetzt werden und innerhalb des Notes Clients aber mit den originären Methoden des Ursprungsprogramms bearbeitet werden. Wenn Sie den Cursor versuchshalber in ein RichText-Feld im Notes Client platzieren und dann den Menübefehl „Create“ -> „Object“ wählen, können Sie manuell ein OLE-Objekt erstellen und das Verhalten überprüfen. Der Bezug eines OLE EmbeddedObject erfolgt auf dem gleichen Weg wie für Attachments beschrieben, wobei Sie Tabelle 7-5 beachten müssen. Allerdings scheint es so, dass die Java-Implementierung von RichTextNavigator für die Verarbeitung von OLE-Objekten in der Client-Version 6.5.3 noch nicht perfekt ist, da in der Praxis eine gewisse Unzuverlässigkeit beobachtet wurde und nicht immer alle Objekte gefunden wurden. Die äquivalenten LotusScript-Befehle unterliegen nicht dieser Einschränkung. Hilfreich ist es, wenn Sie RichTextItems nicht innerhalb von Tabellen platzieren, da offenbar hiervon abhängt, wie gut der RichTextNavigator über getFirstElement und findFirstElement usw. die Objekte referenzieren kann. Der Beispieldatenbank wurde die Aktion „Actions“ -> „Kapitel 7“ -> „3. Embedded Objects via Java RTN anzeigen“ beigefügt. Er versucht in einem markierten Dokument das RichTextItem „F_richText“ zu laden und sucht dort nach verschiedenen RichText-Elementen, wie OLE-Objekten, Dateianhängen, Abschnitten oder Tabellen und Link-Verknüpfungen, wobei die Mechanismen von RichTextNavigator genutzt werden. Zu den Objekten zeigt er verschiedene Werte an, z.B. die unterschiedlichen Ergebnisse von findFirstItem (gibt einen boolean-Wert zurück und zeigt an, ob es ein Objekt des gewünschten Typs gibt) und getFirstItem (zeigt an, ob das Objekt geladen werden konnte).
7.4 Attachments
297
Abb. 7-5 Eigenschaften eines OLE EmbeddedObject im Java-Agenten
getEmbeddedObjects() in RichTextItem arbeitet zuverlässig auch in Java. Hierfür wurde der Beispielagent „Actions“ -> „Kapitel 7“ -> „2. Embedded Objects via Java anzeigen“ beigefügt. Eine typische Ausgabe sehen Sie in Abb. 7-5. Ist ein EmbeddedObject vom Typ EmbeddedObject.EMBED_OBJECT so kann über die Methode getVerbs() ein Vector aus String bezogen werden, der die so genannten Verbs des OLE-Objekts, also die vom Objekt akzeptierten interaktiven Methoden, anzeigt. Ein OLE-Objekt kann ebenfalls einem RichTextItem über embedObject hinzugefügt werden. Verwenden Sie den Typ EmbeddedObject.EMBED_OBJECT: rt = (RichTextItem) doc.getFirstItem("F_richText"); rt.embedObject(EmbeddedObject.EMBED_OBJECT,"","/your/full/filePath","internalName"); doc.save(true,false);
In LotusScript kann im zweiten Parameter eine Klasse statt im dritten Parameter ein Dateipfad, aus dem das Objekt gebildet werden soll, angegeben werden. Dies ist in Java nicht möglich. Auch hier wurden leider Unzuverlässigkeiten beobachtet. Einen Beispielagenten finden Sie unter „Actions“ -> „Kapitel 7“ -> „4. Embedded Object via Java anhängen“. Zum Vergleich der Funktionalität wurde der Agent „Kapitel 7\2.“ (die Namen wurden abegekürzt) zusätzlich in LotusScript als Agent „Kapitel 7\5.“ und der Agent „Kapitel 7\4.“ als Agent „Kapitel 7\6.“ ebenfalls in LotusScript implementiert. Der Agent „Kapitel 7\5.“ analysiert ein markiertes Dokument und gibt Informationen zu eventuell im RichTextItem „F_richText“ enthaltenen OLE-Objekten aus und zeigt sie in der Informationsleiste an, die angeklickt werden muss, um vollständig angezeigt zu werden (s. Abb. 7-6). Den Java-Sourcecode finden Sie im defaultPackage von „01 Examples“ und natürlich in den beigefügten Agenten der Beispieldatenbank „djbuch.nsf“, den LotusScript-Code finden Sie ebenfalls direkt über den Domino Designer in der Beispieldatenbank unter den erwähnten Namen.
298
7 Document
Abb. 7-6 Eigenschaften eines OLE EmbeddedObject im LotusScriptAgenten
Sollen größere Projekte mit OLE umgesetzt werden, empfiehlt es sich zur Zeit, die Verwendung von LotusScript zu erwägen, zumal LotusScript als VisualBasic-Derivat ohnehin technisch näher an den OLE-Objekten ist. Dateianhänge dagegen können perfekt über Java in die Dokumente eingebettet werden, zumal Ihnen für das eigentliche Dateihandling viele nützliche Methoden in Java bereitstehen.
7.4.1
Übersicht über die Methoden zur Attachmentbearbeitung
Attachments können in Domino ähnlich wie diskrete Datentypen in Items eines Notes-Document gespeichert werden. Verwendung findet hier das RichTextItem, in dem sowohl Dateianhänge als auch OLE-Objekte und OLE-Objectlinks gespeichert werden können. Es sind verschiedene Einschränkungen zu beachten, welche Objekttypen auf welchen Plattformen und durch welche Methoden gehandhabt werden können (Tabelle 7-5). Referenziert werden Attachments und OLE-Objekte durch EmbeddedObject, das über verschiedene Methoden in RichTextItem, RichTextNavigator und Document bezogen werden kann. In RichTextItem: • public EmbeddedObject embedObject(int type, String class, String fileName, String objectName) throws NotesException
Für type == EmbeddedObject.EMBED_ATTACHMENT: Fügt dem aufrufenden RichTextItem das Attachment aus der Datei fileName hinzu. fileName ist ein absoluter Dateipfad. class muss den Wert "" haben und objectName spezifiziert den Namen, unter dem das Objekt intern referenziert wird. Dieser Wert darf auch leer "" sein.
7.4.1 Übersicht über die Methoden zur Attachmentbearbeitung
299
Für type == EmbeddedObject.EMBED_OBJECT: Fügt dem aufrufenden RichTextItem ein OLE-Objekt, das aus der Datei in fileName erzeugt wird, hinzu. Verwendung von class und objectName wie oben. Für type == EmbeddedObject.EMBED_OBJECTLINK: Dieser Typ ist hier nicht zulässig. •
public Vector getEmbeddedObjects() throws NotesException
Liefert einen Vector aus EmbeddedObject mit allen Dateianhängen und OLEObjekten des RichTextItems. •
public EmbeddedObject getEmbeddedObject(String objectName) throws NotesException
Liefert ein einzelnes EmbeddedObject innerhalb des aufrufenden RichTextItem mit Namen objectName. In RichTextNavigator: • public Base getFirstElement(int type) throws NotesException RichTextNavigator ermöglicht es, die verschiedenen Elemente und Objekte eines RichTextItems zu referenzieren. getFirstItem liefert das erste im RichTextItem gefundene Element vom Typ type. Für type == RichTextItem.RTELEM_TYPE_FILEATTACHMENT oder RTELEM_TYPE_OLE liefert die Methode den ersten Dateianhang oder das erste OLE-Objekt. • public boolean findFirstElement(int type) throws NotesException
Arbeitet wie getFirstElement, anstatt des eigentlichen Objekts wird jedoch nur über den boolschen Rückgabewert angezeigt, ob es ein Element des Typs type gibt. In Document: • public boolean hasEmbedded() throws NotesException Liefert true, falls es ein wie auch immer geartetes EmbeddedObject im Document bzw. in einem RichTextItem des Documents gibt. • public Vector getEmbeddedObjects() throws NotesException Liefert alle OLE-Objekte des Documents, egal ob im Document oder im RichTextItem gespeichert. • public EmbeddedObject getAttachment(String objectName) throws NotesException
Liefert den Dateianhang mit (internem) Namen objectName. Zusätzlich zu diesen Methoden stehen Ihnen in EmbeddedObject folgende Methoden zur Verfügung.
300
7 Document
In EmbeddedObject: • int activate(boolean showApplication) throws NotesException Aktiviert ein OLE-Objekt in seiner Anwendung. Im Backend muss showApplication auf false gesetzt werden. Liefert je nach Anwendung, die zu dem OLE-Objekt gehört, einen gültigen Handle auf das OLE-Objekt. Gültig für EmbeddedObject.EMBED_OBJECTLINK und EmbeddedObject.EMBED_OBJECT. Für EmbeddedObject. EMBED_ATTACHMENT wird eine NotesException geworfen. • void doVerb(String nameOfVerb) throws NotesException Führt für ein OLE-Objekt die Interaktion mit Namen nameOfVerb aus. • void extractFile(String fullFilePath) throws NotesException
Name der Application, zu der ein OLE-Objekt gehört. • •
int getFileSize() throws NotesException Dateigröße für EmbeddedObject.EMBED_ATTACHMENT String getName() throws NotesException
Interner Name für die Referenzierung des Objekts oder Attachments. •
int getObject() throws NotesException
Liefert je nach Anwendung, die zu dem OLE-Objekt gehört, einen gültigen Handle auf das OLE-Objekt, sofern dies bereits im Hauptspeicher geladen ist. •
RichTextItem getParent() throws NotesException
Liefert das übergeordnete RichTextItem-Objekt. Wurde ein EmbeddedObjekt direkt aus einem Attachment aus dem Document generiert, liefert getParent null, da es kein übergeordnetes RichTextItem gibt. •
String getSource() throws NotesException
Ursprünglicher Dateiname des Objekts. •
•
int getType() throws NotesException Typ des Objekts: EmbeddedObject.EMBED_OBJECTLINK (1452), EmbeddedObject.EMBED_OBJECT (1453) oder EmbeddedObject.EMBED_ATTACHMENT (1454) Vector getVerbs() throws NotesException
Vector der möglichen interaktiven Methoden (verbs) des OLE-Objekts •
void remove() throws NotesException
Entfernt ein EmbeddedObject oder Attachment aus dem RichTextItem (oder Document) •
java.io.Reader getReader() throws NotesException
Inhalt des Objects als Reader-Objekt. Erzeugt eine temporäre Datei, die (erst) beim recycle des Objekts aufgeräumt wird. •
7.4.1 Übersicht über die Methoden zur Attachmentbearbeitung
301
Inhalt des Objekts als org.xml.sax.InputSource. Um die zugehörigen Transformationsmethoden benutzen zu können, müssen alleinstehende Javaanwendungen XML4J.jar46 im Classpath einbinden und Applets diese als ClassPath-Parameter verwenden. Diese Methode erzeugt eine temporäre Datei, die beim Recycling wieder gelöscht wird. •
Parsed den Inhalt eines Attachments mit Type EmbeddedObject.EMBED_ATTACHMENT in ein XML DOM Document. Um die zugehörigen Transformationsmethoden benutzen zu können, müssen alleinstehende Javaanwendungen XML4J.jar48 im Classpath einbinden und Applets diese als ClassPath-Parameter verwenden. void transformXML (Object xslStyleSheet, XSLTResultTarget xsltresulttarget) throws IOException, SAXException, NotesException void transformXML(Object xslStyleSheet, XSLTResultTarget xsltresulttarget) throws NotesException49
Transformiert den Inhalt eines Attachments mit Type EmbeddedObject.EMBED_ATTACHMENT durch ein XSL Stylesheet. Um die zugehörigen Transformationsmethoden benutzen zu können, müssen alleinstehende Javaanwendungen XML4J.jar50 im Classpath einbinden und Applets diese als ClassPathParameter verwenden. xslStyleSheet darf vom Typ org.xml.sax.InputSource, lotus.domino.Item, lotus.domino.EmbeddedObject oder lotus.domino. MIMEEntity sein.
46
47 48 49 50
Seit Domino R7 ist XSLTResultTarget eine eingenständige Klasse und basiert im Wesentlichen auf javax.xml.transform, da seit Java 1.4.x die XML und XSL Klassen Bestandteil des standard JRE sind. Dementsprechend wurden aus der Domino 7 Core Version auch die Bibliotheken xml4j.jar und lotusxsl.jar entfernt. Dies hat leider Auswirkungen auf bestehende Installationen, da nun die neuen Parser verwendet werden müssen. Neue Signatur: Ab Domino Version 7 wirft diese Methode nur eine NotesException. Siehe 46. Neue Signatur: Ab Domino Version 7 wirft diese Methode nur eine NotesException. Siehe 46.
302
7 Document
7.5
Encryption und Signing
Daten in Domino-Datenbanken und -Dokumenten und deren Übertragung können auf verschiedene Weise verschlüsselt oder signiert werden. Verschlüsselung ist nicht zu verwechseln mit den verschiedenen bereits behandelten Methoden der Zugriffskontrolle über Zugriffskontrolllisten, serverbasierter programmatischer Kontrolle oder Autoren- und Leserfelder. Verschlüsselt werden können sowohl der Transport der Daten als auch die Daten selbst, und zwar entweder auf Basis der gesamten Datenbank oder auf Basis einzelner Items eines Dokuments. Die Verschlüsselung eines gesamten Dokuments als Gesamtheit ist nicht möglich, wird aber durch die Verschlüsselung jedes einzelnen Items erreicht. Verschlüsselung kann entweder auf Basis der Notes-Public-/Private Key-Infrastruktur erfolgen, also über eine Verschlüsselung über den öffentlichen Schlüssel eines Benutzers auch über S/MIME jeweils beim Speichern von Daten oder beim Versenden als E-Mail oder auf Basis von geheimen symmetrischen Schlüsseln, die für die Verschlüsselung auf Item-Basis Verwendung finden. Signiert werden können im Prinzip alle Domino-Objekte, die auf der Domino„Note“ basieren, also im Wesentlichen Gestaltungselemente und Dokumente, aber auch z.B. die ACL und unter bestimmten Bedingungen auch einzelne Items, beim Speichern von Items innerhalb von Abschnitten in Notes-Masken und beim Versenden über Notes-Mail oder S/MIME. Domino kennt viele Arten der Verschlüsselung und auch die gesamte Sicherheitsinfrastruktur basiert auf in der Regel asymmetrischen, persönlichen oder serverbasierten Schlüsselpaaren, die nach dem Prinzip von PGP arbeiten. Hierzu gehört die gesamte Notes-Infrastruktur, basierend auf dem Notes-Adressbuch, in dem die öffentlichen Schlüssel der bekannten Benutzer verwaltet werden. Zusätzlich können entfernte Verzeichnisse, hauptsächlich auf der Basis von LDAP, mit in diese Infrastruktur eingebunden werden. Das gesamte Thema „Sicherheit in Domino“ sprengt den Rahmen dieses Buches bei weitem, so dass dieses Kapitel nur einen groben Überblick über die Thematik liefern kann, zumal in diesem Zusammenhang ein großer Teil des benötigten Wissens mit der Infrastruktur und Administration des DominoServers verbunden ist, unabhängig von der Javaprogrammierung. Dieses Kapitel wird ihnen die Werkzeuge an die Hand geben, die Sie befähigen, mit einfachen Mitteln Daten zu verschlüsseln, zu speichern und zu versenden und dabei nur so tief in die Administration einzusteigen, wie es für ein erfolgreiches und einfaches Umsetzen der Methoden notwendig ist. Einen kleinen Überblick über die wichtigsten in Domino zur Verfügung stehenden Verschlüsselungs- und Signaturtechniken bietet folgende Übersicht:
7.5 Encryption und Signing
303
•
Verschlüsselung von Items durch geheime Verschlüsselungsschlüssel Die für die Java-Programmierung wichtigste Art der Verschlüsselung ist die Verschlüsselung von Items in einem Dokument mit einem geheimen symmetrischen Schlüssel. Dieser Schlüssel muss einmalig manuell generiert werden und wird an die berechtigten Benutzer verteilt, die ihn in ihrer Notes-ID speichern. Für die serverseitige Verwendung wird der Schlüssel in der Server-ID gespeichert. Mit Verschlüsselungsschlüsseln werden Daten für die Speicherung in NotesDatenbanken verschlüsselt, nicht für den Versand per E-Mail. Die Behandlung dieser Mechanismen erfolgt ausschließlich auf Dokumentenebene.
•
Verschlüsselung und Signatur von Dokumenten mit Notes-Schlüsselpaaren beim Speichern oder Versenden als Notes-Mail Das gesamte Sicherheitssystem für Encryption und Signing beruht in Domino auf asymmetrischen RSA-Schlüsselpaaren, die für jeden Benutzer und Server in deren User-ID bzw. Server-ID ausgegeben werden und für die RC2- und RC4Verschlüssselungsalgorithmen zum Einsatz kommen. Ähnlich wie durch den zuvor beschriebenen geheimen Schlüssel können Items mit dem öffentlichen Schlüssel aus dem Schlüsselpaar des Benutzers, für den die Daten lesbar bleiben sollen, verschlüsselt werden, wobei dieser über das NotesAdressbuch bekannt ist, und durch diesen Benutzer mit seinem privaten Schlüssel wieder entschlüsselt werden. Innerhalb der Notes-Infrastruktur ist es darüber hinaus möglich, jede Art von Note mit der Notes-Benutzer-ID zu signieren, wobei hierfür nun der private Schlüssel aus diesem Schlüsselpaar verwendet wird, zusätzlich wird der Signatur der öffentliche Schlüssel des Signierers beigelegt, anhand dessen überprüft werden kann, ob die Integrität der Daten erhalten ist. Eine wichtige Verwendung dieser Signaturen haben Sie bereits in Kapitel 4.16 kennen gelernt. Dort werden die Notes-Signaturen verwendet, um das Designelement „Agent“ zu signieren. Hierdurch kann bei der Ausführung eines Agenten sicher ermittelt werden, welcher Benutzer einen Agenten zuletzt gespeichert hat und ob diese Speicherung nicht zwischenzeitlich kompromittiert wurde.
•
Verschlüsselung und Signatur von Dokumenten mit S/MIME-Schlüsselpaaren beim Speichern oder Versenden als E-Mail Parallel zu den Notes-Verschlüsselungs- und Signaturmechanismen unterstützt Domino S/MIME. Die Grundprinzipien sind dieselben, jedoch kommen hier X.509-Zertifikate zum Einsatz.
•
Verschlüsselung gesamter Datenbanken Notes-Datenbanken können als Gesamtheit entweder anhand der Server-ID des Servers, auf dem sie sich befinden, oder anhand der ID eines Benutzers verschlüsselt werden, indem die entsprechende Datenbankeigenschaft über das Menu „File“ -> „Database“ -> „Properties“ und dort über den Button „Encryption settings“ aktiviert wird.
304
7 Document Für diese Art der Verschlüsselung wird ein zufälliger geheimer (symmetrischer) Schlüssel neu erstellt, der mit dem öffentlichen Schlüssel der Benutzer- bzw. Server-ID verschlüsselt und in der Datenbank gespeichert wird. Nur ein Benutzer, der Zugriff auf den entsprechenden privaten Schlüssel hat (also das Passwort der Benutzer- oder Server-ID kennt), kann diesen DatenbankVerschlüsselungsschlüssel entschlüsseln und erhält somit Zugriff auf die Daten der Datenbank. Datenbankverschlüsselung ist hauptsächlich für die Verschlüsselung lokaler Repliken von Datenbanken gedacht, da hier die ACL-Sicherheit nicht greift. Derartig verschlüsselte Datenbanken bleiben sicher, auch wenn sie auf Dateisystem-Ebene kopiert werden. Sollen Datenbanken nachträglich verschlüsselt werden, muss zuvor eine Datenbankwartung mit „compact“ vorgenommen werden.
•
Verschlüsselung des generischen Notes-Protokolls (NRPC) bei der Datenübertragung Die Verschlüsselung ist nahtlos in den Domino-Server und Notesclient integriert und wird über geheime, symmetrische Schlüssel abgewickelt. Um die Encryption für einen Server zu aktivieren, muss im Domino Administrator unter Configuration das Serverdokument markiert und die Aktion im Menü „Configuration“ -> „Server“ -> „Setup ports“ gewählt werden. Auf einem Notes Client kann ebenfalls der ausgehende Traffic verschlüsselt werden, indem im Menü „File“ -> „Preferences“ -> „User Preferences“ im Abschnitt „Ports“ die Eigenschaft „Encrypt network data“ aktiviert wird. Ist die NRPC-Protokoll-Verschlüsselung aktiviert, wird automatisch jede Datenübertragung, die über dieses Protokoll abgewickelt wird, zwischen Server und Client verschlüsselt.
•
Verschlüsselung von InternetProtokollen bei der Datenübertragung Für sämtliche von Domino unterstützten InternetProtokolle kann (sofern für das Protokoll verfügbar) für die Kommunikation mit dem Domino-Server SSL aktiviert werden. Ausführliche Informationen hierzu finden Sie im Kapitel 5.3.4. Dort wurde die SSL-Verschlüsselung anhand des DIIOP-Protokolls näher erläutert.
Für die Verwendung im Kontext mit Java soll auf die ersten beiden Punkte näher eingegangen werden.
7.5.1
Items mit geheimen Verschlüsselungsschlüsseln verschlüsseln
Vorbereitend für die Verwendung von Verschlüsselungsschlüsseln auf Item-Ebene – zum Teil wird auch von Feldverschlüsselung gesprochen – wird zunächst der Schlüssel benötigt. Öffnen Sie im Notes Client das Menü „File“ -> „Security“ -> „User security“ und dort die Sektion „Notes data“ -> „Documents“ (Abb. 7-7). Ein neuer Schlüssel wird mit dem Button „New secret key“ erstellt (Abb. 7-8), wobei hierbei noch ein Kommentar und die Einstellung, ob der Schlüssel bei der Verwendung älte-
7.5.1 Items mit geheimen Verschlüsselungsschlüsseln verschlüsseln
305
rer Notes-Versionen (vor 5.04) die in Domino so genannte „internationale Verschlüsselung“ verwenden soll, vorgenommen wird, was eine gut gemeinte Umschreibung der Verwendung einer weniger sicheren Verschlüsselung mit kürzeren Schlüsseln ist, die zu Zeiten von Domino 5.04 und früher bei der Verwendung außerhalb der Abb. 7-7 Verschlüsselungsschlüssel USA verwendet werden mussten, da die US-amerikanischen Exportvorschriften dies vorschrieben. Seit Version 5.04 existiert zwar weiterhin diese Unterscheidung, um vor möglichen neuerlichen Änderungen in den Exportvorschriften gewappnet zu sein, aber technisch sind nach dieser Zeit erstellte Schlüssel unabhängig von dieser Einstellung identisch. Kompatibel sind ohnehin alle Versionen untereinander. Beim Erstellen wird der Schlüssel automatisch in der User-ID des aktuellen Benutzers gespeichert. Um den Schlüssel zu verteilen, kann er entweder über den Button „Mail Secret Key“ per Notes-Mail versendet oder über „Other Actions“ als Datei exportiert werden. Im letzten Fall sollte immer ein nicht zu kurzes Passwort vergeben werden und der Schlüssel kann für die Benutzung durch bestimmte Empfänger geschützt werden, wobei zusätzlich noch festgelegt werden kann, ob solche Benutzer die Erlaubnis erhalten, den Schlüssel ihrerseits weiterzuversenden, so dass sehr genau kontrolliert werden kann, wer Zugriff auf den Schlüssel erhält. Es muss wohl nicht darauf hingewiesen werden, dass sämtliche Daten, die mit diesen Schlüsseln verschlüsselt wurden, verloren sind, wenn der Schlüssel verloren wird. Administrativ sind dies schon alle Schritte, die durchgeführt werden müssen, um mit Verschlüsselungsschlüsseln zu arbeiten.
Abb. 7-8 Neuen Verschlüsselungsschlüssel erstellen
306
7 Document
7.5.2
Verwendung der Verschlüsselungsschlüssel
Die eigentliche Verschlüsselung wird in drei Schritten erledigt. Erstens müssen Sie dem zu verschlüsselnden Item über void setEncrypted (boolean encrypt) die Eigenschaft boolean isEncrypted() mitgeben, wobei natürlich der Parameter encrypt auf true gesetzt werden muss. Diese Eigenschaft können Sie für jedes Item eines Dokuments einstellen, allerdings muss dies für jedes Item einzeln erledigt werden. Es genügt, diese Einstellung einmalig vorzunehmen, da diese Eigenschaft bis auf Widerruf im Item bleibt. Im zweiten Schritt müssen Sie – ebenfalls einmalig – dem Dokument den Namen des zu verwendenden Verschlüsselungsschlüssels mitgeben. Da es grundsätzlich erlaubt ist, mehrere Verschlüsselungsschlüssel auf ein Dokument anzuwenden, wird für diesen Zweck ein Vector, bestehend aus den zu verwendenden Schlüsselnamen, mittels void setEncryptionKeys (Vector keys) übergeben, der über Vector getEncryptionKeys() wieder gelesen werden kann. Die Verwendung mehrerer Verschlüsselungsschlüssel bedeutet, dass das verschlüsselte Dokument mit einem einzigen der verwendeten Schlüssel, der für die Verschlüsselung verwendet worden war, wieder entschlüselt werden kann. Schließlich muss durch encrypt() in Document die eigentliche Verschlüsselung durchgeführt und durch save() gespeichert werden. Im package djbuch.kapitel_07 finden Sie die Beispielklasse EncryptItems, mit den beiden Methoden beispiel1 (Listing 7-2) und beispiel2 (Listing 7-3), die eine erfolgreiche und fehlerhafte Verwendung der Encryption zeigen. Das erste Beispiel erzeugt zunächst ein Dokument und belegt die Items Form, F_Titel und F_secret mit Werten . Das Item F_secret soll verschlüsselt werden, private static Document beispiel1 (Database db) throws NotesException { System.out.println (LINE + "\nBeispiel 1" + LINE); Document docOne=db.createDocument(); docOne.replaceItemValue("Form", "FO_Dokument_k6"); docOne.replaceItemValue("F_Titel","Automatisch (1) erstellt am " + Calendar.getInstance().getTime()); Item item = docOne.replaceItemValue("F_secret","my Secret Value"); item.setEncrypted(true); Vector keys = new Vector (); keys.add("encryptionKey"); docOne.setEncryptionKeys(keys); docOne.encrypt(); boolean ok = docOne.save (true,false); String unid = docOne.getUniversalID(); if (docOne != null) {docOne.recycle();docOne=null;} Document result = db.getDocumentByUNID(unid); System.out.println ("Dokument ist verschlüsselt: " + result.isEncrypted()); System.out.println ("Geheimer Wert: " + result.getItemValueString("F_secret")); return result; }
Listing 7-2 Beispiel für Feldverschlüsselung
7.5.2 Verwendung der Verschlüsselungsschlüssel
307
also muss die Verschlüsselung für dieses Item mit setEncrypted (true) aktiviert werden . Der Vector keys enthält für dieses Beispiel lediglich einen einzelnen Namen. Dem Dokument wird der zu verwendende Schlüssel über setEncryptionKeys mitgeteilt. Alle derart für das Dokument definierten Schlüssel müssen sämtlich für die Verschlüsselung in der aktuellen User-ID vorliegen. Für die Entschlüsselung genügt es, mindestens einen dieser Keys zu besitzen. Nachdem die Verschlüsselung angestoßen wurde, muss das Dokument noch gespeichert werden . Um das Verhalten beim Entschlüsseln zu demonstrieren, wird das zuvor erstellte Dokument anhand der Universal ID komplett neu in ein neues Document-Objekt geladen . Beachten Sie, dass für diese Demonstration die Universal ID zwischengespeichert wird, aber das ursprüngliche Document-Objekt recycelt wird, um es endgültig aufzuräumen. Folgerichtig liefert die Methode isEncrypted für dieses neu geladene Document true. Allerdings ist darauf zu achten, dass isEncrypted keine Auskunft darüber liefert, ob die Daten zur Zeit der Abfrage tatsächlich verschlüsselt sind, sondern diese Methode gibt lediglich an, ob eine Verschlüsselung vorgesehen ist, also zuvor über setEncrypted (true) aktiviert wurde. Das Entschlüsseln ist transparent, d.h. es muss nicht ausdrücklich durchgeführt werden, sondern es genügt, wie gewohnt lesend oder schreibend auf das Dokument zuzugreifen . Am Rande sei noch darauf hingewiesen, dass diese Methode das von ihr erzeugte Dokument als Rückgabewert zurückgibt, es also nicht in der Methode selbst recycelt werden kann (und darf), sondern außerhalb der Methode der aufrufende Code sich darum kümmern muss, dass das über diese Methode erzeugte Dokument recycelt wird .
Bedenken Sie Sie können nur Verschlüsselungsschlüssel verwenden, die auch in Ihrer User- (oder Server) ID gespeichert sind. Die Übereinstimmung des Namens genügt nicht, sondern es können nur Items mit ein und demselben Schlüssel entschlüsselt werden, mit dem sie verschlüsselt wurden. Die Verwendung mehrerer Schlüssel bei der Verschlüsselung bedeutet, dass ein Item mit lediglich einem dieser Schlüssel entschlüsselt werden kann. Für die Verschlüsselung werden alle Schlüssel benötigt. Verschlüsselte Felder werden nicht in Ansichten angezeigt. Ansichten können nicht über Spalten mit verschlüsselten Feldern durchsucht werden.
308
7 Document
7.5.3
Fehlerquellen bei der Verwendung von Verschlüsselungsschlüsseln
In der Transparenz der Entschlüsselung liegt gleichzeitig eine Fehlerquelle. Folgt auf einen schreibenden Zugriff ein save, so wird das implizit entschlüsselte Dokument nicht automatisch neu verschlüsselt. Um hier keine Sicherheitslücke zu riskieren, gilt also bei der Verwendung der Feldverschlüsselung, dass immer vor jedem save auch ein encrypt erfolgen muss. Das zweite Beispiel (Listing 7-3) zeigt diese Problematik und noch einige andere Nebenwirkungen recht plastisch. Zunächst wird wie zuvor ein neues Dokument angelegt, verschlüsselt und gespeichert (Listing 7-3). In diesem Zustand zeigt sich über die Ausgabe des Items F_secret die erste Nebenwirkung. Wider Erwarten ist hier die Entschlüsselung weder automatisch noch transparent – die Ausgabe zeigt hier den Wert null, anstatt das Item implizit zu entschlüsseln. Dieses Verhalten gilt für den Zustand direkt nach dem Aufruf von encrypt. Wird ein (beliebiges anderes) Item schreibend verändert , wird die Encryption komplett aufgehoben, der zuvor als null angezeigte verschlüsselte Wert wird wieder korrekt ausgegeben . Auch wenn die Eigenschaft isEncrypted im Document
7.5.3 Fehlerquellen bei der Verwendung von Verschlüsselungsschlüsseln
309
Abb. 7-9 Verschlüsselte und unverschlüsselte Items
weiterhin über den Rückgabewert true suggeriert, es handele sich um ein verschlüsseltes Dokument , ist diese Information hier nicht mehr korrekt, das Dokument war unverschlüsselt gespeichert worden. Leicht können Sie dies überprüfen, wenn Sie den Notes Client öffnen und dort die Ansicht „kapitel 7\Dokumente“ öffnen und sich die Eigenschaften der beiden neuen Dokumente ansehen, die durch die Ausführung des Java-Programms Encrypt-Items aus den Beispielen erzeugt wurden (Abb. 7-9). Im rechten Eigenschaftenfenster der Abbildung erkennen Sie, dass der Wert des Items F_secret im Klartext lesbar ist und somit unverschlüsselt geblieben (genauer: geworden) ist. Hieraus kann es nur eine Konsequenz geben: Die beiden Methoden encrypt und save sind ein unzertrennliches Paar. Außerdem ergibt sich eine klare Reihenfolge, die bei der Verarbeitung einzuhalten ist: Führen Sie alle notwendigen Veränderun-
Der Verarbeitungszyklus für die Encryption Schritt 1 Vorbereitung der Encryption durch Setzen von setEncrypted in Item und setEncryptionKeys in Document Schritt 2 Durchführen aller benötigten Veränderungen auf dem Document Schritt 3 Aufruf von encrypt und save. Diese beiden Methoden sind ein unzertrennliches Paar bei der Verwendung der Feldverschlüsselung. Schritt 4 Keine weitere Verwendung des Documents, sonst muss der Zyklus erneut durchlaufen werden (im Einzelfall auch ohne Schritt 1).
310
7 Document
gen auf Ihrem Dokument durch, rufen Sie encrypt und save auf. Danach darf das Document nicht mehr verwendet werden, oder es muss der komplette Zyklus erneut durchlaufen werden. Wenn Sie nun im Notes Client das Dokument öffnen, werden in jedem Fall alle Werte korrekt angezeigt, sofern in Ihrer User-ID die korrekten Schlüssel gespeichert sind, denn beim Öffnen von Dokumenten im Notes Client wird Notes ebenfalls implizit eine Entschlüsselung durchführen. Beim Speichern des Dokuments würde übrigens dieser Encryption-Fehler korrigiert, aber nur, da in der Beispieldatenbank das korrespondierende Feld F_secret in der Maske mit der Eigenschaft „Enable Encryption for this Field“ aktiviert und für die Maske der „Default Encryption key“ auf den Namen des verwendeten Schlüssels („encryptionKey“) gesetzt wurde, so dass der Notes Client die Verschlüsselung implizit beim Speichern durchführt. Umgekehrt sei hier nochmals der Hinweis erlaubt, dass bei der Java-Programmierung immer bedacht werden muss, dass die verwendeten oder erzeugten Daten unerwartete Veränderung über die Verwendung von Masken im Notes Client erfahren können, auch wenn das Unerwartete in diesem Fall einen Fehler wieder wettmacht (Nachholen der Verschlüsselung). Im NotesClient können Sie in der Ansicht „kapitel 7\Dokumente“ eine weitere Auswirkung der Feldverschlüsselung erkennen. Derart verschlüsselte Items können nicht in Ansichtenspalten angezeigt werden und ebensowenig können verschlüsselte Items über Ansichtenspalten gefunden werden.
7.5.4
Verwendung von Verschlüsselungsschlüsseln über DIIOP
Verschlüsselungsschlüssel sind an eine User- oder Server-ID gebunden. Wie können also solche Keys über DIIOP, also über Internet-Sessions verwendet werden? Domino bietet hier den Ausweg über die Server-ID, in die lediglich der Key importiert51 wird. Liegt der Schlüssel in der Server-ID vor, so können alle Benutzer, die Zugriffsrechte auf Server, Datenbank und Dokument haben, mit diesem Key das Dokument (Item) ver- und entschlüsseln. Eingeschränkt wird dieses Recht aber über die programmatischen Agentenberechtigungen (s. Kap. 4.16). Nur Benutzer, die über volle Agentenberechtigungen („Run unrestricted methods and operations“) verfügen52, können dieses Verfahren anwenden, andernfalls wird eine NotesException „You must have permission to encrypt documents for server based agents“ geworfen. Liegt der benötigte Key nicht in der Server-ID vor, so wird dies mit der NotesException „You don't have any of the specified encryption keys“ quittiert.
51
52
Ein Verschlüsselungsschlüssel wird in die Server-ID importiert, indem diese ID im Notes Client über das Menü „File“ -> „Security“ -> „Switch ID“ geladen und der Schlüssel importiert wird. Beachten Sie, dass sich Änderungen am Serverdokument nicht immer sofort durchsetzen.
7.5.5 Verschlüsselung und Signatur von Dokumenten 7.5.5
311
Verschlüsselung und Signatur von Dokumenten
Neben der Verwendung geheimer Verschlüsselungsschlüssel besteht unter Domino die Möglichkeit, die vorhandene Infrastruktur zu nutzen und die Schlüsselpaare der User-IDs für die Verschlüsselung zu verwenden. Gleichzeitig eröffnet dies die Möglichkeit, Daten zu signieren und bei der Verwendung von Notes-Mail auch Mail-Inhalte verschlüsselt und signiert zu versenden. Dies kann direkt aus den Domino-JavaObjekten heraus erfolgen, ohne zusätzlichen Setup53.
7.5.6
Verwendung öffentlicher Verschlüsselungsschlüssel
Bei den bisher gezeigten Beispielen spricht man von geheimen Verschlüsselungsschlüsseln. Bei öffentlichen Verschlüsselungsschlüsseln kommt der öffentliche Schlüssel aus dem asymmetrischen Schlüsselpaar aus einer User-ID zum Einsatz, d.h. Items können ausdrücklich für bestimmte Personen verschlüsselt werden und können dann nur von dieser Person gelesen werden54. Die Verschlüsselung von Items über öffentliche Schlüssel erfolgt sehr ähnlich wie bei der Verwendung geheimer Schlüssel, lediglich die Methode setEncryptionKeys steht hier nicht zur Verfügung. Stattdessen wird einfach mit replaceItemValue das Feld „PublicEncryptionKeys“ mit dem kanonischen Namen des gewünschten Benutzers in der einfachen Form (ohne CN= etc.) gefüllt. Auch hier kann ein Vector eingesetzt werden, um die Daten für mehrere Zielpersonen lesbar zu machen. Damit Domino die Verschlüsselung durchführen kann, wird der öffentliche Schlüssel dieses Benutzers benötigt, den Domino entweder aus dem Adressbuch des Home-Servers des aktuellen Benutzers, bei einer lokalen Notes-Session, oder aus dem Adressbuch des Servers, mit dem die DIIOP-Session verbunden ist, ausliest. Insbesondere im ersten Fall muss eine Verbindung zu diesem Server möglich sein, sonst wird die Verschlüsselung scheitern und mit der NotesException „There is no certificate in the Address Book“ quittiert.
53
54
Zusätzlicher Setup ist erforderlich, falls keine Standardinstallation des Domino-Servers vorliegt, der per Default über die benötigten Einstellungen und Dienste für das NotesMail-Routing verfügt. Sollen Mails an entfernte Domino-Server übermittelt werden, müssen entsprechende Notes-Netze und Verbindungsdokumente eingerichtet werden. In der asymmetrischen Verschlüsselung wird mit einem öffentlichen Schlüssel verschlüsselt. Derart verschlüsselte Daten können nur mit dem zu diesem Schlüsselpaar gehörigen privaten Schlüssel entschlüsselt werden. Der für die Verschlüsselung verwendete Schlüssel ist für die Entschlüsselung nicht geeignet. Hierin liegt das Prinzip der asymmetrischen Verschlüsselung, da zwar ein Schlüssel öffentlich verfügbar, aber für die Entschlüsselung ungeeignet ist. Der private Schlüssel liegt in der User-ID des Benutzers, ist durch ein Passwort geschützt und sollte von dem Benutzer vertraulich behandelt werden.
312
7 Document
7.5.7
Signieren von Daten
Während die Verschlüsselung von Daten den Zugriff auf die Daten an sich durch Unkenntlichmachung der Daten erreichen will, ist das Ziel der Signatur, feststellen zu können, ob Daten zum einen von einer bestimmten Person stammen und ob diese Daten seit dem Speichern oder Senden durch diese Person nicht möglicherweise nachträglich (durch Dritte) verändert wurden. Der Vorgang des Signierens wird (nicht nur) in Notes wie folgt durchgeführt: Von den Daten wird ein Hashwert55 erzeugt. Der Hashwert wird mit dem privaten Schlüssel des Absenders (oder des Originators der Daten) verschlüsselt. Im Gegensatz zur oben beschriebenen Verschlüsselung wird hier der private Schlüssel des Absenders verwendet. • Die Daten werden zusammen mit dem Hashwert, dem öffentlichen Schlüssel des Originators und dem so genannten Zertifizierer des Originators gespeichert oder versendet. • Hashwert, Daten und öffentlicher Schlüssel bilden eine konsistente Einheit. Nur wenn die Daten unversehrt sind, kann ihr Hashwert mit dem öffentlichen Schlüssel des Originators entschlüsselt werden. Hieran kann erkannt werden, dass der Originator den Hash verschlüsselt hat, da nur er Zugriff auf seinen privaten Schlüssel hat, mit dem seine Daten verschlüsselt wurden. Wer der Originator ist, kann anhand seines Zertifizierers aus dem öffentlichen Schlüssel erkannt werden, der auf Basis der Notes-Zertifizierungsstelle erzeugt wurde. Dass die Daten unversehrt sind, kann daran erkannt werden, dass von den erhaltenen Daten erneut durch den Empfänger ein Hashwert gebildet wird. Da der mathematische Algorithmus eindeutig ist, wird aus denselben Daten derselbe Hashwert errechnet, der mit dem Hashwert aus der Signatur verglichen werden kann. • Anhand des Zertifizierers kann erkannt werden, ob dem Signierer vertraut wird. Dies ist der Fall, wenn der Signierer entweder zum selben Zertifizierer gehört, oder wenn über so genannte Gegenzertifikate (Cross Certificate) vom prüfenden Benutzer festgelegt wurde, dass er dem überprüften Zertifikat vertraut. Das Ausstellen von Gegenzertifikaten ist immer eine ausdrücklich manuelle Tätigkeit, bei der der Leser der Daten festlegt, welchen Organisationen (Herausgebern von Zertifikaten) er vertraut. • Nur wenn die Entschlüsselung des Hashwertes, dessen Vergleich mit dem aus den erhaltenen Daten errechneten Hashwert, positiv ausfällt und ein vertrauenswürdiges Zertifikat gefunden wurde, wird die Signatur als echt anerkannt, d.h. es wird anerkannt, dass die Daten vom angegebenen Originator stammen und unversehrt sind. Das Signieren ist eng in die Infrastruktur von Domino integriert und kann von außen nicht beeinflusst werden. Dokumente werden signiert, indem einzelne Items • •
55
Ein Hash ist ein nicht umkehrbarer, aber eindeutiger mathematischer Algorithmus, d.h. identische Daten liefern immer den identischen Hashwert. Umgekehrt lassen sich aber die Daten aus dem Hashwert nicht wieder herstellen. Selbst die kleinste Veränderung in den Daten führt zu einem völlig anderen Hashwert. Daher wird der Hashwert auch fingerprint (Fingerabdruck) genannt, da er einen Datensatz eindeutig identifiziert.
7.5.7 Signieren von Daten
313
durch void setSigned(true) in Item für die Signatur aktiviert und das Dokument über void sign() in Document signiert werden. Auch hier sollten Sie ähnlich wie für die Encryption das Dokument direkt nach dem Signieren speichern und nicht weiter verändern (sonst würde ja die Signatur zerbrechen). Alle für die Signatur notwendigen Schritte werden von Domino dann automatisch ausgeführt und zwar bezogen auf die ID des aktuellen Benutzers, dem die Notes-Session gehört. Hieraus folgt übrigens, dass niemand im Namen eines Dritten signieren kann. Die Methode boolean isSigned() in Item zeigt an, ob ein Item in die Signatur einbezogen ist, aber nicht etwa, ob die Signatur für dieses Item gültig ist. Gleiches gilt für die gleichnamige Methode void isSigned () in Document. Diese Methode liefert keine (!) Auskunft über den Zustand der Signatur, sondern zeigt lediglich an, ob es ein Item mit Namen „$Signature“ im Dokument gibt. Normalerweise sind sämtliche Mechanismen für die Erstellung und Überprüfung von Signaturen eng in den Notes Client verwoben und recht schwer von außen verwendbar. Beim Versuch, eine Verifizierung der Signatur vorzunehmen, ist ein erster Schritt, zu überprüfen, ob das Feld $Signature nicht nur vorhanden ist, sondern ob der Typ des Items – getFirstItem ("$Signature").getType() – den int-Wert der Konstante Item.SIGNATURE (8) liefert. Aber auch dieser Wert liegt nicht immer zuverlässig vor, da signierte Mails die Signatur in einem verschlüsselten Feld tragen. Notes unterstützt nur im geöffneten (LotusScript-) UI-Document ein Item mit Namen $SignatureStatus, das den Wert "1" hat, sofern eine Signatur vorhanden und gültig ist. Dieses Item nimmt den Wert "0" an, falls keine Signatur vorliegt und den Wert "2", falls eine zerbrochene Signatur vorliegt. Da eine Überprüfung dieses Items wegen der fehlenden UI Klassen in Java nicht möglich ist, müssen Sie sich anderer Mittel bedienen. Die beiden Methoden String getSigner() und String getVerifier() in Document liefern nur dann sinnvolle Werte (nicht ""), falls die Signatur gültig ist, denn beide Werte werden aus dieser extrahiert. Diese beiden Methoden liefern Ihnen immer sicher Auskunft, ob eine Signatur vorliegt und diese gültig ist. Bedenken Sie auch, dass die Überprüfung von Signaturen immer benutzerabhängig ist, da eine gültige Signatur immer nur dann vorliegt, wenn zwei Bedingungen erfüllt sind: Einerseits kann der Hash entschlüsselt werden und passt zu den Daten und andrerseits muss ein Benutzer die Zertifizierungsstelle des Absenders anerkennen, d.h. der aktuelle Benutzer der Notes-Session bürgt z.B. über Gegenzertifikate, dass er den Zertifizierer (den Aussteller) der Signatur anerkennt. Bedenken Sie, dass bei einer DIIOP-Verbindung, ebenfalls wegen der Problematik der fehlenden User-ID, wie zuvor bei der Verschlüsselung die Server-ID diesmal für die Signatur herangezogen wird. Wenn also z.B. eine DIIOP-Verbindung zu einem Server aufgebaut wird, der die ID CN=meinServer/O=meineOrganisation besitzt – wobei die DIIOP-Verbindung selbst durch einen beliebigen Benutzer (mit den nötigen Rechten) geöffnet wird – so liefert die Methode getSigner() für alle Dokumente, die über die beschriebenen Mechanismen mit setSigned() und sign() signiert wurden, den Wert CN=meinServer/O=meineOrganisation. Die Methode getVerifier() liefert analog den Wert O=meine Organisation. Auch für den Vor-
314
7 Document
gang des Signierens werden daher volle Agentenrechte im Serverdokument benötigt, sonst wird eine NotesException „You must have permission to sign documents for server based agents“ geworfen. Neben der hier beschriebenen Signatur über den Hashwert aller für die Signierung markierten Items kennt Domino auch noch die Abschnitt-weite Signatur. Diese entsteht, wenn Sie für Felder innerhalb eines Abschnittes einer Notes-Maske im Domino Designer die Eigenschaft „Sign if mailed or saved in section“ aktivieren. Dann erstellt Domino den Hash nicht über alle zu signierenden Items, sondern jeweils eine Signatur für die zu signierenden Items eines Abschnitts.
7.5.8
Versenden von signierten und verschlüsselten Daten
Die bisher beschriebenen Verfahren ermöglichen es, Daten zu verschlüsseln und zu signieren, wenn sie in einer Datenbank gespeichert werden. Sehr wichtig sind diese Verfahren jedoch ebenfalls, wenn Daten per Mail versendet werden sollen. Für den Versand per Notes-Mail (über die Notes-eigenen Routing-Mechanismen) stehen hierfür einfach anzuwendende Methoden zur Verfügung. Listing 7-4 zeigt ein Beispiel aus der Klasse EncryptItems im package djbuch.kapitel_07 für den Versand einer verschlüsselten und signierten Notes-Mail. Notes-Dokumente können direkt über die Methode void send (), in Document per Notes-Mail versendet werden. Vorbereitend müssen aber einige PflichtItems im Dokument vorliegen. Wichtig ist, dass die Maske auf „Memo“ lautet und die eigentliche Nachricht im Item „Body“ liegt, wobei dieses Item in der Regel ein RichTextItem ist. Im Item „Subject“ kann die Betreffzeile der Mail be-
Listing 7-4 Verschlüsselte und signierte Mail versenden
7.6 Document Properties und weitere Methoden
315
stimmt werden und schließlich bestimmt SendTo den Adressaten. Bis auf SendTo sind diese Items unerlässlich. Wird SendTo weggelassen, können über zusätzliche Signaturen von send ein oder mehrere Empfänger übermittelt werden: void send(String recipient) throws NotesException void send(Vector recipients) throws NotesException void send(boolean attachForm) throws NotesException void send(boolean attachForm, String recipient) throws NotesException void send(boolean attachForm, Vector recipients) throws NotesException
Der Parameter attachForm ist eine Domino-Besonderheit und sorgt dafür, dass die Maske mit der ein Dokument erstellt wurde zusammen (im Domino-Routing) mit dieser Mail versendet wird. Eine über send verschickt mail trägt ein spezielles Feld, so dass über isSentByAgent angezeigt werden kann, ob die Mail automatisiert erstellt wurde. isSaveMessageOnSend und setSaveMessageOnSend ermöglichen es, das Senden mit einem automatisierten Speichern zu verbinden und Document createReplyMessage(boolean replyToAll) throws NotesException erzeugt ein Dokument, das eine Antwortmail darstellt, mit (optional allen) Absendern als Empfänger. Zusätzlich können Sie noch im Feld „From“ den anzuzeigenden Absender und im Feld „Principal“ den tatsächlichen Absender festlegen, wobei Domino intern immer den Benutzer der Session als Absender führt, um Absenderfälschungen zu unterbinden. Die zu verschlüsselnden oder zu signierenden Items werden wie oben durch setEncrypted und setSigned markiert. Anstatt jedoch das Verschlüsseln oder Signieren durch encrypt oder sign ausdrücklich anzustoßen werden zwei Properties im Document gesetzt. setEncryptOnSend (true) und setSignOnSend(true) erreichen, dass Verschlüsselung und Signatur direkt vor dem Versenden angestoßen werden, so dass das Document nur noch verschickt werden muss . Ob diese Eigenschaften im Document gesetzt sind, kann durch boolean isEncryptOnSend() und boolean isSignOnSend() in Document überprüft werden. Auch hier wird automatisch die Benuzter-ID des die Session führenden Benutzers verwendet, um das Dokument zu signieren. Auch für die Verschlüsselung kann kein Schlüssel angegeben werden, sondern es wird automatisch der öffentliche Schlüssel des Empfängers verwendet, was natürlich auch hier voraussetzt, dass dieser über das Domino Directory (auch Notes-Adressbuch genannt) bekannt ist und dort sein öffentlicher Schlüssel zur Verfügung steht.
7.6
Document Properties und weitere Methoden
Neben den beschriebenen Methoden zur Arbeit mit Dokumenten finden sich noch einige Methoden und Eigenschaften im Document, die nicht unerwähnt bleiben sollen. Properties in Document
Liefert eine Liste der Benutzer, die das Document zuletzt geändert haben (nicht zu verwechseln mit Autorenfeldern). Solange ein Document nicht zum ersten Mal durch save gespeichert wurde, liefert diese Methode null. •
DateTime getCreated() throws NotesException
Liefert das Erstelldatum des Document. Steht bereits vor dem ersten save zur Verfügung. •
DateTime getLastAccessed() throws NotesException
Liefert den Zeitpunkt des letzten Zugriffs auf das Document. Diese Methode liefert nur den exakten Tag. Wird ein Document innerhalb eines Tages mehrmals gelesen, so wird jeweils nur der Zeitpunkt des ersten Lesezugriffs dieses Tages zurückgegeben. Der Wert wird nur dann sofort aktualisiert, wenn ein Dokument verändert und gespeichert wird. •
DateTime getLastModified() throws NotesException
Liefert den letzten Änderungszeitpunkt des Document. •
int getSize() throws NotesException
Liefert die Gesamtgröße des Dokuments einschließlich des durch die Note benötigten Überhangs. •
Liefert das Datenbankobjekt, über das das Document geladen wurde. XML • String generateXML() throws NotesException Anhand der Domino DTD wird XML-Code für das aktuelle Dokument (Abb. 710) erzeugt und als String zurückgegeben. • void generateXML(Writer writer) throws NotesException, IOException;
Statt den XML Code als String zurückzugeben, kann das Ergebnis auf einen Writer umgeleitet werden. •
Erzeugt das XML des Document und transformiert es mit einem XSL Stylesheet. Das Ergebnis wird als XSLTResultTarget zurückgegeben. Der Parameter xslStyleSheet darf vom Typ org.xml.sax.InputSource, lotus.domino. Item, lotus.domino.EmbeddedObject oder lotus.domino.MIMEEntity sein. Ein typisches Code-Beispiel sieht wie folgt aus: String xsl = "//your XSL StyleSheet"; Item xslItem = styleDoc.replaceItemValue("F_xsl",xsl); XSLTResultTarget xsltResult = new XSLTResultTarget(); StringWriter wr = new StringWriter (); 56
Neue Signatur: Ab Domino Version 7 wird keine SaxException mehr geworfen.
RichText-Methoden in Document Im Zusammenhang mit der RichText-Verarbeitung befinden sich einige Methoden in Document, die hier als Vorgriff auf Kapitel 11.1 erläutert werden. •
Domino bietet die Möglichkeit, ein komplettes (!) Document als RichText in ein RichTextItem zu rendern. Damit dies sinnvoll möglich ist, wird natürlich eine Maske benötigt, damit Domino weiß, auf welcher Basis die Darstellung des Documents erzeugt werden soll. Verwendet wird hierfür die im Document angegebene Maske im Item Form. Verwendet wird die Methode, indem im zu rendernden Quelldokument die Methode createRichTextItem aufgerufen wird. Ziel ist ein RichTextItem (eines anderen Documents), das als Parameter übergeben wird. View- und Folder-Methoden in Document Im Zusammenhang mit Ansichten (View) befinden sich einige Methoden in Document, die hier als Vorgriff auf Kapitel 10.4 erläutert werden. •
Vector getColumnValues() throws NotesException
Falls ein Document über eine Ansicht selektiert wurde, so können die Spaltenwerte für diese Ansicht über getColumnValues() als Vector geladen werden. Einige Werte des Vector können daher auch leer sein. Jedes Element des Vector repräsentiert die Berechnung einer Spalte für die Ansicht, die wiederum (in der Regel) auf den Items des Dokuments basiert. Falls eine Spalte einer Ansicht ohne weitere Berechnung Items eines Dokuments anzeigt, so ist es effizienter, die Item Values über diese Methode zu beziehen, als über den im Kapitel über Items beschriebenen Weg. Spaltenberechnungen, die sich auf UI-Funktionen beziehen, werden nicht zurückgegeben. •
int getFTSearchScore() throws NotesException
Dokumente können in Domino über Volltextsuchen über die Datenbank oder eine Ansicht selektiert werden. Falls dies der Fall ist, liefert diese Methode eine Trefferbewertung für dieses Dokument anhand von Häufigkeit der Suchwörter aber auch Vergleichsoperationen und Wertungen in der Volltextsuche. Gültig ist immer die zuletzt durchgeführte Suche. Falls eine Datenbank keinen Volltextindex hat, liefert diese Methode zufällige Ergebnisse. Falls das Document nicht über eine Volltextsuche selektiert wurde, liefert diese Methode 0. •
View getParentView() throws NotesException
Falls das Dokument über eine Ansicht selektiert wurde, liefert diese Methode eine Referenz auf das zugehörige Objekt. •
Domino kennt so genannte Ordner (s. Kap. 3.3), die zwar den Ansichten recht ähnlich sind, aber anstatt Dokumente über Ansichtenauswahlformeln zu selektieren, beliebige Referenzlisten auf Dokumente enthalten, so dass Dokumente frei in Ordner gelegt oder auch wieder entfernt werden können, und zwar unabhängig vom eigentlichen Dokument.
7.7 Document Locking
319
Normalerweise wird aber keine umgekehrte Referenzliste von Domino gepflegt, in der erkenntlich wäre, in welchen Ordnern sich ein Dokument befindet. Wird diese Funktion benötigt, muss in der Datenbank diese Eigenschaft mit void setFolderReferencesEnabled(true) aktiviert werden. Dies beeinflusst aber die Gesamtperformance der Datenbank. Zusätzlich werden noch die beiden Ansichten „($FolderInfo)“ und „($FolderRefInfo)“ in der Datenbank benötigt. Andernfalls wirft setFolderReferenceEnabled eine NotesException „Database does not support folder references“. •
Um für ein Dokument eine Referenz in einen Ordner zu legen, wird die Methode putInFolder mit dem Namen des Zielordners als Parameter aufgerufen. Per Default wird ein nicht existierender Ordner automatisch neu angelegt. Optional kann dies mit dem Wert false für den optionalen Parameter createIfNotExists unterbunden werden. Ordner können inneinander verschachtelt werden, indem die Ordnernamen durch einen Backslash „\“ getrennt werden. So erzeugt z.B. „Äusserer Ordner\Innerer Ordner“ zwei ineinander geschachtelte Ordner „Äusserer Ordner“, der den Ordner „Innerer Ordner“ enthält. •
Entfernt eine Dokumentenreferenz wieder aus einem Ordner.
7.7
Document Locking
Seit Version 6 verfügt Domino über die Möglichkeit des Document Locking. Dokumente können entweder durch den Benutzer manuell im Notes Client gesperrt werden oder über die API-Funktionen. Letztere stehen für Java allerdings erst seit Version 6.5 zur Verfügung. Die Idee des Lockings ist einfach. Anstatt grundsätzlich auf allen Repliken zuzulassen, dass mehrere Benutzer gleichzeitig ein Dokument ändern und speichern, und diese unterschiedlichen Änderungen in Speicher- und Konfliktdokumenten zu behandeln, wird an einem zentralen Dominoserver das Locking verwaltet und den einzelnen Benutzern und Prozessen die Möglichkeit gegeben, einzelne Dockumente exklusiv für die eigene Bearbeitung zu sperren. Dadurch wird der Zugriff durch Dritte verhindert, bis das Locking wieder aufgehoben ist. Hieraus folgen zwei Bedingungen, die für das Locking notwendig werden: Zum einen muss ein zentraler Locking-Server zur Verfügung stehen. Bei Domino ist dies der für eine Datenbank in deren ACL eingestellte Administrationsserver, der auch sonst für zentrale Verwaltungsaufgaben für die Datenbanken zuständig ist. Die zweite
320
7 Document
Voraussetzungen Ein zentraler Locking-Server ist verfügbar. Der Locking Server ist als Administrationsserver in der Datenbank eingetragen: db.getACL().getAdministrationServer() ist nicht der leere String "" In der Datenbank, für die Locking aktiviert werden soll, ist isLockingEnabled() == true
Folge ist, dass das Locking ausdrücklich von den jeweiligen Benutzern vor einem gewünschten exklusiven Zugriff aktiviert und danach natürlich wieder deaktiviert werden muss, damit das Dokument nicht dauerhaft gesperrt bleibt. Um zu überprüfen, ob ein Administrationsserver für eine Datenbank eingetragen ist, kann über ACL getACL () in Database die ACL bezogen werden und in ACL über String getAdministrationServer() der eingetragene Administrationsserver in der abgekürzten Form (z.B. „Java/DJBUCH“) geladen werden und über void setAdministrationServer (String name) wieder gesetzt werden. Beachten Sie, dass Veränderungen in der ACL Administratorenrechte voraussetzen57. Falls in der Datenbank kein Administrationsserver eingetragen ist, liefert getAdministrationServer den leeren String "". Damit für eine Datenbank das Locking verwendet werden kann, muss für diese Datenbank in den Eigenschaften das Document Locking aktiviert werden. Dies kann programmatisch in Database auch über void setDocumentLockingEnabled(boolean state) mit state==true erreicht und natürlich mit boolean isDocumentLockingEnabled() wieder abgefragt werden. Wenn nun ein Benutzer ein Dokument sperrt, so wird am zentralen Locking Server diese Sperrung hinterlegt und zwar unabhängig davon, auf welcher Replik der Benutzer gerade arbeitet. Einzige Voraussetzung ist, dass der Locking Server (direkt, in der Regel im LAN) erreichbar ist. Jeder andere Benutzer, Server oder Agentenprozess, der jetzt versucht, auf einer beliebigen (!) Replik das Dokument oder eine Replik davon zu öffnen, wird eine NotesException erhalten, da das Dokument für den Benutzer, der die Sperrung eingerichtet hat, den so genannten Lock Holder, exklusiv gesperrt ist. Um dies zu gewährleisten, wird Domino, sobald das Locking für eine Datenbank aktiviert ist, für jedes Öffnen eines Dokuments am Locking Server nach eventuell vorliegenden Sperrungen nachfragen. Das Locking sollte also immer nur in performanten lokalen Netzen für eine Datenbank aktiviert werden, in der der Locking Server immer erreichbar ist, sonst muss mit erheblichen Wartezeiten durch den erhöhten Traffic durch die Locking-Anfragen und mit eventuellen Timeouts, bei Nichtverfügbarkeit, gerechnet werden. 57
Eine häufige Fehlerquelle bei der Verwendung von DIIOP ist an dieser Stelle, dass die maximalen Internet-Zugriffsrechte per Default auf „Editor“ eingestellt sind, so dass Veränderungen an der ACL per DIIOP nicht vorgenommen werden können und in einer NotesException resultieren.
7.7 Document Locking
321
Document Locking Kann für mehrere Lock Holder gleichzeitig eingerichtet werden. Locking kann für den aktuellen Benutzer oder für Dritte eingerichtet werden. Kann nur von einem eingetragenen Lock Holder wieder aufgehoben werden. Sollte nur in schnellen lokalen Netzen eingesetzt werden. Erfordert mindestens Autoren-Rechte. Provisorisches Locking erfolgt, falls der zentrale Locking-Server nicht erreichbar ist und provisorisches Locking akzeptiert wurde. Locking kann nur für Dokumente eingesetzt werden, die bereits in der Datenbank gespeichert wurden. Benutzer mit Manager-Rechten können ein Lock aufheben, ohne selbst Lock Holder zu sein. Kann in Document, View, Form oder Agent verwendet werden.
Aktiviert wird das Locking für ein Dokument durch die Methode boolean lock (boolean acceptProvisional) in Document. Rückgabewert dieser Methode ist true, falls das Locking erfolgreich war. Der Parameter acceptProvisional zeigt an, ob beim Locking akzeptiert werden soll, dass das Locking nur provisorisch eingerichtet wird. Provisorisches Locking findet nur auf der aktuellen Replik statt, auf der der aktuelle Benutzer gerade arbeitet, und wenn der zentrale Locking-Server nicht erreichbar ist. Domino versucht dann, sobald der Locking-Server erreichbar ist, das Locking endgültig auf diesem Server zu platzieren. Die Durchführung des provisorischen Lockings erfolgt transparent und es kann lediglich gesteuert werden, ob es grundsätzlich akzeptiert wird. Anstelle von lock (true) kann auch boolean lockProvisional() verwendet werden. Das provisorische Locking ist nicht unproblematisch, wenn mehrere Benutzer gleichzeitig provisorisch locken oder ein Benutzer provisorisch und einer endgültig lockt. Es wird sich dann der Benutzer durchsetzen, der zuerst den endgültigen Lock auf dem Administrationsserver platzieren kann. Änderungen eines weiteren Benutzers, der versucht hat provisorisches Locking für ein Dokument einzurichten, werden verworfen (s. Abb. 7-11)! Hiermit wird der Tatsache Rechnung getragen, dass im Falle eines endgültigen Lockings der zweite Benutzer ohnehin keine Änderungen hätte durchführen können. Falls derartige Daten verworfen wurden, wird der Benutzer automatisch per Notes-Mail informiert, dass seine Änderungen verfallen sind. lock und lockProvisional zeigen folgendes Verhalten:
•
Per Default wird provisorisches Locking nicht akzeptiert (boolean lock()).
322
7 Document 1
Benutzer A sperrt ein Dokument auf dem Administrationsserver A
1
Benutzer A Replik A
2
B
AdministrationsServer
2
Benutzer B erhält keinen Zugriff auf das Dokument, da es gesperrt ist. Die Sperrung wird auf dem Administrationsserver erkannt.
Replik B
Benutzer B
3
C 3
Benutzer C Replik C
Benutzer C hat temporär keine Verbindung zum Administrationsserver. Sobald die Verbindung wieder hergestellt ist, gehen seine Änderungen verloren, da ein Locking erkannt wird und er keine Änderungen hätte vornehmen dürfen.
Abb. 7-11 Document Locking in verteilter Umgebung
•
Falls provisorisches Locking nicht akzeptiert wird (lock (false)) und der Locking-Server nicht erreichbar ist, wird eine NotesException „Unable to lock cannot connect to Master Lock Database“ geworfen. Falls provisorisches Locking nicht akzeptiert wird und der Locking-Server erreichbar ist und noch kein endgültiges Locking vorliegt, bzw. der aktuelle Benutzer bereits als Lock Holder eingetragen ist, wird true zurückgegeben und das Locking etabliert bzw. beibehalten. Falls das endgültige Locking nur für andere Benutzer eingetragen ist, wird die NotesException „Note is already locked by someone else“ geworfen.
7.7 Document Locking •
•
323
Falls provisorisches Locking akzeptiert wird (lock (true) oder lockProvisional()), wird ein provisorisches Locking eingetragen, falls der Locking-Server nicht erreichbar ist, aber noch kein (provisorisches) Locking vorliegt, bzw. der aktuelle Benutzer bereits (provisorischer) Lock Holder ist. Provisorisches Locking wird nicht eingetragen, falls ein Locking bereits für andere Benutzer vorliegt. Dann wird eine NotesException „Note is already locked by someone else“ geworfen. Falls der Locking-Server erreichbar ist und noch kein Locking vorliegt, bzw. der aktuelle Benutzer bereits Lock Holder ist, wird endgültiges Locking eingetragen und true zurückgegeben, andernfalls eine NotesException „Note is already locked by someone else“ geworfen. Provisorisches Locking kann nicht erzwungen werden, sondern wird implizit verwendet, falls der Locking-Server nicht verfügbar ist. In jedem anderen Fall wird endgültig gelockt.
Zusätzlich kann den Methoden lock und lockProvisional als erster Parameter ein String oder ein Vector aus Strings übergeben werden. Hierdurch kann das Locking für einen bestimmten Benutzer bzw. für mehrere bestimmte Benutzer eingetragen werden. Hieraus ergeben sich die Signaturen: boolean lock() throws NotesException boolean lock(boolean acceptProvisional) throws NotesException boolean lock(String lockHolder) throws NotesException boolean lock(String lockHolder, boolean acceptProvisional) throws NotesException boolean lock(Vector lockHolders) throws NotesException boolean lock(Vector lockHolders, boolean acceptProvisional) throws NotesException boolean lockProvisional() throws NotesException boolean lockProvisional(String lockHolder) throws NotesException boolean lockProvisional(Vector lockHolders) throws NotesException
Wird durch die Verwendung eines der Parameter lockHolder oder lockHolders ein Lock für einen Dritten eingerichtet, kann das Locking nicht mehr durch den aktuellen Benutzer aufgehoben werden, es sei denn er war bereits selbst Lock Holder. Einzige Ausnahme ist ein Benutzer, der als Manager in der ACL einer Datenbank eingetragen ist. Manager dürfen vorhandene Lockings aufheben, auch wenn sie nicht selbst Lock Holder sind. Dies ist der einzige Ausnahmefall. So kann zum Beispiel ein Dokument, das für einen anderen Benutzer gelockt ist, nicht durch den Administrator erneut gelockt werden. Aufgehoben wird das Locking durch die Methode void unlock(). Berechtigt sind hierfür nur die als Lock Holder (im gelockten Document oder Designelement) eingetragenen Benutzer. Falls ein nicht berechtigter Benutzer versucht, das Locking aufzuheben, wird eine NotesException „Notes error: The document is not locked by you“ geworfen.
Über die Methode getLockHolders() kann ein Vector aus String mit allen Lock Holdern abgerufen werden. Falls das Dokument nicht gelockt ist, wird ein leerer Vector zurückgegeben. Neben Dokumenten können auch Ansichten, Masken oder Agenten mit einem Locking gesperrt werden. Sämtliche Methoden entsprechen den zuvor beschriebenen.
7.8 Zusammenfassung
325
Design Locking muss zuvor in der Datenbank über void setDesignLockingEnabled(boolean allow) erlaubt werden. Ob das Design Locking aktiviert ist, kann über boolean isDesignLockingEnabled() geprüft werden. In dem Package djbuch.kapitel_07 findet sich die Klasse DocumentLocking mit zwei Beispielen für das Document Locking. Listing 7-5 zeigt das zweite Beispiel, in dem einige Ausnahmesituationen aufgezeigt werden. Bevor in einer Java-Anwendung Locking verwendet wird, sollte überprüft werden, ob es für die aktuelle Datenbank überhaupt zulässig ist . Um das Locking demonstrieren zu können, wird in diesem Beispiel ein Dokument neu angelegt. Damit es gelockt werden kann, muss es gespeichert werden , denn ein Dokument, das nur als Java-Objekt existiert, wie dies hier für das Dokument result vor dem save der Fall ist, wird vom Locking-Mechanismus nicht gefunden und kann nicht gelockt werden. Das Dokument wird für einen anderen Benutzer gelockt und danch wird versucht für den aktuellen Benutzer ein endgültiges Lock durchzuführen . Zur Verdeutlichung wurde hier der Parameter acceptProvisional ausdrücklich auf false gesetzt. Erwartungsgemäß wird hier eine NotesException geworfen, da das Dokument ja bereits einen anderen Lock Holder hat. Entsprechend wird auch der Versuch, ein provisorisches Locking durchzusetzen, scheitern . Der weitere Verlauf hängt davon ab, welche Rechte Ihr lokaler Benutzer auf der Datenbank hat. Sollte er keine Manager-Rechte für die Datenbank haben, wird auch beim unlock eine NotesException anzeigen, dass das Locking des vorhandenen Lock Holders nicht aufgehoben werden darf, was auch gut an den vor und nach dem unlock angezeigten Lock Holdern erkannt werden kann.
7.8
Zusammenfassung
Das Domino-Document und sein Item sind die zentrale Speicherstruktur, in der alle Domino-Daten abgelegt werden. Ein Document stellt die Klammer für sämtliche enthaltenen Items, RichTextItems, Anhänge oder OLE-Objekte dar, die im Document gespeichert werden können. Das Document ist das wichtigste Objekt innerhalb der Domino-Datenstruktur und mit Sicherheit auch das komplexeste. Daher wird sich das Kapitel 13 erneut intensiv mit dem Document auseinandersetzen. Es kann auf die verschiedensten Arten erzeugt werden, indem es direkt aus einer Datenbank geladen, über eine Suche oder Ansicht und die hieraus resultierenden Collections selektiert oder indem es über Volltextsuchen gefunden wird. In diesem Kapitel wurden wichtige Techniken anhand und für das Document erläutert. Hierzu gehören die Verschlüsselung und Signatur von Daten, aber auch das seit Domino R6 neue Document Locking.
8
8 Session
In diesem Kapitel: Session als zentrale Basis von Domino-Java-Anwendungen Bezug von DBDirectory als Abbild der Datenbankstruktur eines Domino-Servers oder eines Notes Clients Bezug des AgentContext Administrative Anwendungen im API – AdministrationProcess DateTime, Name oder Stil-Objekte beziehen Convenience-Methoden
328
8 Session
Die Verwendung und der Bezug einer Notes-Session wurde in den Kapiteln 4 und 5 ausführlich diskutiert. Einen schnellen Überblick über die NotesFactory zum Bezug einer Session bietet Kapitel 5.3.7. Die Session ist die Basis aller Domino-Java-Anwendungen. Neben dieser grundlegenden Aufgabe liefert sie aber noch einige nützliche Objekte, wie Streams, Datums- oder Farbobjekte und einige Properties, die für die aktuelle Verbindung hilfreich und wichtig sind. Darüber hinaus werden über die Session verschiedener Anwendungen instanziert, wie z.B. der Administrationsprozess. Im Folgenden werden daher nochmals alle Methoden und Properties aus Session zusammengefasst und kurz erläutert.
8.1 •
Bezug von Datenbanken Database getDatabase(String serverName, String databaseName) throws NotesException Database getDatabase(String serverName, String databaseName, boolean createOnFail) throws NotesException Öffnet die Datenbank mit Namen databaseName auf dem Server mit dem Namen serverName. Falls serverName null ist, wird der aktuelle Server ange-
nommen. Falls es sich nicht um eine lokale Notes-Session handelt, also z.B. um einen periodischen Agenten oder eine DIIOP-Session, so können nur Datenbanken auf dem aktuellen Server der Session geöffnet werden, es sei denn, es wurde im Serverdokument eine entsprechende Vertrauensstellung zwischen den Servern eingerichtet. Der Name der Datenbank muss den vollen Pfad einschließlich eventueller Ordner enthalten, in denen sich die Datenbank befindet. Es ist zulässig (auch unter Windows), in jedem Fall den Vorwärtsslash „/“ als Pfadtrenner zu verwenden. Falls der optionale Parameter createOnFail den Wert true hat, so wird die Datenbank neu angelegt, falls sie nicht geöffnet werden konnte und der aktuelle Benutzer entsprechende Rechte hat. Wird diese Methode mit zwei null-Parametern aufgerufen, liefert sie ein leeres, neu instanziertes Database-Objekt. •
Database getURLDatabase() throws NotesException
Liefert die Default-Web-Navigator-Datenbank des aktuellen Benutzers. Steht nur für lokale Notes-Sessions auf einer Client-Installation zur Verfügung. In der Regel ist die Web-Navigator-Datenbank die Datenbank mit dem Namen perweb50.nsf (auch für R6.x).
8.2 Einfache unabhängige Stil- und Eigenschafts-Objekte
8.2 •
329
Einfache unabhängige Stil- und Eigenschafts-Objekte ColorObject createColorObject() throws NotesException
Domino kennt eigene Farb-Objekte, die für die unterschiedlichsten Zwecke verwendet werden. Diese reichen von Textstilen in RichText Items bis hin zu Farbeigenschaften von Ansichtsspalten oder Maskenhintergründen. Ein ColorObject kennt die Darstellung von Farbräumen über RGB- oder HSLWerte, d.h. über Werte für Rot, Grün und Blau (jeweils zw. 0 und 255) oder für Farbton (0..240: 0=Rot, 80=Grün, 160=Blau), Sättigung (Anteil des Grauwertes; 0..240: 240 entspricht der reinen Farbe; der Wert 0 hat immer einen Grauton als Ergebnis) und Helligkeit (0..240: 0 ergibt immer Schwarz, 240 ergibt immer Weiß). Notes weicht mit den möglichen Werten für HSL leider vom Standard ab, der jeweils Werte im Bereich 0..360 für den Farbton (Gradangabe im Farbkreis) und 0..100 für Sättigung und Helligkeit (in Prozent) vorsieht. Bevor für ein Notes-Designelement oder einen RichTextStyle eine Farbe gesetzt werden kann, muss über createColorObject ein Farbobjekt erzeugt werden. Für dieses kann dann über die setter int setRGB(int red, int green, int blue) oder int setHSL(int hue, int saturation, int luminance) der gewünschte Farbwert gesetzt werden, wobei diese Methoden gleichzeitig als Ergebnis den nächst möglichen Wert der so genannten Notes-Farbe zurückgeben, der lediglich aus einem int besteht und die Werte von 0 bis 240 annehmen kann. Die ersten 16 Notes-Farben sind als Konstanten in RichTextStyle definiert und lauten: public public public public public public public public public public public public public public public public
Für ColorObject bestehen auch die getter-Methoden getRed(), getGreen(), getBlue(), getHue(), getSaturation() und getLuminance(), die die int Werte für die einzelnen Komponenten der beiden Farbmodelle liefern.
330 •
•
8 Session DateRange createDateRange() throws NotesException DateRange createDateRange(DateTime begin, DateTime end) throws NotesException DateRange createDateRange(Date begin, Date end) throws NotesException Ein DateRange Objekt ist ein Zeitraum, der aus zwei lotus.domino. DateTime- oder java.util.Date-Objekten besteht, die jeweils Anfang und Ende des Zeitraumes angeben. Sie können über die Methoden getStartDateTime(), setStartDateTime(DateTime datetime), getEndDateTime() und setEndDateTime(DateTime datetime) gesetzt und gelesen werden. DateTime createDateTime(Date date) throws NotesException DateTime createDateTime(Calendar calendar) throws NotesException DateTime createDateTime(String s) throws NotesException Über einen String, ein java.util.Date- oder ein java.util.Calendar (seit Version 6)-Objekt kann ein neues lotus.domino.DateTime-Objekt erstellt werden. Ein DateTime-Objekt ist die Domino-Repräsentation eines Zeit-
Objektes. Zulässige Formate für die Verwendung des String finden sich in Kapitel 12.2. •
International getInternational() throws NotesException Liefert ein lotus.domino.International-Objekt, das ähnlich wie der Ca-
Name createName(String name) throws NotesException Name createName(String name, String language) throws NotesException
Domino kann einen hierarchischen Domino-Namen in einem Name-Objekt kapseln. Dieses stellt über seine Properties verschiedene Teile des hierarchischen Namens zur Verfügung und erlaubt zusätzlich auch RFC E-Mail-Adressen aufzunehmen. Zum Beispiel kann der angemeldete User über getUserNameObject() als Name-Objekt geladen werden (s. Kap. 12.1). •
RichTextParagraphStyle createRichTextParagraphStyle() throws NotesException Ein RichTextItem ist, wie bereits in Kapitel 2.2.4.8 eingeführt, in Paragraphen aufgeteilt. Jeder Paragraph kann über ein RichTextParagraphStyle-Objekt
•
RichTextStyle createRichTextStyle() throws NotesException Ein RichText Item kann über ein RichTextStyle-Objekt mit Schriftstilen ver-
mit Layout-Eigenschaften versehen werden (s. Kap. 11.1ff).
Der Administrationsprozess ist die zentrale Verwaltungsengine in Domino. Über ihn werden viele der periodischen und unternehmensübergreifenden Aufgaben abgewickelt. Hierzu gehört die Erzeugung und Löschung von Repliken, Servern, Gruppen oder Benutzern, die Rezertifizierung von Benutzern oder Servern, domänenübergreifende Suchen nach Benutzern und Gruppen oder die Verwaltung von Gruppen und Serverclustern. Die meisten dieser Aufgaben werden abgewickelt, indem Aufträge als Notes-Dokumente in der Datenbank „Administration Requests“ (in der Regel admin4.nsf) hinterlegt und je nach Art der Aufgabe vom Administrationsprozess in entsprechend vordefinierten Intervallen abgearbeitet werden. Um dieses Objekt erzeugen und die Methoden nutzen zu können, werden volle Zugriffsrechte auf den Server und Zugriffsberechtigung für das Domino Directory benötigt. Seit Domino R6.5 kann der Administrationsprozess über createAdministrationProcess als Objekt instanziert und somit auch über Java gesteuert werden. •
AgentContext getAgentContext() throws NotesException Der AgentContext ist ein Objekt, das den Kontext eines im Domino Designer
erstellten und in Domino betriebenen Agenten darstellt. Er wird über getAgentContext geladen, sofern die Session im Kontext mit einem Agenten steht. Nähere Informationen finden Sie in Kapitel 3.4 ff. im Zusammenhang mit Agenten und in Kapitel 4.10. Die Dokumentenbasis, auf der ein Agent arbeitet, kann über Volltextsuchen eingeschränkt werden, wie in Kapitel 3.4.2 und 7.1.4 („Vordefinierte DocumentCollections“) beschrieben. •
DbDirectory getDbDirectory(String serverName) throws NotesException Liefert ein DbDirectory-Objekt, das die Datenbanken auf einem Domino-Server oder einer lokalen Arbeitsstation repräsentiert. Wurde über getDbDirectory ein DbDirectory-Objekt auf dem Server serverName bezogen, kann dieses mit getFirstDatabase() und getNextDatabase durch alle gefundenen Da-
tenbanken navigiert werden, ohne zuvor den Namen einer Datenbank zu kennen. Für serverName == null wird der aktuelle Server der Session verwendet. Für DIIOP-Sessions muss dieser Parameter mit null angegeben werden. DbDirectory kennt folgende Methoden: – –
Database createDatabase(String dbNameAndPath) throws NotesException Database createDatabase(String dbNameAndPath, boolean alreadyOpenIt) throws NotesException Erstellt eine Datenbank mit Namen dbNameAndPath und öffnet sie, falls alreadyOpenIt==true. Die neue Datenbank ist leer und enthält keine
Öffnet die erste gefundene Datenbank eines bestimmten Typs. databaseType kann die Werte DbDirectory.DATABASE (eine Domino-Datenbank „.nsf“), DbDirectory.TEMPLATE (ein Datenbanktemplate „.ntf“), DbDirectory.REPLICA_CANDIDATE (eine Datenbank oder ein Template, für das die Replikation nicht abgeschaltet ist) oder DbDirectory. TEMPLATE_CANDIDATE (eine beliebige Datenbank oder Template) annehmen. Beachten Sie, dass bei der Verwendung von DIIOP die Eigenschaft „Allow HTTP clients to browse databases:“ im Serverdokument unter „Internet Protocolls“ -> „Http“ auf „Yes“ gestellt sein muss, da die DIIOP-Session als Internetsession behandelt wird. Die Datenbank wird nicht automatisch geöffnet (siehe isOpen() und open() in Database). –
Database getNextDatabase() throws NotesException
Die nächste Datenbank des zuvor durch getFirstDatabase definierten Typs. Die Datenbank wird nicht automatisch geöffnet (siehe isOpen() und open() in Database). –
String getName() throws NotesException
Der Name des aktuellen Servers, auf dan sich das DbDirectory-Objekt bezieht. –
Session getParent() throws NotesException
Die aktuelle Domino-Session. – –
Database openDatabase(String dbNameAndPath) throws NotesException Database openDatabase(String dbNameAndPath, boolean failOverInCluster) throws NotesException Liefert die geöffnete Datenbank mit dem relativen Pfad dbNameAndPath,
wobei der Ordner und Dateiname innerhalb des Domino-Dataverzeichnisses angegeben werden kann. Ausser in DIIOP-Sessions kann mit failOverInCluster==true erlaubt werden, bei Nichtverfügbarkeit einer Datenbank innerhalb eines Clusters auf andere Cluster-Server auszuweichen. –
–
Database openDatabaseIfModified(String dbNameAndPath, DateTime datetime) throws NotesException Siehe openDatabase. Die Datenbank wird nur geöffnet, falls sie Dokumente enthält, die nach dem Zeitpunkt datetime geändert wurden. Andernfalls wird null zurückgegeben. Database openMailDatabase() throws NotesException
Über das Personendokument eines Benutzers im Domino-Verzeichnis wird festgelegt, wie die Mail-Datenbank eines Benutzers heißt. Über openMailDatabase() kann diese geöffnet werden und zwar auf demjenigen Server, auf dem sich die Maildatenbank des Benutzers befindet. Dies ist insbesondere bei lokalen Sessions interessant, da so ein direkter Bezug zwischen der lokalen Session und dem „Heim“-Server des Benutzers hergestellt werden kann.
Erzeugt einen Dxl Importer bzw. Exporter. Dxl steht für Domino-XML. Diese Methoden sind neu seit R6.5 (s. Kap. 17.7.3). •
Log createLog(String s) throws NotesException
Erzeugt ein Log-Objekt, mit dem innerhalb von Domino ein einfaches Logging ermöglicht wird. Log Informationen können in eine Datei, eine Notes-Datenbank, im File System oder in der Log Property eines Agenten gespeichert werden (s. Kap. 19.6.1). •
Ein Newsletter in Notes ist eine einfache Möglichkeit eine Sammlung von Informationen (Dokumente) an einen Mail-Empfänger zu senden. Sie wird automatisiert anhand der übergebenen DocumentCollection erzeugt. Über Document formatMsgWithDoclinks(Database database) wird ein Document erzeugt, das Links auf die Dokumente der DocumentCollection enthält. Dieses kann wie gewohnt über save gespeichert und mit send versendet werden. Document formatDocument(Database database, int indexOfDocumentInCollection) kann verwendet werden, um komplette Abbilder der ein-
zelnen Dokumente im Zieldokument zu erzeugen. Diese Methode muss für jedes einzelne Dokument der Collection angewendet werden. •
Registration createRegistration() throws NotesException Erzeugt ein Registration-Objekt. Ein Registration-Objekt bildet einer-
seits sämtliche Eigenschaften und Methoden ab, die benötigt werden, wenn neue Benutzer registriert werden sollen. Über dieses Objekt können folglich Batchprozesse für die automatisierte Benutzerregistrierung erstellt werden. Gleichzeitig liefert diese Klasse Methoden und Eigenschaften, die sich entweder auf ein ID File oder auf den Eintrag eines Benutzers im Domino-Verzeichnis beziehen. So kann z.B. mit String switchToID(String idFullFilePath, String password) in einer aktiven lokalen Session der aktuelle Benutzer gewechselt werden, indem auf ein neues ID File gewechselt wird. Dies hebt die in Kapitel 4.3 diskutierte Beschränkung auf lediglich den aktuellen Benutzer auf. •
Stream createStream() throws NotesException Seit Version 6 kennt Domino ein eigenes lotus.domino.Stream-Objekt, das allerdings auf den aktuellen NotesThread beschränkt ist und über createStream bezogen werden kann. Es handelt sich hierbei um einen Character-
(readText, writeText) oder Byte-Stream (read, write) und die Klasse kennt folgerichtig ein Characterset, das über getCharset() überprüft werden kann. Anders als z.B. java.io.Reader oder java.io.Writer wird der Stream automatisch intern gepuffert, wie es in java.io.Reader durch mark(int readAheadLimit) erreicht werden könnte.
334
8 Session Der lotus.domino.Stream kann sowohl als Input- als auch als OutputStream genutzt werden. Über void setContents(Reader reader), void setContents(InputStream inputstream), void getContents(Writer writer), void getContents(OutputStream outputstream) können klassische Java Streams mit dem Domino Stream gekoppelt werden. boolean isEOS() zeigt an, ob der Pointer sich am Ende des Streams befindet. setPosition (int) und getPosition können den Pointer innerhalb des Streams
neu positionieren bzw. dessen Position anzeigen. String readText() liest Text als Character Stream, String readText(int readLine) liest Text zeilenweise, wenn readLine==Stream.STMREAD_LINE. String readText(int readLine, int endOfLineChar) liest Text zei-
lenweise mit einem steuerbaren Zeilentrenner. endOfLineChar kann die Werte Stream.EOL_CRLF (ASCII 13 und ASCII 10), Stream.EOL_LF (ASCII 10), Stream.EOL_CR (ASCII 13), Stream.EOL_PLATFORM (plattformspezifisch), Stream.EOL_ANY (ASCII 10, 13 oder 13+10), Stream.EOL_NONE (per Default
wird kein Zeichen als Zeilenende definiert) annehmen. Dementsprechend existieren analog die Methoden writeText. Ein Domino Stream kann als Datei-Stream über boolean open(String filePath) oder boolean open(String filePath, String characterSet) geöffnet werden. Diese Methoden liefern false, falls die Datei nicht gefunden wurde, nicht geöffnet werden konnte, oder das spezifizierte CharacterSet nicht gültig ist, andernfalls true.
Die Befehle der Dominokonsole können seit Version 6.5 mit dieser Methode programmatisch ausgeführt werden. Der Parameter serverName gibt den Server an, auf dem der Befehl ausgeführt werden soll. Falls der Parameter null ist, wird der aktuelle Computer / Server der Session verwendet. Rückgabewert ist die Rückmeldung des Befehls, wie er in der Konsole erscheinen würde. Wird der Befehl in einem Domino-Agenten eingesetzt, der auf dem Server ausgeführt wird, muss der Signierer im Serverdokument unter „Security“ mit den vollen Rechten „Run unrestricted methods and operations“ ausgestattet sein. Wird der Befehl in einer lokalen Session auf einem Client eingesetzt, muss der Benutzer im Serverdokument mit vollen Konsolen-Rechten ausgestattet, also im Feld „Full Remote Console Administrators“ eingetragen sein. Da Konsolenbefehle eine Verbindung abbrechen können („Drop <<user>>“), muss in diesem Fall die Session neu aufgebaut werden. •
Formeln der @Formelsprache können über die Session evaluiert werden. Wird der optionale Parameter workingDocument angegeben, so können auch @Formeln, die sich auf ein Dokument beziehen, verwendet werden. Da @Formeln Listen zurückgeben können, wird ein Vector zurückgegeben, der optional alle Listenelemente liefert. Da einige @Formeln Bezug auf die UIKlassen (LotusScript) nehmen, können einige der @Formeln nicht angewendet werden. Hierzu gehören: @Command, @DialogBox, @PickList, @PostedCommand oder @Prompt. •
Da Domino ein Collaboration Tool ist, bietet es natürlich vorgefertigte Kalenderfunktionen. Über freeTimeSearch kann für einen oder für mehrere Benutzer ein freier Zeitraum in den Kalendern nachgeschlagen werden, der für alle gewünschten Teilnehmer gilt. Der Parameter fromTo gibt den gewünschten Zeitraum an, durationInMinutes die Länge des Zeitfensters, nach dem gesucht werden soll, in Minuten, nameAsStrinOrVector kann ein String oder ein Vector aus Strings sein und gibt einen oder mehrere Benutzer oder Gruppennamen an und firstMachtOnly bestimmt mit dem Wert true, ob lediglich eine oder mehrere Möglichkeiten zurückgegeben werden sollen. •
Base resolve(String uri) throws NotesException String getURL() throws NotesException String getHttpURL() throws NotesException Öffnet ein Domino-Objekt anhand seiner URI, die über getURL bezogen wer-
den kann (s. Kap. 7.1.4 – „Dokumente selektieren“).
Domino bietet über die @Formel die Möglichkeit Hashwerte auf Strings anzuwenden. So werden z.B. Internetpasswörter in Domino in dieser gehashten Form im Personendokument abgespeichert. Der Hashmechanismus kann in zwei verschiedenen Ausführungen verwendet werden. Dies hängt von der Einstellung im Serverdokument unter „Security“ -> „Internet Access“ -> „Internet Authentication“ ab. Seit Version 6.5 kann der Hashmechanismus direkt über hashPassword ausgeführt und über verifyPassword überprüft werden, wobei die Einstellungen im Serverdokument automatisch berücksichtigt werden. •
Wie bereits verschiedentlich herausgestellt, ist es unerlässlich Domino-Java-Objekte nach Gebrauch dem Recycling zuzuführen, damit die dahinter liegenden C++ Objekte aufgeräumt werden. recycle() räumt die aktuelle Session auf, während recycle (Vector) alle über den Vector referenzierten Objekte aufräumt (s. Kap. 14.3.1).
Wenn in Domino von Environment-Variablen die Rede ist, sind Parameter-Einträge in der Datei notes.ini gemeint. Domino unterscheidet zwischen DominoSystemparametern und Anwenderparametern, wobei letztere per Konvention mit einem Dollarzeichen „$“ beginnen. Diese Unterscheidung ist nicht technisch zwingend, aber dringend empfohlen. Per Default werden alle Parameter als Anwenderparameter mit einem führenden Dollarzeichen interpretiert. Über den Parameter isDominoParameter==true kann erzwungen werden, dass dieser weggelassen wird. Gesetzt werden kann eine Environment-Variable als Objekt, wobei die Klassen String, DateTime und Integer zulässig sind. Double oder Float sind ebenfalls zugelassen, werden aber gerundet und in Integer umgewandelt. Parameter können in jedem Fall über getEnvironmentString als String geladen werden. Gültige Einträge vom Typ DateTime oder Integer können über getEnvironmentValue als DateTime- oder Integer-Objekt geladen werden. Die Zuordnung und der Cast erfolgen automatisch. Parameter, die mit isDominoParameter==true gespeichert wurden, können auch nur mit isDominoParameter==true geladen werden und umgekehrt. •
Bezug des Anmeldetokens für SSO (s. Kap. 5.3.5f). •
Document getUserPolicySettings(String server, String user, int type) throws NotesException Document getUserPolicySettings(String server, String user, int type, String nameSpace) throws NotesException
8.5 Verschiedene Eigenschaften
337
Seit Version 6 können in Domino weitreichende und differenzierte Policies definiert werden, durch die für verschiedene Bereiche und Prozesse Defaultwerte definiert werden können, die zum Teil entweder einmalig z.B. bei einem Setup zur Anwendung kommen oder als dynamische Grundlage für bestimmte Einstellungen dienen. Policies können vom Typ Session.POLICYSETTINGS_ARCHIVE, Session.POLICYSETTINGS_DESKTOP, Session.POLICYSETTINGS_REGISTRATION, Session.POLICYSETTINGS_SECURITY oder Session. POLICYSETTINGS_SETUP sein. Über getUserPolicySettings können entweder die effektiven Policy Settings für einen bestimmten Benutzer geladen werden oder, falls der Parameter nameSpace zum Einsatz kommt, für einen bestimmten Namespace, wobei in diesem Fall der Parameter user ignoriert wird. Rückgabewert ist ein Document mit der entsprechenden Policy für den spezifizierten type. •
Vector getAddressBooks() throws NotesException Bezug aller für eine aktuelle Session gültigen Adressbücher. Die im Vector zurückgegebenen Database-Objekte sind geschlossen und müssen über open in Database geöffnet werden. Ist die Session lokal, werden alle privaten und öf-
fentlichen Adressbücher des aktuellen Benutzers zurückgegeben, beim Einsatz von DIIOP nur die öffentlichen Adressbücher des Servers. •
String getCommonUserName() throws NotesException
Bezug des Common Name (CN) des Users, der die Session geöffnet hat. •
String getUserName() throws NotesException String toString() Sowohl getUserName als auch toString in Session liefern den vollen Na-
men in der langen kanonischen Form des Users (z.B. CN=User Name/OU=Abteilung/O=Firma/C=Land), der die Session geöffnet hat. •
Name getUserNameObject() throws NotesException
Bezug des Namens des Users, der die Session geöffnet hat als lotus.domino. Name-Objekt. • •
Vector getUserNameList() throws NotesException Benutzername einschließlich alternativer Namen als Vector. Vector getUserGroupNameList() throws NotesException
Neu seit Domino R6. Liste der Gruppen, zu denen der aktuelle Benutzer gehört, einschließlich zusätzlicher, automatisch generierter Einträge für die hierarchischen Organisationen und Unterorganisationen, zu denen ein Benutzer gehört. •
Information, ob RichTextItems für diese Client Session in Mime konvertiert werden. Sollte, nachdem dieser Parameter verändert wurde, nach Gebrauch wieder auf den ursprünglichen Wert zurückgesetzt werden. Default ist true. •
boolean isOnServer() throws NotesException
Information, ob die Session auf einem Server läuft. •
boolean isValid()
Information, ob eine DIIOP-Session noch gültig ist (s. Kap. 5.3 ff), insbesondere 5.3.6.
Seit Version 6 ist es in Domino möglich, einen Server in gehostete Organisationen aufzuteilen. Wenn eine Session zu einer gehosteten Organisation gehört, liefert diese Methode den Default-Dateipfad für diese gehostete Umgebung, allerdings ohne führende Dateipfadzeichen, wie den Slash oder Backslash. Ansonsten liefert diese Methode den leeren String.
8.6
Zusammenfassung
Während Kapitel 4 und 5 ausführlich auf die NotesFactory eingingen und die Erzeugung einer Session näher beleuchteten, wurde in diesem Kapitel nun das eigentliche Java-Ojekt Session näher erläutert. Neben der zentralen Funktion, die Verbindung zum Domino-Server herzustellen und als Basis für alle Domino-Anwendungsfälle zu dienen, stellt die Session einige allgemeingültige Objekte bzw. Anwendungen bereit, die außerhalb des Objektkontextes und der üblichen Hierarchie aus Session, Database oder View und Document stehen. Hierzu gehören Datumsobjekte in Form der Klassen DateTime und DateRange, Namensobjekte in Form des Name-Objektes und Stilobjekte als RichTextStyle, RichTextParagraphStyle und Color und nicht zuletzt der Bezug eines International-Objektes. Das Öffnen von Datenbanken gehört ebenso zu den Aufgaben einer Session wie die Bereitstellung eines DbDirectory-Objektes als Abbild des Serververzeichnisses. Verschiedene administrative Anwendungen und Objekte, wie der Bezug eines AdministrationProcess, eines Log oder einer Registration ermöglichen es, über das JavaAPI Prozesse programmatisch zu steuern, während Methoden wie evaluate, zum Evaluieren von @Formeln, oder verifyPassword und freeTimeSearch unmittelbaren Zugriff auf das Collaboration Environment von Domino geben. Die Session ist also zuständig für Prozesse und Eigenschaften und die Steuerung bzw. Bereitstellung der Domino-Infrastruktur.
9
9 Database
In diesem Kapitel: Database API Database-Lifecycle Templating Replikation Service – Compact und Fixup
340
9 Database
Das Database-Objekt in Java kapselt eine Domino-Datenbank, bzw. ein DominoDatenbanktemplate, also eine nsf- oder ntf-Datei. Es dient nicht nur dazu, Datenbanken zu öffnen und handzuhaben, sondern über verschiedene Methoden und Properties können Datebanken geöffnet, durchsucht, erstellt, repliziert, kopiert, gelöscht, verschiedene Parameter und Einstellungen eingestellt und gelesen und schließlich Wartungsarbeiten durchgeführt werden. Geöffnet und erstellt werden können Datenbanken über die Klassen Session, DbDirectory, AgentContext, Database und über die getParent- bzw. getParentDatabase-Methoden der meisten Domino-Java-Objekte. Alle übrigen Funktionen werden direkt über Database ermöglicht (s. Tabelle 6-1). In Session können Datenbanken über getDatabase oder resolve geöffnet werden und spezielle Datenbanken über getURLDatabase und getAddressBooks geladen werden (s. Kap. 8). In DbDirectory können neue Datenbanken über createDatabase erstellt werden. Außerdem ist diese Klasse geeignet, Datenbanken über getFirstDatabase und getNextDatabase zu öffnen, für die zwar der Server, aber nicht deren Name bekannt ist. openDatabase, openDatabaseByReplicaID und openDatabaseIfModified ermöglichen das Öffnen von Datenbanken, deren Name, bzw. deren Replik ID bekannt ist, und openMailDatabase öffnet schließlich als spezielles Datenbankobjekt die Mail-Datenbank des Benutzers. Schließlich gibt es noch die Möglichkeit, im Falle, dass ein AgentContext zur Verfügung steht, die aktuelle Datenbank, in der sich der Agent befindet, zu dem der AgentContext gehört, über getCurrentDatabase in AgentContext zu öffnen.
9.1
Datenbanken öffnen, schliessen, replizieren und löschen
In Database selbst stehen folgende Methoden zum Öffnen, Erstellen, Kopieren, Löschen und zur Replikation zur Verfügung: •
Beim Öffnen von Datenbanken muss zwischen zwei Vorgängen unterschieden werden. Zum einen kann über verschiedene Methoden (z.B. über openByReplicaID) ein Datenbankobjekt bezogen werden. Um die Datenbank für die Verwendung zu öffnen, um den Zugriff auf die enthaltenen Dokumente und Designelemente zu ermöglichen, muss diese über open geöffnet werden. Die meisten Methoden zum Bezug des Datenbankobjekts öffnen die Datenbank implizit. Über isOpen kann überprüft werden, ob die Datenbank geöffnet ist. Ist sie das nicht, können nur einige wenige Eigenschaften abgefragt werden, wie z.B. der Datenbanktitel. getFirstDatabase und getNextDatabase in DbDirecotry sind Methoden, die zwar das Datenbankobjekt zurückliefern, aber die Datenbank nicht öffnen.
9.1 Datenbanken öffnen, schliessen, replizieren und löschen
341
Best Practice: Überprüfen Sie beim Öffnen von Datenbanken Replik IDs und Datenbanknamen auf Gültigkeit und verhindern Sie den Einsatz von Null-Werten oder leeren Strings. Je nach Konstellation zeigen die verschiedenen Methoden Performance-Vor- und Nachteile. Haben Sie hier den Verdacht eines Performance Leaks, überprüfen Sie die Zeiten für das Öffnen von Datenbanken und halten Sie geöffnete Datenbanken möglichst zentral vor.
Hat ein Benutzer zum Beispiel über die ACL keine Zugriffsberechtigung auf eine Datenbank, so kann er zwar mit getFirstDatabase und getNextDatabase eine solche Datenbank als (geschlossenes) Datenbankobjekt referenzieren und z.B. über getTitle() den Titel der Datenbank anzeigen, erhält jedoch eine NotesException 4060 „User CN=UserName/O=Organisation cannot open database relative\path\to\myDatabase.nsf“, wenn er versucht über open() die Datenbank zu öffnen. Dies ermöglicht Anwendungen, die zwar das Directory eines Domino-Servers anzeigen und die vorhandenen Datenbanken auflistet, ohne dabei die ACL zu unterlaufen. •
Neu seit Domino R6.5. Öffnet die Datenbank mit der Replik ID replicaID auf dem Server serverName. Wird für den Parameter serverName null übergeben, wird die Replik auf dem lokalen Server gesucht. Der Rückgabewert true zeigt an, dass die Datenbank gefunden und geöffnet werden konnte. •
boolean openIfModified(String serverName, String dbNameAndPath, DateTime modifiedSince) throws NotesException Neu seit Domino R6.5. Öffnet eine Datenbank mit Namen dbNameAndPath (einschließlich Pfadangabe) auf dem Server serverName, sofern diese Datenbank Dokumente enthält, die nach dem Zeitpunkt modifiedSince verändert wurden. Liefert true, wenn die Datenbank solche geänderten Dokumente ent-
Bei allen Methoden, die die Angabe eines Servers ermöglichen, gilt, dass entweder ein expliziter Servername genannt werden kann oder der leere String oder der String null angegeben werden kann, um den aktuellen Server zu spezifizieren. Bei DIIOP-Verbindungen ist zu beachten, dass diese Verbindungen keine Verbindung zu dritten Servern aufnehmen dürfen, solange keine Vertrauensstellungen zwischen diesen Servern (seit Version 6) hergestellt wurden. In diesem Fall muss entweder für den Parameter serverName der exakte Name des aktuellen Servers (z.B. über getServerName() in Session) angegeben werden, oder (String) null oder der leere String "".
342
9 Database
Warnung Vermeiden Sie es, Methoden auf Datenbankobjekte anzuwenden, die z.B. auf eine nicht existente Datenbank verweisen, oder Methoden, die eine geöffnete Datenbank erfordern, auf ein geschlossenes Datenbankobjekt anzuwenden. In der Regel werden NotesExceptions auftreten, unerwartete Ergebnisse bis hin zu Abstürzen im Einzelfall sind nicht ausgeschlossen und wurden vereinzelt beobachtet.
Neu seit Domino R6.5. Öffnet eine Datenbank auf dem Server serverName mit Name und Pfad dbNameAndPath. Befindet sich die Datenbank in einem Cluster und konnte die Datenbank auf dem bezeichneten Server nicht geöffnet werden, versucht Domino eine Replik der Datenbank auf einem anderen Server des Clusters zu öffnen. Liegt ein Cluster vor, wurde eine solche Datenbank gefunden, und konnte diese erfolgreich geöffnet werden, wird true zurückgegeben. Gleichzeitig wird die Änderung des Servernamens im zugrunde liegenden Database-Objekt adäquat angepasst. Befindet sich die Datenbank nicht in einem Cluster und konnte sie auf dem durch serverName spezifizierten Server geöffnet werden, wird diese Datenbank im Database-Objekt geöffnet. Da zur Verwendung dieser Methode ein Datenbankobjekt benötigt wird, bevor eine Datenbank geöffnet wird, kann hierfür über getDatabase (null, null) in Session ein leeres Datenbankobjekt instanziert werden. Dies gilt nur für lokale Sessions, die über NotesThread erzeugt wurden, nicht für DIIOPSessions. •
Erstellt eine Kopie der durch das aktuelle Datenbankobjekt repräsentierten Datenbank auf dem Server serverName mit (Datei-) Namen dbNameAndPath. Beim Kopieren werden keine Dokumente, sondern lediglich das Design der Datenbank kopiert, einschließlich der ACL der Datenbank, die als exakte Kopie angelegt wird. Der Parameter maxSizeGig gibt für Datenbanken, die noch im Dateiformat von Domino Version 4 gespeichert sind, die maximale Größe in Gigabyte an. Dieser Parameter kann die Werte 1..4 annehmen. Eine Kopie einer Datenbank ist keine Replik, sondern eine neue unabhängige Datenbank. Eine nachträgliche Replikation der Daten zwischen dem Original und der Kopie ist nicht möglich. Hierfür muss eine Replik mit createReplica erstellt werden. •
Erzeugt aus einer vorhandenen Datenbankschablone eine neue Datenbank. Basis ist das aktuelle Datenbank-Objekt, das createFromTemplate aufruft. Erzeugt wird die Datenbank auf dem spezifizierten Server und Dateinamen. Der Templatingmechanismus in Domino sieht vor, dass eine Datenbank auf Basis einer Vorlage erzeugt wird. Beim ersten Erzeugen gelten einige Regeln für die Übernahme der ACL. Ist der Parameter inheritInFuture auf true gesetzt, wird der Name des Templates (ein vom Datenbankname unabhängiger Name des Templates) hinterlegt und es kann zukünftig für eine Datenbank eine Aktualisierung der Gestaltung von einem Template dieses Templatenamens angefordert werden59. Handelt es sich bei dem Datenbankobjekt um ein Template, so liefert getDesignTemplateName diesen Templatenamen. Für die Übernahme der ACL gilt: Handelt es sich bei dem Template, das von dem aktuellen Datenbankobjekt repräsentiert wird, um eine NTF-Datei und ist die Eigenschaft „Database file is a Master Template“ gesetzt, so gilt für die ACL: Alle in eckige Klammern gesetzten Einträge werden als gültige Einträge ohne Klammern aufgenommen (z.B. [Benutzer/Organisation] wird als Benutzer/Organisation in die ACL aufgenommen). Dieser Mechanismus dient dazu, bestimmte Einträge in der ACL als Templates zu hinterlegen. Ist das Template eine NSF-Datenbank oder ist die Eigenschaft nicht gesetzt, so wird die ACL kopiert. In jedem Fall gilt: Wird die neue Datenbank auf einem Server neu angelegt, so wird dieser Server als Manager und als Administrationsserver in der ACL eingetragen. Der aktuelle Benutzer wird als Manager in der ACL eingetragen. Der Parameter maxSizeGig gibt für Datenbanken, die noch im Dateiformat von Domino Version 4 gespeichert sind, die maximale Größe in Gigabyte an. Dieser Parameter kann die Werte 1..4 annehmen. •
Erzeugt eine neue Replik der Datenbank auf dem spezifizierten Server und mit dem spezifizierten Namen. •
boolean replicate(String serverName) throws NotesException Stößt den Replikationsvorgang mit dem Server serverName an unter Beibehaltung der Replikationsregeln, die unter getReplicationInfo gespeichert sind.
59
Über den Menubefehl „File“ -> „Database“ -> „Refresh Design“ kann dieses Update der Gestaltung angefordert werden. Hierfür muss ein Server angegeben werden, von dem das Template dieses Namens angefordert werden soll. Domino kenn zusätzlich den so genannten Design Task, der diese Updates automatisch und regelmäßig durchführt. Hierbei ist zu beachten, dass dieser Automatismus unter Umständen für einzelne Datenbanken nicht erwünscht ist, bzw. zu unerwarteten Nebenwirkungen führen kann, wenn auf dem aktuellen Server ein zugehöriges Template zur Verfügung steht. Unerwünschte Effekte treten vor allem auf, wenn die Gestaltung einer Datenbank manuell aktualisiert wird und auf dem Server ein veraltetes Template vorliegt, so dass dann bei der Ausführung des Design Tasks die alte Gestaltung wiederhergestellt wird. Der Design Task kann über die Datei notes.ini im Feld TASKS durch Entfernen des Eintrags DESIGN abgeschaltet werden.
Liefert die Replikationsregeln für die aktuelle Datenbank. Diese können über Re-
•
plicationEntry getEntry(String sourceComputer, String targetComputer) throws NotesException und ReplicationEntry getEntry(String sourceComputer, String targetComputer, boolean create) throws NotesException erzeugt bzw. gelesen werden. void remove() throws NotesException
Löscht eine Datenbank endgültig aus dem Dateisystem. Nicht zu verwechseln mit recycle. Eine Datenbank kann nicht gelöscht werden, wenn sie in Benutzung ist. •
Neu seit Domino R6.5. Löscht eine Replik innerhalb eines Clusters. Die Löschung wird vom Cluster Manager durchgeführt. Wurde einmal die Eigenschaft markForDelete gesetzt, kann dies nicht rückgängig gemacht werden. Bevor der Cluster Manager die Datenbank löscht, wird dieser noch eine Replikation mit einer anderen Replik im Cluster durchführen, sofern eine vorliegt. isPendingDelete zeigt an, ob eine Markierung durch markForDelete vorliegt.
9.1.1
Vergleich – Methoden zum Öffnen einer Datenbank
Die häufigsten Methoden zum Öffnen einer Datenbank sind in der Praxis die Methode openDatabaseByReplicaID in DbDirectory und getDatabase in Session. Der entscheidende Unterschied zwischen diesen beiden Methoden ist die Verwendung des (vollständigen, relativen) Dateipfades im einen und der Replik ID im anderen Fall. Beide Methoden haben ihre Vor- und Nachteile. Während für getDatabase immer der relative Dateipfad innerhalb des Domino-Dataverzeichnisses vollständig bekannt sein muss, es aber möglich ist, sofern serverübergreifend dieselbe Ordnerstruktur beibehalten wird, einmalig einen Schlüssel (den Dateinamen) für eine Datenbank zu vergeben, der über verschiedene Installationen beibehalten werden kann, muss openDatabaseByReplicaID die Replik ID bekannt sein, die sich mit jeder Installation ändern wird. Stellt man Performance-Untersuchungen an, stellt man fest, dass getDatabase bei der Verwendung von NotesThread nicht sehr schnell arbeitet. openDatabaseByReplikID ist hier viel schneller. Allerdings wird für die Instanzierung von DbDirectory so viel Zeit benötigt, dass der Vorteil verloren geht und erst beim Öffnen von sehr vielen Datenbanken auf Basis von einem DbDirectory-Objekt zum Tragen kommt. Bei der Verwendung von DIIOP dreht sich das Verhältnis zu Gunsten von getDatabase (s. Tabelle 9-1). Beachten Sie bei Verwendung aller Methoden zum Öffnen von Datenbanken, dass in keinem Fall null-Parameter als Replik ID oder Datenbankname übergeben werden. In vielen Versionen von Domino wurden in unterschiedlichen Konstallationen bei der Verwendung von null-Parametern leider immer wieder Probleme bis hin zu Serverabstürzen beobachtet. Hier ist es sinnvoll für jedes Öffnen einer Daten-
9.2 Datenbanken, Dokumente und Document Collection
345
Tabelle 9-1 openDatabaseByReplicaID und getDatabase
Schlüssel, anhand dessen die Datenbank geöffnet wird Performance bei Verwendung von NotesThread
Performance bei Verwendung von DIIOP
Robustheit
Recycling (s. Kap. 14.3.2)
openDatabaseByReplicaID
getDatabase
Replik ID – Diese muss bekannt sein, wenn nicht das gesamte DbDirectory durchsucht werden soll. Ist die Replik ID bekannt, erheblich schneller als getDatabase. Allerdings wird der Vorteil beim Öffnen weniger Datenbanken oder nur einer durch den Zeitverbrauch für die Instanzierung von DbDirectory zu Gunsten von getDatabase umgekehrt. Deutlich langsamer als bei der Verwendung von NotesThread.
Vollständiger Dateipfad relativ zum Data Directory.
Reagiert in einigen Domino-Versionen empfindlich auf null- bzw. ""Parameter für die ReplikID. Jedes Objekt muss recycelt werden
Insgesamt gute Performance, da zwar getDatabase langsamer als openDatabaseByReplicaID ist, aber kein DbDirectory instanziert werden muss. Erheblich schneller als bei der Verwendung von NotesThread. Schneller als openDatabaseByReplicaID. Unkritisch.
Identische Datenbanken werden durch identische Objekte repräsentiert und müssen nur einmal recycelt werden.
bank, egal für welche Methode Sie sich entscheiden, sicherzustellen, dass null-Werte und leere Strings als Parameter vermieden werden. Es kann sinnvoll sein, ein eigenes Wrapper-Objekt einzusetzen, das sich ums Öffnen und Bereitstellen von Datenbanken kümmert und dabei gleich unzulässige Replik IDs oder Dateinamen identifiziert.
9.2
Datenbanken, Dokumente und Document Collection
Die Methoden in Database zur Behandlung von Dokumenten und DocumentCollections wurden bereits in vorherigen Kapiteln behandelt. getAllDocuments() liefert alle Dokumente einer Datenbank (s. Kap. 7.1.4.3), createDocument, getDocumentByID, getDocumentByUNID und getDocumentByURL wurden in Kapitel 7.1.4.1 erläutert. Informationen zu getProfileDocCollection und getProfileDocument finden Sie im Kapitel 7.2 über Profildokumente. Bleibt in diesem Zusammenhang noch die Methode NoteCollection createNoteCollection(boolean selectAll) zu erwähnen, mit der Sie eine NoteCollection (s. Kap. 17.7.1) aus der Datenbank erstellen können. Dieses Objekt wird dazu verwendet aus einer Datenbank alle oder einzelne Domino-Notes, also z.B. Gestaltungselemente oder Dokumente, zu selektieren. Üblicherweise wird eine leere NoteCollection mit createNoteCollection (false) erstellt, dann wird über setSelectDocuments, setSelectForms, setSelectFiews usw. festgelegt, wel-
346
9 Database
che Elemente zur Collection gehören sollen und dann wird diese NoteCollection mit buildCollection in NoteCollection aufgebaut. Die Methode createNoteCollection und die Klasse NoteCollection sind neu seit Version 6.5.
9.3
Allgemeine Datenbankeigenschaften
Im Folgenden finden sich setter- und getter-Methoden in Database für verschiedene Datenbankeigenschaften. • •
DateTime getCreated() throws NotesException DateTime-Ojekt mit dem Erstelldatum und -uhrzeit der Datenbank. DateTime getLastModified() throws NotesException DateTime-Ojekt mit dem letzten Änderungsdatum und -uhrzeit der Daten-
bank. •
String getReplicaID() throws NotesException
Die Replik ID der Datenbank. Diese 16-stellige Hexadezimalzahl wird als String zurückgegeben. Die Datenbank muss nicht geöffnet sein, um die Replik ID auszulesen. •
String getServer() throws NotesException
Der Servername des Servers, auf dem sich die Datenbank befindet, in der hierarchischen Langform (z.B. CN=Java/O=DJBUCH) aus. Die Datenbank muss nicht geöffnet sein, um den Servernamen auszulesen. •
String getTemplateName() throws NotesException
Der Templatename, unter dem eine Datenbank als Template geführt wird, wenn die Eigenschaft „Database file is a master template“ gesetzt ist, andernfalls der leere String. Die Datenbank muss nicht geöffnet sein, um den Templatenamen auszulesen. •
String getFilePath() throws NotesException
Liefert den vollen oder relativen Dateipfad einschließlich Dateiname der Datenbank. Liegt die Datenbank lokal, wird der vollständige Dateipfad (z.B. c:\lotus\ notes\data\djbuch\djbuch.nsf) ausgegeben, liegt sie auf einem Server oder wird sie über eine Remote-Session referenziert, wird der relative Pfad zum Dataverzeichnis (z.B. djbuch\djbuch.nsf) ausgegeben. Wurde die Datenbank auf Basis eines Verzeichnis- oder Datenbanklinks60 geöffnet, wird, im Falle einer lokalen Session, diese Methode den tatsächlichen Dateipfad der Datenbank liefern, wenn die Datenbank, auf die der Link verweist, ebenfalls lokal liegt und den Link-Pfad, wenn die Datenbank auf einem Server liegt. 60
Im Domino Administrator können für ganze Verzeichnisse oder für einzelne Ordner virtuelle Namen eingerichtet werden, die dann auf tatsächliche Ordner oder Dateien im Filesystem verweisen. Solche Ordner- oder Dateinamen können dann über die Methoden, die den Pfad einer Datenbank verlangen, verwendet werden. Solche Links können im Domino Administrator im Bereich „Files“ über den Menübefehl „Files“->„New“->„Link“ erstellt werden.
9.3 Allgemeine Datenbankeigenschaften
347
Falls die Session auf dem Server ausgeführt wird (DIIOP), so wird in jedem Fall der volle tatsächliche Dateipfad der Datenbank zurückgegeben, wenn es sich bei der Datenbank um einen Link handelt. Die Datenbank muss nicht geöffnet sein, um den FilePath auszulesen. Allerdings ist zu beachten, dass ein Database-Objekt einer nicht existenten Datenbank durchaus mit getDatabase (<<ServerName>>, <>) geöffnet werden kann. Dann liefert getFilePath diesen im Parameter übergebenen, nicht existenten DateiPfad, ohne eine Exception zu werfen. Anhand von isOpen () lässt sich aber feststellen, dass das Öffnen nicht erfolgreich war. •
String getFileName() throws NotesException
Liefert den Dateinamen einer Datenbank, jedoch ohne Pfadangabe. Wird die Methode in einer lokalen Session aufgerufen und der FileName einer Datenbank, die über einen Datenbanklink geöffnet wurde, erfragt, so liefert sie den Namen des Links, wenn die Datenbank auf dem Server liegt und den tatsächlichen Dateinamen, wenn die verlinkte Datei lokal liegt. Die Datenbank muss nicht geöffnet sein, um den FileName auszulesen. Auch hier gilt, dass auch der FileName einer nicht existierenden Datenbank angezeigt werden kann. Eine ausführliche Auflistung befindet sich in Tabelle 9-2 •
Neu seit Domino R6.5. Für jede Datenbank können über das Kontextmenü erweiterte Eigenschaften eingestellt werden. Diese können seit Version 6.5 mit setOption programmatisch geändert werden. Über den Parameter option können die Eigenschaften anhand der Konstanten in Database wie folgt über switch ein- und ausgeschaltet werden. Für switch == true bedeutet dies im Einzelnen: Database.DBOPT_NOUNREAD – Es werden keine Ungelesenkennzeichen in
der Datenbank verwaltet. Diese Option kann sinnvoll sein, um die Geschwindigkeit zu optimieren. Wenn diese Option verändert wird, muss die Datenbank komprimiert werden. Siehe Database.compact(). Database.DBOPT_OPTIMIZATION – Durch diese Option werden interne Tabellen, in denen Referenzen zwischen Dokumenten, Ansichten und Masken geführt werden optimiert. Vorteile hat dies nur für Ansichten mit Select-Formeln, die „Form=...“ enthalten, und insbesondere für kleine Ansichten in großen Datenbanken. Wenn diese Option verändert wird, muss die Datenbank komprimiert werden. Siehe Database. compact().
9 Database
348
Tabelle 9-2 getFileName und getFilePath Session
Datenbank
Anzeige durch getFilePath
lokal lokal lokal lokal remote remote
lokal, Link lokal auf Server, Link auf Server auf Server, Link auf Server
lokal, Link lokal auf Server, Link auf Server auf Server, Link auf Server
tatsächlicher Name der Datei tatsächlicher Name der Datei Name des Dateilinks Name des Datei tatsächlicher Name der Datei tatsächlicher Name der Datei
Database.DBOPT_NOOVERWRITE – Verhindert das Überschreiben von lee-
rem Speicherplatz innerhalb einer Datenbank. Database.DBOPT_MAINTAINLASTACCESSED – Aktiviert die Verwendung
der LastAccessed-Eigenschaft von Dokumenten (s. Kap. 7.6). Database.DBOPT_NOTRANSACTIONLOGGING – Deaktiviert das Transakti-
onslog. Database.DBOPT_SOFTDELETE – Aktiviert die Möglichkeit der Softdele-
tion (s. Kap. 7.1.4.6). Database.DBOPT_NORESPONSEINFO – Verhindert die Speicherung interner Informationen, die von den @Funktionen @AllChildren und @AllDescendants benötigt werden. Werden diese Funktionen nicht benötigt (sie finden z.B. in SELECT-Formeln von Ansichten Verwen-
dung), so kann durch Setzen dieses Flags eine deutliche Performanceverbesserung erreicht werden. Wenn diese Option verändert wird, muss die Datenbank komprimiert werden. Siehe Database.compact(). Database.DBOPT_NOHEADLINEMONITORS – Verhindert die Verwendung von Datenbankabonnements, mit deren Hilfe Benutzer bestimmte Datenbanken auf neue Einträge überwachen lassen können. Database.DBOPT_MOREFIELDS – Erlaubt die Verwendung von mehr Feldern pro Datenbank. Statt des sonst gültigen Limits von 64 KByte für die Gesamtlänge aller Feldnamen wird durch Setzen dieser Option ermöglicht, bis zu 23.000 Feldnamen in einer Datenbank zu speichern. Database.DBOPT_LZCOMPRESSION – Aktiviert die Verwendung von LZ1Kompression von Anhängen für die Datenbank.
9.3 Allgemeine Datenbankeigenschaften
349
Database.DBOPT_LZ1 – Aktiviert die Verwendung von LZ1-Kompression
von Anhängen für die Datenbank. Database.DBOPT_REPLICATEUNREADMARKSTOCLUSTER – Aktiviert die
Replikation von Ungelesenkennzeichen innerhalb der Cluster-Replikation. Wird diese Option auf true oder false gesetzt, wird DBOPT_REPLICATEUNREADMARKSTOANY auf false gesetzt. Database.DBOPT_REPLICATEUNREADMARKSTOANY – Aktiviert die Replikation von Ungelesenkennzeichen auf alle Server. Wird diese Option auf true gesetzt, wird DBOPT_REPLICATEUNREADMARKSTOCLUSTER auf false gesetzt. •
Liest die durch option spezifizierte Eigenschaft. •
double getPercentUsed() throws NotesException
Liefert den Prozentteil, der innerhalb des Datenbankfiles genutzt wird. Durch die Methode compact in Database (s. Kap. 9.8) oder den Konsolenbefehl load compact <> kann eine Kompression der Datenbank durchgeführt werden. Durch die Option -B kann eine Dateigrößenreduktion erreicht werden. •
double getSize() throws NotesException
Dateigröße der Datenbank im Dateisystem in Byte. •
int getSizeQuota() throws NotesException
Für Datenbanken können Quota-Angaben gemacht werden, durch die die Maximalgröße limitiert wird. Die Datenbank muss nicht geöffnet sein, um diesen Wert abzufragen. •
Setzt das Datenbank-Quota. Angegeben wird das Quota in KiloByte. Wird das Quota überschritten, wird beim Schreiben von Dokumenten eine NotesException „NotesException: Notes error: Unable to write to database because database would exceed its disk quota.“ mit ID=4000 geworfen. Wird ein Quota neu gesetzt, so setzt sich dieses nicht sofort am Server durch. Da Domino die Daten beim Speichern innerhalb des NSF Files optimiert und zum Teil auch komprimiert, besteht immer eine gewisse Diskrepanz zwischen dem gesetzten Wert und dem Moment, zu dem die Exception geworfen wird. •
long getSizeWarning() throws NotesException void setSizeWarning(int i) throws NotesException Der Parameter sizeWarning korrespondiert mit dem Datei-Quota. Sie sollte
niedriger als das Quota gesetzt werden. Ein Benutzer wird gewarnt, wenn er diesen Wert überschreitet. Speichern ist jedoch weiterhin möglich. Für die Benutzung von getSizeWarning muss die Datenbank nicht geöffnet sein. •
long getMaxSize() throws NotesException
Das Datei-Quota ist nicht zu verwechseln mit der Maximalgröße einer Datenbank. Diese Größe kann nicht überschritten werden. Sie ist nur für Datenbanken, die noch im Dateiformat von Domino R4 gespeichert wurden, gültig und kann nur über den Domino Administrator geändert werden. •
9 Database Setzt und liest den Titel der Datenbank. Nicht zu verwechseln mit dem Dateinamen. Die Datenbank muss für die Benutzung von getTitle nicht geöffnet sein.
•
int getType() throws NotesException
Neu seit Domino R 6.5. Liefert den Typus einer Datenbank. Hier wird angezeigt, ob es sich um einen besonderen Datenbanktypus handelt. Zurückgegeben wird eine der Konstanten aus Database:
•
DBTYPE_WEB_APP, DBTYPE_MAILFILE, DBTYPE_MAILBOX, DBTYPE_SUBSCRIPTIONS, DBTYPE_NEWS_SVR_PROXY, DBTYPE_IMAP_SVR_PROXY, DBTYPE_PORTFOLIO, DBTYPE_MULTIDB_SRCH, DBTYPE_LIGHT_ADDR_BOOK, DBTYPE_ADDR_BOOK, DBTYPE_PERS_JOURNAL, DBTYPE_LIBRARY oder DBTYPE_STANDARD. int getFileFormat() throws NotesException
Liefert das Format der OnDiskStructure der NSF-Dateien. So wird z.B. für eine R6.x Datenbank 43 zurückgegeben. •
boolean isLink() throws NotesException Liefert true, sofern die Datenbank auf Basis eines Verzeichnis- oder Datenbank-
•
boolean isPublicAddressBook() throws NotesException Liefert true, sofern es sich bei der Datenbank um das Domino-Verzeichnis des
links geöffnet wurde.
Servers handelt (auch Domino-Adressbuch oder Domino Directory genannt). In der Regel ist dies die Datenbank names.nsf auf einem Server. •
boolean isPrivateAddressBook() throws NotesException Liefert true, sofern es sich bei der Datenbank um ein persönliches Adressbuch
handelt. In der Regel ist dies die Datenbank names.nsf auf einem Client-Computer. •
Diese Methoden arbeiten analog zu den gleichnamigen Methoden in Document (s. Kap. 7.1.4.2).
9.3.1 •
Weitere Datenbankeigenschaften
boolean isDB2() throws NotesException
Neu seit Domino R7. Zeigt an, ob eine Datenbank im NSFDB2-Format vorliegt61 (s. Kap. 13.4). 61
In der zum Zeitpunkt der Drucklegung vorliegenden Version R7.0 waren für diese Methode bisher nur die lokalen Methoden aus lotus.domino.local.Database implementiert und eine Benutzung über DIIOP nicht möglich.
9.3.1 Weitere Datenbankeigenschaften
351
Falls eine Datenbank in diesem Format vorliegt, jedoch DB2 nicht erreichbar oder aus sonstigen Gründen nicht verfügbar ist, liefert diese Methode ebenfalls false. Nur wenn die Datenbank über NSFDB2 erfolgreich geöffnet werden kann, liefert isDB2 true. •
String getDB2Schema() throws NotesException
Neu seit Domino R7. Gibt das DB2-Schema62 einer Datenbank im NSFDB2-Format zurück. Dies ist der im Domino Designer als „db2 groups“ bezeichnete Schemaname (z.B. „GRP1“). •
In Domino-Dokumenten werden im Item $Revisions Datumsangaben der Änderungszeitpunkte gespeichert. Durch Setzen dieses Parameters kann seit Domino R6.5 die Anzahl auf 0 bis 2147483647 (Integer.MAX_VALUE) Werte limitiert werden. •
In Domino-Dokumenten werden im Item $UpdatedBy die Namen der Personen, die ein Dokument geändert haben, in der hierarchischen Langform (CN=Vorname Nachname/O=DJBUCH) gespeichert. Dieses Item wird erst beim zweiten Speichern zum ersten Mal gesetzt. Während $Revisions bei jedem Speichern gesetzt wird, wird $UpdatedBy nur gesetzt, wenn zwischenzeitlich ein anderer Benutzer das Dokument speichert. Durch Setzen dieses Parameters kann seit Domino R6.5 die Anzahl auf 0 bis 2147483647 (Integer.MAX_VALUE) Werte limitiert werden. •
int getUndeleteExpireTime() throws NotesException void setUndeleteExpireTime(int timeInHours) throws NotesException
Werden in einer Datenbank SoftDeletions zugelassen (s. Kap. 7.1.4.6), so kann über diesen Parameter die Zeit in Stunden angegeben werden, nach der die Löschung eines Dokuments nicht mehr rückgängig gemacht werden kann. •
•
String getURLHeaderInfo(String url, String headerName, String userName, String userPassword, String proxyUser, String proxyPassword) throws NotesException Liest einen HTTP-Header aus der unter der URL url gelieferten Webadresse. Vergleiche getDocumentByURL in Kapitel 7.1.4.2. boolean isDelayUpdates() throws NotesException void setDelayUpdates(boolean flag) throws NotesException
Wird diese Eigenschaft für eine Datenbank gesetzt, so werden Speicher- und Löschoperationen für Dokumente nicht sofort, sondern mit einem Delay, sofern Domino dies günstig erscheint, durchgeführt. Dies kann zu Performanceverbesserungen führen, birgt jedoch das Risiko, dass im Falle eines Crashs Daten verlorengehen. 62
dito
352
9 Database
9.4
Suche und Catalog
In Domino stehen verschiedene Suchkonzepte zur Verfügung. Diese werden in Kapitel 15 ausführlich vorgestellt. Neben der Suche in Voltextindizes oder Ansichten in Datenbanken stellt Domino verschiedene Verzeichnisdienste und Volltextindizes über mehrere Datenbanken zur globalen Suche (DomainSearch, seit Domino R5) zur Verfügung. Hierfür werden verschiedene Methoden in Database zur Verfügung gestellt, die im Folgenden kurz vorgestellt werden. Insbesondere die Verzeichnisdienste und die Domainsearch erfordern insbesondere administrative Tätigkeiten, deren Beschreibung nicht Ziel dieses Buches ist. Daher sind diese Methoden aus Database hier lediglich kurz erläutert. •
DocumentCollection FTSearch(String searchQuery) throws NotesException DocumentCollection FTSearch(String searchQuery, int max) throws NotesException DocumentCollection FTSearch(String searchQuery, int max, int options, int otherOptions) throws NotesException DocumentCollection FTSearchRange(String searchQuery, int max, int options, int otherOptions, int start) throws NotesException Mit der Methode FTSearch können Datenbanken mit einer Volltextsuche
durchsucht werden. Die Suche findet über alle Dokumente, unabhängig von einzelnen Items statt. Sie kann über eine bestimmte Syntax der searchQuery und verschiedene Suchparameter gesteuert werden. Näheres hierzu in Kapitel 15. FTSearchRange ist neu seit Domino R6. •
DocumentCollection search(String searchQuery) throws NotesException DocumentCollection search(String searchQuery, DateTime cuttOfDateTime) throws NotesException DocumentCollection search(String searchQuery, DateTime cuttOfDateTime, int maxResults) throws NotesException Neben Unterschieden in der Syntax für die searchQuery unterscheiden sich FTSearch und search in der Herangehensweise. Während FTSearch eine Volltextsuche über den Datenbankindex durchführt, ist search vergleichbar mit den SELECT-Formeln, die in Views verwendet werden. FTSearch kennt typi-
sche Funktionen, die aus verschiedenen Suchmaschinen bekannt sind, wie Wortbeugungen oder Fuzzysearch, während für search die @Formelsprache zum Einsatz kommt. Näheres hierzu in Kapitel 15. •
Damit eine Volltextsuche effizient arbeiten kann, muss ein Datenbankindex erzeugt werden. Dies kann mit der Methode createFTIndex erreicht werden. Mögliche Optionen werden durch folgende Konstanten in Database gesteuert. Mehrere Optionen können gleichzeitig gesetzt werden, indem sie addiert werden. FTINDEX_ENCRYPTED_FIELDS – Verschlüsselte Felder werden ebenfalls in-
diziert. Beachten Sie, dass dies bis zu einem gewissen Grad die Verschlüsselung aushebelt. Werden z.B. bei einer Suche nach einem Stichwort Dokumente gefunden, die das Stichwort in verschlüsselten Feldern enthalten, kann zwar ein Benutzer die Daten als solche in ihrer Gesamtheit nicht lesen, erhält aber zumindest die Information, dass Daten zu diesen Stichworten vorliegen. Bei sensiblen Daten kann allein diese Information ausreichen, um die Datensicherheit zu korrumpieren. FTINDEX_ALL_BREAKS – Sätze und Absätze werden ebenfalls indiziert. Dies ermöglicht der FTSearch-Suchoperationen derartig durchzuführen, dass die Trefferwertung abhängig davon gemacht wird, wie nahe zwei Worte innerhalb eines Absatzes zueinander stehen. Die Suchsyntax hierzu ist Wort1 PARAGRAPH Wort2. FTINDEX_CASE_SENSITIVE – Der Index unterscheidet nach Groß- und Kleinschreibung. Die Suche kann dann mit dem Schlüsselwort EXACTCASE nach Wörtern in exakter Schreibweise suchen. Beispielsweise findet die Suchquery „EXACTCASE DOMINO“ nur Dokumente, in denen Domino komplett groß geschrieben ist. „Domino“ oder „dominO“ würde nicht gefunden. FTINDEX_ATTACHED_FILES – Dateianhänge in Dokumenten werden ebenfalls indiziert. FTINDEX_ATTACHED_BIN_FILES – Kann zusätzlich (und nur zusätzlich) zu FTINDEX_ATTACHED_FILES verwendet werden. In diesem Fall werden Anhänge, sofern unterstützt, nicht nur anhand der Rohdaten, sondern soweit möglich anhand der generischen Anhangsformate indiziert, so dass Anhänge wie Word- oder PDF-Dokumente deutlich besser durchsucht werden können. Diese Option entspricht der Indizierungseigenschaft „using conversion filters on supported files“.
• •
removeFTIndex () löscht einen Volltextindex und updateFTIndex aktualisiert ihn, wobei er nur neu erstellt wird, falls createOnFail true ist. boolean isFTIndexed() throws NotesException Liefert true, sofern eine Datenbank volltextindiziert ist. DateTime getLastFTIndexed() throws NotesException void setFTIndexFrequency(int option) throws NotesException int getFTIndexFrequency() throws NotesException
Diese Methoden steuern die Updatehäufigkeit für einen Volltextindex. getLstFTIndexed liefert Datum und Uhrzeit der letzten Indizierung. Der Rhythmus der Indizierung kann anhand der folgenden Konstanten aus Database gesetzt werden:
354
9 Database
Best Practice Verwenden Sie die Methoden queryAccess, queryAccessPrivileges und queryAccessRoles, wenn es darum geht, die aktuellen detaillierten Rechte des Benutzers aufzu-
lösen. Domino verwendet hierfür dieselben Prozesse wie bei der Ermittlung der Berechtigungen beim Zugriff auf Domino-Objekte. Verwenden Sie die Methoden in ACL, wenn Sie einzelne Einträge der ACL manipulieren möchten, also wenn es darum geht, Rechte für Benutzer, Server oder Gruppen hinzuzufügen, zu verändern oder zu löschen.
FTINDEX_DAILY – Indizierung einmal täglich. FTINDEX_HOURLY – einmal stündlich. FTINDEX_IMMEDIATE – sofort nach der Änderung eines Dokuments. FTINDEX_SCHEDULED – entsprechend dem Schedule des Servers.
Nimmt die Datenbank in die datenbankübergreifende Domainsearch auf oder entfernt sie daraus. •
boolean isMultiDbSearch() throws NotesException
Zeigt an, ob es sich bei der Datenbank um eine Domainsearch-Datenbank (Domain Catalog) handelt. •
Document FTDomainSearch(String searchQuery, int max, int options, int otherOptions, int start, int pageCount, String searchForm) throws NotesException Kann nur angewendet werden, falls isMultiDbSearch() == true. Liefert ein
Dokument mit den Ergebnissen der Domainsearch. Für die Abfrage in searchQuery gelten die gleichen Regeln wie für FTSearch, wobei noch einige zusätzliche Parameter zugelassen sind. pageCount zeigt an, wieviele Trefferseiten zurückgegeben werden sollen. searchForm gibt die Maske an, mit denen die
Zeigt an, ob es sich bei der Datenbank um einn Domino Directory Catalog (basierend auf dircat5.ntf) handelt. Funktioniert nur, wenn das Datenbankobjekt über getAddressBooks in Session bezogen wurde. •
Neu seit Domino R6.5. Zeigt an, ob es sich bei der Datenbank um ein Domino Configuration Directory handelt. Funktioniert nur, wenn das Datenbankobjekt über getAddressBooks in Session bezogen wurde. •
Zeigt an, unter welchen Kategorien die Datenbank im Datenbankkatalog (catalog.nsf) erscheinen soll (s. Abb. 9-1).
9.5
Sicherheit
In Kapitel 4.16 und Abbildung Abb. 4-9 wurde die Hierarchie der in Domino zum Tragen kommenden Sicherheitsmechanismen beschrieben. Dort wurde insbesondere auf die Ausführungsberechtigungen und die programmatische Sicherheit eingegangen. Die wichtigste Sicherheitsfunktion in Domino ist die Datenbank ACL (Access
Abb. 9-1 Datenbankeigenschaften
356
9 Database
Control List – Zugriffskontrollliste – s. Abb. 9-2)63. Über sie wird gesteuert, wer grundsätzlich Zugriff auf eine Datenbank erhält und mit welchen Rechten. Die ACL wird in einer eigenen Note gespeichert und im Java-Objekt ACL abgebildet. Die Berechtigungen sind wie folgt eingeteilt. Zunächst gibt es eine grundsätzliche Einteilung in sieben Standardrollen (s. Tabelle 9-3), wie z.B. Leser oder Manager. Diesen Standardrollen sind Maximalrechte zugeordnet (ein Leser darf keinerlei Dokumente
Abb. 9-2 ACL
erstellen) und ihnen können optionale Berechtigungen (s. Tabelle 9-4) zugeordnet oder entzogen werden (einem Autor kann z.B. das Recht, Dokumente zu löschen, entzogen werden). Jedem Eintrag in der ACL werden diese Standardrollen und solche optionalen Berechtigungen zugeordnet. Hierbei ist zu beachten, dass für einen Eintrag in der ACL festgelegt werden kann, für welche Arten von Benutzern, Servern oder Gruppen er gelten soll. Ist ein Eintrag z.B. einer Person zugeordnet, so würden die durch diesen Eintrag spezifizierten Rechte nicht für einen Server Anwendung finden, der zufällig den gleichen Namen hätte, wie in diesem Eintrag festgelegt. Soll ein Eintrag unabhängig von der Art des Benutzers gelten, muss er als „Unspecified“ markiert werden. Neben diesen optionalen Rechten und den Standardrollen können in der ACL entweder im ACL-Dialog des Notes Clients oder über renameRole(String s, String s1), addRole(String s), deleteRole(String s) und getRoles() in ACL eigene benutzerdefinierte Rollen erstellt werden. Diese werden in den einzelnen ACLEntry-Einträgen über enableRole(String roleName) und disableRole(String roleName) gesetzt bzw. über isRoleEnabled(String roleNa63
Die ACL wird im Notes Client über „File“ -> „Database“ -> „Access Control“ oder mit rechtem Mausklick auf das Symbol der Datenbank geöffnet.
9.5 Sicherheit
357
Tabelle 9-3 Zugriffslevel in der ACL MANAGER
DESIGNER EDITOR
AUTHOR READER
DEPOSITOR
NOACCESS
Ein Manager der Datenbank darf Dokumente der Datenbank lesen und ändern und optional löschen. Dies gilt nicht für Dokumente, für die ein Manager nicht in einem eventuell vorhandenen Leserfeld eingetragen ist. Nur im Serverdokument als Full-Access Administrators geführte Manager können auch solche Dokumente lesen und ändern. Nur Manager dürfen die ACL der Datenbank ändern. Designer haben Editor-Rechte auf der Datenbank. Nur Designer dürfen eine Datenbank im Domino Designer öffnen. Leser- und Autorenfelder finden Anwendung. Editoren haben Autorenrechte und dürfen neben den Dokumenten, die sie selbst erstellt haben, auch Dokumente anderer Autoren ändern. Leser- und Autorenfelder finden Anwendung, d.h. wenn z.B. beim Speichern eines Dokuments (durch einen Autor) ein Autorenfeld mit bestimmten Benutzern gefüllt wird, dürfen nur diese Benutzer und nicht grundsätzlich alle Editoren dieses Dokument ändern. Falls in einer Datenbank die Dokumente in verschiedene Klassen aufgeteilt werden können, ist es Best Practice, für diese Dokumente ein Autorenfeld mit einer Rolle (also mit einem Wert in der Art „[AutorFuerDokumentenArt1]“ zu füllen. Dann dürfen alle Benutzer, denen diese Rolle zugeordnet wurde diese Dokumente ändern, auch wenn sie nur als Autor in der ACL eingetragen sind. Auf diese Art lassen sich einerseits die Zugriffsberechtigungen auf den Autorlevel begrenzen und andernseits die Dokumente bzw. Autorenzugriffe klassifizieren. Autoren dürfen neue Dokumente erstellen, aber nur eigene Dokumente ändern oder optional löschen. Leser- und Autorenfelder finden Anwendung. Leser dürfen nur lesend auf eine Datenbank zugreifen. Leserfelder finden Anwendung. Autorenfelder spielen für diese Benutzer insofern eine Rolle, dass sie durch ein Autorenfeld zum Lesen berechtigt werden können. Benutzer des Levels Depositor dürfen nur Dokumente erstellen, aber nicht lesen. Dies ist z.B. für Datenbanken interessant, die als Wahlurne dienen. Leser- und Autorenfelder spielen für diese Benutzer keine Rolle, da sie nur neue Dokumente erstellen dürfen. Benutzer ohne Zugriff. Diesen Benutzernn kann die Berechtigung „Read / Write public documents“ zugeordnet werden. Hierdurch können Benutzer einerseits grundsätzlich vom Zugriff ausgeschlossen werden aber für einzelne ausdrücklich hierfür bestimmte öffentliche Dokumente berechtigt werden.
me) abgefragt. Im ACL-Dialog tauchen die benutzerdefinierten Rollen rechts unten im „Basiscs“-Fenster auf. Benutzerdefinierte Rollen werden in der Regel in Autoren-
358
9 Database
Tabelle 9-4 Optionale Berechtigungen in der ACL Level
NOAC- DEPO- REACESS SITOR DER
AUTH OR
EDITOR
DESIG- MANANER GER
Berechtigung Create documents Delete documents Create private agents Create personal folders/views Create shared folders/views Create LotusScript/ Java Agents Read public documents Write public documents Replicate or copy documents
x
o
x
x
x
o
o
o
o
o
o
o
x
x
o
o
o
x
x
o
x
x
o
o
x
x
x
x
o
o
o
d
o
d
d
d
x
x
x
d
d
d
d
o
d
x – Option ist fest voreingestellt aktiviert und kann nicht geändert werden. o – Option ist deaktiviert und kann optional aktiviert werden. d – Option ist aktiviert und kann optional deaktiviert werden. (kein Eintrag) – Option kann nicht aktiviert werden. oder Leserfeldern eingesetzt und immer in eckigen Klammern angegeben (z.B. „[Sachbearbeiter]“). Ist ein Domino-Objekt für eine bestimmte Rolle berechtigt, so kann ein Benutzer, dem diese Rolle zugeordnet ist, dieses Recht wahrnehmen. Die Einträge in ACL werden in Objekten der Klasse ACLEntry abgebildet, die über getFirstEntry(), getNextEntry(ACLEntry actualEntry), getNextEntry() und getEntry(String entryName) in ACL abgefragt werden können. Beachten Sie, dass die ACL nach einer Änderung über save() in ACL gespeichert werden muss, damit die Änderungen wirksam werden. Die einzelnen Eigenschaften werden in ACLEntry Einträgen innerhalb von ACL entsprechen Tabelle 9-4 abgebildet und über Methoden in ACLEntry, z.B. durch isCanCreateDocuments(), setCanCreateDocuments(boolean flag), isCanDeleteDocuments() oder setCanDeleteDocuments(boolean flag) usw. gelesen oder gesetzt. Zur Überprüfung der Rechte des aktuellen Benutzers geht Domino wie folgt vor: Zunächst wird überprüft, ob es einen Eintrag in der ACL gibt, der mit dem aktuellen Benutzernamen übereinstimmt und vom entsprechenden Typ (Benutzer, Server, etc.)
9.5 Sicherheit
359
ist. Wird ein solcher Eintrag gefunden, kommen diese Berechtigungen zum Einsatz. Einzige Einschränkung hierbei ist für Internet-Sessions (Browserzugriff, DIIOP) der maximale Internet-Zugriff, der unter „Advanced“ limitiert werden kann. Der maximale Internet-Zugriff kann über getInternetLevel() und setInternetLevel(int level) in ACL gelesen oder gesetzt werden. Der Parameter level beschreibt die grundsätzlichen Berechtigungsrollen und wird über die Konstanten LEVEL_NOACCESS, LEVEL_DEPOSITOR, LEVEL_READER, LEVEL_AUTHOR, LEVEL_EDITOR, LEVEL_DESIGNER oder LEVEL_MANAGER in ACL gesetzt. Wurde kein Eintrag für den Benutzer gefunden, so wird überprüft, ob der Benutzer sich in einer Gruppe befindet, die in der ACL gelistet ist. Wird der Benutzer in mehreren Gruppen gefunden, für die Einträge in der ACL vorliegen, wird der höchste (!) Zugriffslevel zugeordnet. Rechte, die sich aus benutzerdefinierten Rollen bzw. aus optionalen Rechten (Dokumente löschen, etc.) ergeben, werden gemischt. Allerdings muss das Recht, Agenten zu erstellen, in der Gruppe mit dem höchsten Level eingetragen sein, damit es zugeteilt wird. Wird der Benutzer auch in keiner Gruppe gefunden, so wird der Level des Eintrags „-Default-“ zurückgegeben. Das ACL-Objekt ist gut geeignet, um die ACL zu ändern. Um jedoch die Berechtigungen des aktuellen Benutzers abzufragen, ist es recht umständlich hierüber einen Eintrag für den aktuellen Benutzer auszulesen. Insbesondere bei Berechtigungen, die über Gruppen erhalten werden, ist die Auflösung der Rechte über ACL sogar unmöglich. Daher gibt es in Database einige Methoden, die dies erleichtern. Insbesondere queryAccess, queryAccessPrivileges und queryAccessRoles sind wichtige Methoden, da sie es erlauben für einen beliebigen Benutzernamen zu ermitteln, welche Rechte er auf einer Datenbank hat. Hierbei wird intern derselbe Prozess ausgeführt, den Domino anwendet, um beim Zugriff auf Objekte die Rechte des Benutzers zu ermitteln. Anders formuliert sind queryAccess, queryAccessPrivileges und queryAccessRoles die Methoden der Wahl, wenn es darum geht, die aktuellen detaillierten Rechte des Benutzers aufzulösen. In Database stehen folgende Methoden bezüglich der ACL zur Verfügung: • •
ACL getACL() throws NotesException Liefert das ACL-Objekt. Vector getACLActivityLog() throws NotesException Neu seit Domino R6.5. Liefert einen Vector mit Änderungsereignissen für die
ACL. •
•
int getCurrentAccessLevel() throws NotesException Liefert einen int-Wert entsprechend den Konstanten LEVEL_NOACCESS, LEVEL_DEPOSITOR, LEVEL_READER, LEVEL_AUTHOR, LEVEL_EDITOR, LEVEL_DESIGNER oder LEVEL_MANAGER aus ACL, der den Zugriffslevel angibt. Vector getManagers() throws NotesException
360
9 Database Liefert einen Vector von Namen als String mit ACL-Einträgen, für die Manager-Zugriff aktiviert ist. Die Einträge werden in der hierarchischen Langform (CN=Name/O=Organisation/C=Land) angegeben, sofern sie hierarchisch eingetragen wurden. Gruppennamen, die nicht hierarchisch angelegt sind (z.B. LocalDomainServers) werden in der Kurzform angegeben.
•
void grantAccess(String aclEntryString, int accessLevel) throws NotesException Setzt den Zugriffslevel des Eintrags aclEntryString auf den Level accessLevel. Hierbei wird nur der Zugriffslevel angepasst. Eventuell vergebene beson-
dere Berechtigungen (Löschen von Dokumenten, etc.) oder benutzerdefinierte Rollen bleiben erhalten, sofern sie für den neu zu setzenden Level zulässig sind. accessLevel muss einer der Konstanten entsprechen, die von getCurrentAccessLevel zurückgegeben werden können. •
int queryAccess(String aclEntryString) throws NotesException Liefert den Zugriffslevel als int-Wert (siehe getCurrentAccessLevel). Ent-
scheidend ist hierbei, dass für die Auswertung durch diese Methode auch Gruppen aufgelöst werden. queryAccess ist die Methode, um programmatisch festzustellen, welchen Zugriff der aktuelle Benutzer hat. •
int queryAccessPrivileges(String aclEntryString) throws NotesException
Neu seit Domino R6.5. Liefert die besonderen Zugriffsrechte (s. Tabelle 9-4), die dem Benutzer (oder seiner Gruppe) zugeordnet wurden. Der Wert entsteht als Addition der möglichen Werte der Konstanten aus Database DBACL_CREATE_DOCS, DBACL_DELETE_DOCS, DBACL_CREATE_PRIV_AGENTS, DBACL_CREATE_PRIV_FOLDERS_VIEWS, DBACL_CREATE_SHARED_FOLDERS_VIEWS, DBACL_CREATE_SCRIPT_AGENTS, DBACL_READ_PUBLIC_DOCS, DBACL_WRITE_PUBLIC_DOCS oder DBACL_REPLICATE_COPY_DOCS aus Database, die binär codiert sind und über bitweises Und
(&) abgefragt werden können. •
void revokeAccess(String aclEntryString) throws NotesException Entfernt den Eintrag aclEntryString aus der ACL. Dies ist nicht zu verwechseln mit grantAccess (aclEntryString, Database.LEVEL_NOACCESS).
Wurde ein Eintrag aus der ACL entfernt, gelten entweder die Default-Rechte oder die Rechte einer Gruppe, in der der Benutzer sich möglicherweise befindet. •
Vector queryAccessRoles(String s) throws NotesException Neu seit Domino R6. Liefert einen Vector der Namen der benutzerdefinierten
•
boolean isCurrentAccessPublicReader() throws NotesException Neu seit Domino R6.5. Liefert true, falls der aktuelle Benutzer der Session berechtigt ist, öffentliche Dokumente zu lesen (Eigenschaft Database. DBACL_READ_PUBLIC_DOCS). boolean isCurrentAccessPublicWriter() throws NotesException
Rollen, für die ein Benutzer berechtigt ist.
•
9.6 Designelemente und Ordner
361
Neu seit Domino R6.5. Liefert true, falls der aktuelle Benutzer der Session berechtigt ist, öffentliche Dokumente zu schreiben (Eigenschaft Database. DBACL_WRITE_PUBLIC_DOCS).
9.6
Designelemente und Ordner
Wenn Sie Java-Programme für Domino entwickeln, werden Sie in der Regel nicht in die Situation kommen, dass Sie Designelemente der Domino-Datenbank referenzieren müssen. Lediglich der View und der Agent nehmen hier eine Zwitterrolle ein, da diese Java-Objekte zwar das Designelement „View“ bzw. „Agent“ repräsentieren, aber gleichzeitig die „in Memory“-Darstellung einer Ansicht bzw. eines Agenten repräsentieren, über den Dokumente gesucht und selektiert werden und Agenten angesteuert und gestartet werden. Näheres zu Ansichten finden Sie in Kapitel 10 zu Agenten in Kapitel 4.10ff. Alle übrigen Designelemente können unterschiedlich gut über das Java-API manipuliert werden. Seit der Einführung der XML-Unterstützung in Domino können Designelemente zumindest sehr gut angezeigt, exportiert und sogar wieder importiert werden, da für jede Note, also auch für Designelemente, eine XML-Darstellung existiert. Ab Version 6 stellt Domino im Designer drei kleine Tools zur Verfügung, mit denen die XML-Darstellung einfach sichtbar gemacht werden kann. Öffnen Sie die Demo-Datenbank djbuch.nsf im Domino Designer und wählen Sie „Shared Code“ -> „Agents“. Im Menü finden Sie unter „Tools“ -> „DXL Utilities“ diese Tools. Der Viewer und der Exporter generieren den XML-Code zu dem markierten XML-Dokument und zeigen ihn im Browser an bzw. speichern ihn im Dateisystem. Der Transformer erlaubt zusätzlich die Anwendung eines XSL Stylesheets zur Transformation des XML-Codes. Die Funktionen des DXL-Exporters sind ebenfalls im Java-API vorhanden und können programmatisch eingesetzt werden. Zur Demonstration wurde der DemoDatenbank djbuch.nsf ein kleiner Agent beigefügt. Sie finden ihn, wenn Sie die Datenbank im Notes Client öffnen und im Menü „Actions“ den Befehl „Tools“ -> „Gesamtes Design als XML“ wählen. Der Agent erzeugt ein neues Dokument, das Sie in der Ansicht „Alle Dokumente“ mit dem Titel „Alle Designelemente dieser Datenbank als XML“ finden. Ein weiterer Demo-Agent erzeugt den XML-Code für ein einzelnes Dokument. Hierfür ist es wichtig, dass Sie die Datenbank lokal, nicht über einen Server öffnen. Der Agent „Actions“ -> „Tools“ -> „3. Dokument als XML“ erzeugt zu einem zuvor markierten Dokument ein Antwortdokument, in dem alle Items als XML dargestellt werden. Die zweite Möglichkeit Designelemente darzustellen, wurde in Kapitel 9.2 bereits kurz angedeutet. Die Klasse NoteCollection ermöglicht den programmatischen Zugriff auf alle Designelemente einer Datenbank. Die dritte Möglichkeit sind verschiedene Methoden in Database, die direkten Zugriff auf bestimmte Designelemente erlauben. In der Demo-Datenbank befindet sich ein Agent, der über „Actions“ -> „Tools“ -> „1. Masken anzeigen“ erreichbar ist und von der Methode getForms() Gebrauch macht. Er erzeugt ein neues Doku-
362
9 Database
ment in der Ansicht „Alle Dokumente“ und gibt einen Überblick über die Masken und deren Felder der aktuellen Datenbank, indem er die verschiedenen Properties der über getForms bezogenen Form-Objekte ausliest. Direkt über das Objekt Database können Agenten, Masken, Ansichten, Outlines und Ordner manipuliert werden. Beim programmatischen Erstellen von Designelementen über die folgenden Methoden benötigt der Benutzer mindestens Designerrechte für die Datenbank.
9.6.1
Designspezifische Methoden in Database
Das Database-Objekt stellt eine große Auswahl an Methoden zur Verfügung, über die die Gestaltung (im Sinne der Programmierung) der Domino-Datenbank über das API manipuliert werden kann, ohne den Domino Designer verwenden zu müssen. Diese Methoden sind grundsätzlich mit Vorsicht zu genießen, denn ihre Mächtigkeit bedingt auch ihr Risiko. •
Agent getAgent(String agentName) throws NotesException Liefert ein Agent-Objekt des Agenten mit dem Namen agentName. Dieses Ob-
jekt repräsentiert sowohl das Designelement des Agenten als auch den programmatischen Agenten. Typische Methoden des Designelements sind getName(), getTrigger (), getTarget(), die verschiedenen Methoden des Designlockings (s. Kap. 7.7) und natürlich save() oder remove(). Insbesondere bei der letzten Methode wird am ehesten deutlich, dass man es mit dem Designelemente zu tun hat, da diese Methode den Agenten endgültig aus der Datenbank löscht. Gleichzeitig kann das Agent-Objekt zur programmatischen Steuerung eines Agenten genutzt werden. Die wichtigsten Methoden run und runOnServer wurden bereits in Kapitel 3.4 besprochen. •
Vector getAgents() throws NotesException Liefert einen Vector von Agent-Objekten aller in der Datenbank befindlichen
•
Form getForm(String formName) throws NotesException Liefert das Form-Objekt einer durch formName spezifizierten Maske. Neben dem Designlocking können über dieses Objekt die Felder der Maske über getFields als Vector aus Strings bezogen werden. Die Sicherheitsvorgaben, über die be-
Agenten.
stimmt wird, ob die Maske nur bestimmten Benutzern erlauben soll, mit Hilfe dieser Maske Dokumente zu erstellen oder zu lesen, können über Vector getFormUsers(), void setFormUsers(Vector vector), Vector getReaders(), void setReaders(Vector vector), boolean isProtectReaders(), void setProtectReaders(boolean flag), boolean isProtectUsers() oder void setProtectUsers(boolean flag) beeinflusst werden. • •
Vector getForms() throws NotesException Liefert einen Vector von Form-Objekten aller Masken einer Datenbank. View getView(String viewName) throws NotesException
9.6.1 Designspezifische Methoden in Database
363
Lädt ein View-Objekt der durch viewName spezifizierten Ansicht. Der Name ist nicht abhängig von Groß-Kleinschreibung. Es kann entweder der Klarname oder der Alias verwendet werden. Es wird empfohlen bei einer programmatischen Verwendung wie durch getView immer Aliasnamen zu verwenden, um von späteren Änderungen von im GUI Verwendung findendenden Namen unabhängig zu sein. •
Vector getViews() throws NotesException Liefert einen Vector von View-Objekten aller in der Datenbank befindlichen
Ansichten. Allerdings werden auch für diese Methoden die Berechtigungen angewendet. In den Sicherheitseinstellungen für eine Ansicht im Domino Designer findet sich die Einstellung „Who may use this view“. Ist ein Benutzer hier nicht aufgelistet, wird dieser View auch nicht von getViews angezeigt. Sie können die Liste der erlaubten Benutzer auch über Vector getReaders() in View abfragen. •
Neu seit Domino R6.5. Erstellt eine neue Ansicht in der Datenbank und gibt ein entsprechendes View-Objekt zurück. Diese Methode ist sehr mächtig und daher mit besonderer Vorsicht zu behandeln. Durch die Erstellung einer Ansicht werden auch entsprechende Indizes für sortierte Spalten in der Datenbank angelegt. Hierdurch entstehender Speicherbedarf und Performanceaufwand für die Aktualisierung sind zu berücksichtigen. Wenn in der Datenbank ein View existiert, der die Eigenschaft „Default design for new folders and views“ (Abb. 9-3) hat, so wird der neue View von diesem kopiert und es werden nur die in der Methode createView übergebenen Parameter neu gesetzt. Ist dies nicht der Fall, wird wie folgt vorgegangen. Wird kein Parameter verwendet, wird die Ansicht mit einer einzigen Spalte mit der Spaltenberechnung @DocNumber und der Select Formel @all und dem Titel „(untitled)“ angelegt. Werden nur einzelne Parameter weggelassen, so gelten Defaulteinstellungen. Für den viewName ist dies „(untitled)“, für die selectFormula ist dies „SELECT @all“. Wird kein templateView angegeben, so wird der View mit einer Spalte mit der Spaltenformel „@DocNumber“ angelegt. Der Parameter prohibitDesignRefresh setzt das Flag „Prohibit design refresh or replace to modify“, das auch in den Ansichteneigenschaften im Domino Designer gesetzt werden kann. Wird dieser Parameter nicht übergeben, so wird das Flag gesetzt.
364
9 Database Beachten Sie, dass folglich über createView erzeugte Ansichten
vor Design Updates geschützt sind, wenn nicht ausdrücklich der Parameter prohibitDesignRefresh == false gesetzt wird. Insgesamt entspricht createView dem Verhalten beim Erstellen einer neuen Ansicht über das GUI des Domino Designers. •
•
Outline getOutline(String outlineName) throws NotesException Abb. 9-3 Eigenschaft "Default design for Liefert ein Outline-Objekt der new folders and views" Outline mit Namen outlineName. Outline createOutline(String outlineName) throws NotesException Outline createOutline(String outlineName, boolean useDefaultDesign) throws NotesException Erstellt eine Outline mit Namen outlineName. Ist useDefaultDesign == true, wird optional aufgrund der vorhandenen Ansichten und Ordner ein De-
fault Design zusammengestellt, andernfalls wird eine leere Outline erstellt. Eine Outline besteht aus hierarchisch gestaffelt angeordneten OutlineEntryObjekten, die einen Navigationsbaum darstellen. Über das zurückgegebene Outline-Objekt können in der Outline Einträge (der Klasse OutlineEntry) hinzugefügt und entfernt werden. Dies kann dazu verwendet werden, dynamische Navigationselemente zu erzeugen. Für die Verwendung von Ordnern stehen neben den Methoden in View (s. Kap. 10 und 3.3) noch drei Methoden in Database zur Verfügung: •
void enableFolder(String folderName) throws NotesException Während bei der Verwendung von putInFolder ("folderName", true) in Document ein Ordner neu erstellt wird, wenn er noch nicht existiert, kann über enableFolder direkt ein neuer Ordner mit Namen folderName erstellt wer-
den. Die Gestaltung des Ordners (Spalten) wird von der als Defaultview gekennzeichneten Ansicht kopiert. •
Wenn Dokumente in Ordner kopiert werden, genauer gesagt, wenn Referenzen auf Dokument in Ordnern hinterlegt werden, so wird im Normalfall nicht im Dokument hinterlegt, in welchen Ordnern sich dieses Dokument befindet. Damit diese umgekehrte Referenzierung möglich wird, muss für die Datenbank die entsprechende Funktionalität über setFolderReferencesEnabled (true) aktiviert werden. Beachten Sie, dass dies nagtiven Einfluss auf die Geschwindigkeit der Datenbank haben kann.
9.7
Locking und Signatur
Die Methoden isDocumentLockingEnabled, setDocumentLockingEnabled, isDesignLockingEnabled, setDesignLockingEnabled und sign wurden in den Kapiteln 7.7 und 7.5 ausführlich erläutert.
9.8
Service Tasks
Wie alle Datenbanken benötigen auch Domino-Datenbanken verschiedene Wartungstools, insbesondere eine Komprimierung und Wartung zur Wiederherstellung der Dateiintegrität nach eventuellen Abstürzen. Insbesondere seit Domino R6.5 können diese Wartungstasks nicht nur über den Domino Administrator, sondern auch programmatisch angestoßen werden. •
int compact() throws NotesException
Beim Löschen von Daten innerhalb einer Domino-Datenbank wird Domino versuchen, sofern nicht die Option Database.DBOPT_NOOVERWRITE gesetzt ist, den so frei werdenden internen Speicher wiederzuverwenden. Dies wird nicht immer möglich sein, wenn z.B. neue größere Inhalte nicht in kleinere freigewordene Segmente passen, so dass es zur Fragmentierung des Speicherplatzes kommen wird. Durch die Komprimierung einer Datenbank kann die Fragmentierung rückgängig gemacht und der interne Speicheraufbau reorganisiert werden. Dieser Vorgang kann auch programmatisch auf eine Datenbank angewendet werden, allerdings ist die Ausführung von compact auf lokale Datenbanken beschränkt. Entgegen älteren Hilfe-Einträgen in der Domino-Designerhilfe kann auch die aktuelle Datenbank, in der der programmatische Aufruf von compact erfolgt, komprimiert werden (Stand Domino R6.5.3). Domino kennt drei Arten des Compact, den datenbankinternen Compact ohne Dateiverkleinerung, mit Dateiverkleinerung und den „Copy-style“ Compact, bei dem eine neue und komprimierte Datenbank aus der vorliegenden als Kopie erstellt wird. Bei der Verwendung des Compact ohne Parameter durch compact() wird wie folgt vorgegangen:
366
9 Database Datenbanken mit Transaktionslogging werden durch Compact ohne Dateiverkleinerung komprimiert. Hierbei wird interner Speicher reorganisiert und freier Speicher freigegeben. Die Größe der Datenbank im Dateisystem ändert sich nicht. Datenbanken ohne Transaktionslogging werden duch Compact mit Dateiverkleinerung komprimiert. Hierbei wird zusätzlich die Datenbankgröße im Dateisystem verkleinert. Bei Datenbanken, für die strukturelle Änderungen anstehen (wg. Veränderung der Parameter Database.DBOPT_NOUNREAD, Database.DBOPT_OPTIMIZATION oder Database.DBOPT_NORESPONSEINFO), wird die Copy-style-Komprimierung verwendet. Rückgabewert der Methode ist die Anzahl an Bytes, um die die Dateigröße verkleinert wurde.
•
int compactWithOptions(String optionString) throws NotesException int compactWithOptions(int optionInt) throws NotesException int compactWithOptions(int optionInt, String spaceThreshhold) throws NotesException
Neu seit Domino R6.5. Ermöglicht einen Compact mit den auch für die Kommandozeile verfügbaren Optionen. Diese können entweder als String optionString oder als int-Konstante optionInt übergeben werden. Hierfür stehen in Database verschiedene Konstanten zur Verfügung. Es können nur lokale Datenbanken komprimiert werden und nicht die aktuelle Datenbank, in der der Code ausgeführt wird. •
Neu seit Domino R6.5. Korrupte Datenbanken können über fixup repariert werden. Über optionInt können Optionen anhand von Konstanten aus Database übergeben werden. Es können nur lokale Datenbanken repariert werden und nicht die aktuelle Datenbank, in der der Code ausgeführt wird. •
DateTime getLastFixup() throws NotesException
Neu seit Domino R6.5. Liefert Datum und Uhrzeit des letzten Fixup. Bei bestimmten Compact-Aufrufen wird ein impliziter Fixup durchgeführt, der dieses Datum ebenfalls beeinflusst.
9.9
Zusammenfassung
Das Databaseobjekt kapselt nicht nur die Domino-Datenbank in ein API-Objekt, sondern stellt alle Services rund um die Datenbank zur Verfügung bzw. ermöglicht deren Bezug. Hierzu gehörenen die Volltext- und Domainsearch, die Replikation, die Zugriffskontrolliste ACL und natürlich verschiedene Tasks für Wartung, Erstellung und Templating.
10
10 View
In diesem Kapitel: View API Indizierung und Performance Programmatische Manipulation von Ansichten View, ViewEntry, ViewEntryCollection ViewNavigator DocumentCollection Kapselung von Schleifen
368
10 View
Ansichten nehmen für Notes und Domino eine zentrale Rolle ein. Während in den folgenden Kapiteln die technische Seite beschrieben wird und die verschiedenen mit dem View im Zusammenhang stehenden Klassen und Methoden eingehend erläutert werden, finden Sie in Kapitel 15 zusätzliche Informationen zur Bedeutung von Ansichten für die Suche in Domino. Wichtig für das Verständnis ist, dass, wie bereits zuvor erwähnt, das Java-Objekt View eine Zwitterrolle einnimmt und einerseits das Designelement beschreibt, so dass es möglich ist mit entsprechenden Methoden in View das Design zu manipulieren, also z.B. neue Ansichtenspalten hinzuzufügen oder die SELECT-Formel zu manipulieren. Die zweite Rolle, die das View-Objekt einnimmt, ist die der Ansicht als Collection von Dokumenten, die gelistet, durchsucht oder durchschritten werden kann, um Dokumente zu finden und anzuzeigen. Auch wenn das API Ihnen die Funktionen zur programmatischen Manipulation des Designs einer Ansicht zur Verfügung stellt, sind diese mit Vorsicht zu verwenden. Rufen Sie sich nochmals den Aufbau einer Ansicht ins Gedächtnis (vgl. Kapitel 3.3ff.). Eine Ansicht besteht im Wesentlichen aus einer oder mehreren Spalten mit entsprechenden Spaltenberechnungen und -eigenschaften, wie Sortierung oder Kategorisierung, und einer SELECT-Formel, die festlegt, welche Dokumente in einer Ansicht vorliegen. Wenn nun dynamisch über das API Spalten oder die SELECT-Formel verändert werden, so müssen entsprechend die zugrunde liegenden Indizes, die z.B. für sortierte Spalten angelegt werden, neu aufgebaut werden. In einigen Fällen mag dies erwünscht sein, aber in Ansichten, in denen viele Dokumente angezeigt werden, kann dies einen erheblichen Performanceeinfluss haben. Da Ansichten zwischen den Benutzern geteilt werden – sie werden (in der Regel) als so genannte gemeinsame Ansichten angelegt – ist eine derartige dynamische Änderung natürlich auch nicht thread-save, da sich die dynamischen Änderungen mehrerer Benutzer gegenseitig in die Quere kommen würden. Darüber hinaus müssen diese Änderungen mit Designerberechtigung durchgeführt werden, die nicht jedem Benutzer gewährt wird. Ansichten sind daher (empfohlenerweise) nicht dynamisch zu verändern: Sie werden im Domino Designer oder eventuell auch über ein Installationsskript über das API, aber in jedem Fall nur einmalig angelegt und mit den benötigten Spalten, Eigenschaften und SELECT-Formel versehen. Spätere Änderungen sollten nur im Zuge der Versionspflege oder des Refactorings vorkommen. Berücksichtigen Sie zusätzlich, ob eine Ansicht im Notes Client, also im GUI, zum Einsatz kommen soll, oder ob die Ansicht internen Zwecke zur Suche und Selektion dienen soll. Wird eine Ansicht für beide Zwecke benötigt, sollte die Ansicht doppelt angelegt werden, damit spätere optische Gestaltungsänderungen für das GUI keine Nebenwirkungen auf die Programmierung haben. Suchen und Selektion von Untergruppen solcher Dokumente, die in einer Ansicht angezeigt werden, können dann anhand der hierfür vorgesehenen Methoden, wie FTSearch und getAllDocumentsByKey in View durchgeführt werden.
10.1 Updates, Indizes und Performance
369
Best Practice Berücksichtigen Sie, ob eine Ansicht im Notes Client, also im GUI, zum Einsatz kommen soll, oder ob die Ansicht internen Zwecke zur Suche und Selektion dienen soll. Wird eine Ansicht für beide Zwecke benötigt, sollte die Ansicht doppelt angelegt werden, damit spätere optische Gestaltungsänderungen für das GUI keine Nebenwirkungen auf die Programmierung haben.
10.1
Updates, Indizes und Performance
Für jede sortierte Spalte legt Domino automatisch einen Ansichtenindex an. Dies ist praktisch, transparent und einfach zu handhaben, da kein administrativer Aufwand anfällt. Allerdings wird dies zum einen durch den hierfür benötigten Speicherbedarf64 und natürlich durch die für die Updates der Indizes benötigte Performance erkauft. Domino unterscheidet zwischen den Volltextindizes und normalen Ansichtenindizes. Volltextindizes werden nur erstellt, wenn die entsprechende Datenbankeigenschaft über createFTIndex in Database aktiviert wurde. Sie kann über updateFTIndex abgeschaltet werden. Ansichtenindizes werden automatisch erstellt und können nicht abgeschaltet werden, es sei denn, eine Ansicht verzichtet komplett auf sortierte Spalten. Domino kennt verschiedene Optionen, wie die Volltextindizes aktualisiert werden sollen. Grundsätzlich wird zwischen periodischer und sofortiger Aktualisierung unterschieden. Dies kann durch setFTIndexFrequency in Database eingestellt werden. Beide Ansätze haben ihre Vorteile. In Datenbanken, die sich nicht schnell ändern, sollten Sie in jedem Fall die periodische Aktualisierung wählen. Die sofortige Aktualisierung wird benötigt, wenn Änderungen immer sofort zur Verfügung stehen müssen, was aber seltener der Fall ist, als man annehmen sollte. Da Domino vor allem im Zusammenhang mit unstrukturierten Daten zum Einsatz kommt, wie Sie in Collaboration- oder Datamining-Anwendungen entstehen, werden in den seltensten Fällen diese Daten in der gleichen Sekunde im Index für die Suche benötigt. Selbstverständlich ist die sofortige Aktualisierung ein Muss, wenn Sie über Ansichten Daten nachschlagen, für die eine referenzielle Integrität benötigt wird. Letztendlich muss zwischen vorhandener Leistungsfähigkeit und der für die Aktualisierung der Indizes benötigten Rechenleistung und der benötigten Aktualität abgewogen werden. Auch für die Ansichtenindizes kann eine sofortige Aktualisierung über setAutoUpdate(true) in View oder über den Domino Designer über das Kontextmenu der Ansichteigenschaften aktiviert werden. 64
Die Ansichtenindizes werden innerhalb der NSF-Datei angelegt und können über die Option Database.CMPC_DISCARD_VIEW_INDICES in compactWithOptions oder die -D Option im Kommandokonsolenbefehl compact gelöscht werden, so dass sie bei der nächsten Benutzung automatisch von Grund auf neu erstellt werden können. Volltextindizes werden, sofern für die Datenbank Volltextindizierung aktiviert ist, im Domino-Dataverzeichnis in Ordnern abgespeichert, die den Namen der Datenbank und die Endung .ft tragen.
370
10 View
Wenn ein Programm einen View verwendet, um in einem Loop durch die enthaltenen Dokumente zu schreiten und für diese Dokumente Veränderungen vorzunehmen, kann es sinnvoll sein, vorübergehend die sofortige Aktualisierung des Index zu aktivieren. Wird nämlich durch die Veränderung des Dokuments dieses von der Ansicht ausgeschlossen, so wird diese Veränderung nicht sofort sichtbar und das Dokument bliebe in der Ansicht. Insbesondere wenn z.B. Stempel im Dokument gesetzt werden, die kennzeichnen, dass eine Verarbeitung erfolgt ist, werden Sie dringend erwarten, dass das Dokument im zweiten Durchlauf nicht mehr in der Ansicht enthalten ist. In Kapitel 10.8 werden Codebeispiele vorgestellt, wie am besten Loops über Dokumente aus Ansichten oder Teilmengen der selektierten Dokumente gefunden und durchschritten werden.
10.2
Programmatische Manipulation des Designs von Views
Ähnlich wie für Database gilt auch für View, dass es in der Regel Sinn macht, die Gestaltung, also gewissermaßen die Programmierung einer Ansicht im Domino Designer zu erledigen. Wenn es jedoch notwendig ist, z.B. in einem Installationsscript, womöglich abhängig von Benutzerangaben oder sonst dynamisiert, Ansichten anzulegen, ist es hilfreich, die API-Funktionen nutzen zu können. Für Domino R6.5 wurden die API-Funktionen für die Manipulation von Ansichtenspalten und deren Eigenschaften erheblich ausgebaut, so dass inzwischen nahezu alle Eigenschaften einer Ansicht oder einer Ansichtenspalte auch durch das Java-API erreicht, angelegt, gelöscht oder sonst verändert werden können. Ein ausdrückliches Speichern, wie dies nach einer Veränderung im Domino Designer notwendig wäre, muss nach Veränderungen an einer Ansicht, die das Design betreffen, nicht erfolgen. Wenn also z.B. über createColumn einer Ansicht neue Spalten hinzugefügt werden, stehen diese sofort in der Ansicht zur Verfügung. Folglich ist also zu berücksichtigen, dass die Manipulation einer Ansicht direkt Auswirkungen auf andere Programmbestandteile und die Programmierung haben kann. Zur Manipulation des Gestaltungselements „View“ über API-Funktionen stehen in View folgende Methoden zur Verfügung: •
ViewColumn getColumn(int columnNumber) throws NotesException Liefert ein ViewColumn-Objekt der Spalte mit der Ordnungsnummer columnNumber. Eine Spalte einer Ansicht ist in keinem Fall mit einem Result Set zu ver-
wechseln. Sie enthält keine Daten, sondern die Definition von Eigenschaften für die Darstellung im Notes Client, bzw. Spaltenberechnungen, die festlegen, welche Daten bei der Benutzung der Ansicht später anzuzeigen sind. Hier tritt die eingangs erwähnte Zwitterrolle deutlich zu Tage.
10.2 Programmatische Manipulation des Designs von Views
371
Beachten Sie Beim Verwenden der setter für Spalteneigenschaften sollte unbedingt vermieden werden, null-Parameter oder leere Strings für Stringparameter zu übergeben. Einerseits ist dies in-
haltlich ohnehin nicht sinnvoll und vermeidet unerwartete Ergebnisse. z.B. sollte setFormula (null) vermieden werden.
Die Spalten sind von 1 aufsteigend nummeriert. Die ViewColumn bietet insbesondere ab Domino R6.5 für fast alle Eigenschaften einer Spalte setter und getter. Beachten Sie, dass die Veränderung dieser Eigenschaften der ViewColumn sofort wirksam werden und direkt in der Ansicht gespeichert werden. Ein kleiner Wermutstropfen ist, dass in Domino 6.5.x vereinzelt Abstürze bei Verwendung derjenigen setter-Methoden festgestellt wurden, die neu seit Domino 6.5 sind. Eine Ursache konnte hier leider nicht nachvollzogen werden. Dies wurde nur bei der Benutzung einer lokalen Notes-Session auf einem Client festgestellt. In Verbindung mit DIIOP traten diese Probleme nicht auf. Im Folgenden eine kleine Auswahl der Methoden in ViewColumn: –
Setzen und Lesen der Spaltenformel, die zur Berechnung der in dieser Spalte angezeigten Werte dient. isFormula liefert true, falls es sich bei der Spaltenberechnung um eine Formel handelt. isField liefert true, falls es sich bei der Spaltenberechnung lediglich um den Namen eines Items handelt. Falls die Formel mit einer so genannten einfachen Spaltenberechnung identisch ist (z.B. @Author), dann liefern sowohl isField false und isFormula true, im Domino Designer wird aber die @Formel als einfache Formel angezeigt. setFormula darf weder null noch einen leeren String übergeben. Dies ist sehr wichtig, nicht nur weil es inhaltlich keinen Sinn machen würde, sondern da vereinzelt in diesem Zusammenhang Serverabstürze (Domino 6.5.3) durch die Übergabe von null-Parametern provoziert werden konnten. Ungültige @Fomeln werden mit einer NotesException quittiert. Alternativ können über den Domino Designer statt einer Berechnung auch einfache Feldnamen für einen Spalteninhalt festgelegt werden. Die Änderung einer Spaltenberechnung ist auch z.B. für eine programmatische Suche über getAllDocumentsByKey relevant. Bedenken Sie, dass eine Veränderung einer Spaltenberechnung in der Regel eine Neuberechnung des Ansichtenindexes zur Folge hat. –
Neu seit Domino R6.5. Die Sortierung einer Spalte lässt sich ebenfalls programmatisch über das ViewColumn-Objekt verändern. Ob eine Spalte kategorisiert ist, lässt sich allerdings nur über boolean isCategory() auslesen. Diese Eigenschaft kann nicht geändert werden (s. Kap. 3.3.2). –
void setFontStyle(int styleConstant) throws NotesException int getFontStyle() throws NotesException
Neu seit Domino R6.5. Über die Konstanten ViewColumn.FONT_PLAIN (seit R6.5), ViewColumn.FONT_BOLD, ViewColumn.FONT_ITALIC, ViewColumn.FONT_UNDERLINE, ViewColumn.FONT_STRIKEOUT, ViewColumn.FONT_STRIKETHROUGH (seit R6.5) kann die Darstellung der Schrift in einer Ansichtenspalte geändert werden. Diese Änderung ist relevant für die Darstellung im Notes Client oder bei Ansichten, die von Domino für den Browser gerendert wurden. –
int getFontColor() throws NotesException void setFontColor(int colorConstant) throws NotesException
Neu seit Domino R6.5. Die Schriftfarbe für eine Ansichtenspalte kann über die in Kapitel 8 im Zusammenhang mit dem ColorObject beschriebenen und in RichTextStyle definierten Konstanten für Farben gesetzt werden. –
String getFontFace() throws NotesException void setFontFace(String fontName) throws NotesException Neu seit Domino R6.5. Schriften werden in der ViewColumn über deren Na-
men angesprochen. Gültige Werte sind z.B. „Arial“, „Courier“, „Default Sans Serif“, „Helvetica“ oder „Times New Roman“. Die Darstellung der Schrift ist relevant für die Anzeige einer Ansicht (und deren Ansichtenspalten) im Notes Client –
void setFontPointSize(int fontPointSize) throws NotesException int getFontPointSize() throws NotesException
Neu seit Domino R6.5. Für die Anzeige von Spaltenschriften im NotesClient kann die Schriftgröße für eine Spalte verändert werden. –
Neu seit Version R6.5. Eine Spalte kann seit Version 6 versteckt werden. –
boolean isResponse() throws NotesException
10.2 Programmatische Manipulation des Designs von Views
373
Die Methode isResponse liefert true, wenn die Eigenschaft „Show responses only“ in der Spalte gesetzt ist. Diese Eigenschaft ist etwas ungewohnt in der Verarbeitung und steht im Zusammenhang mit Antwortdokumenten (s. Kap. 7.3). Wird diese Eigenschaft gesetzt, so werden im NotesClient, aber auch bei der Auswertung durch API-Java-Methoden nur für Anwortdokumente Werte in dieser Spalte angezeigt. Alternativ könnte dies durch eine Spaltenberechnungsformel in der Art @if (@isResponseDoc;"<<show field of Response>>";""); ersetzt werden. •
Vector getColumns() throws NotesException Liefert einen Vector aus ViewColumn-Objekten mit allen Spalten einer An-
sicht. Beachten Sie, dass hier nicht etwa ein Result Set zurückgegeben wird, sondern die oben beschriebenen ViewColumn-Objekte, die die Gestaltung, also im Wesentlichen Eigenschaften für die Darstellung oder Spaltenberechnungen betreffen. •
Neu seit Domino R6.5. Erstellt eine neue Spalte für die Ansicht. Der Parameter columnFormula bestimmt die festzulegende Spaltenberechnungsformel. Wird er weggelassen, so wird die Formel @DocNumber verwendet. Der Parameter titel bestimmt den anzuzeigenden Titel der Spalten, wie er im Notes Client erscheint. Wird er weggelassen, wirft Domino (R6.5.3) eine NullPointerException. Der Parameter position gibt die Position der neu zu erstellenden Spalte an, wobei die erste Spalte die Nummer 1 erhält. Wird er weggelassen, so wird eine neue Spalte ganz rechts an den View angefügt. Es wird empfohlen, immer die ausführliche Signatur mit den Parametern position, title und columnFormula zu verwenden. Vermeiden Sie null-Werte und leere Strings für title und columnFormula. •
10 View Neu seit Domino R6.5. Kopiert eine Spalte einer Ansicht. Werden die Signaturen mit den Parametern numOfCopyColumn oder titleOfCopyColumn verwendet, so können nur Spalten derselben Ansicht kopiert werden. Die Parameter geben die Nummer (beginnend mit 1) oder den Titel der zu kopierenden Spalte an. Unter Titel ist der Anzeigetitel der Spalte zu verstehen. Alternativ kann ein ViewColumn-Objekt als Parameter übergeben werden, so dass diese Ansichtenspalte in die aufrufende Ansicht kopiert wird. Optional kann die Position der neuen Spalte mit newPosition angegeben werden. Vermeiden Sie leere Strings und null-Werte für titleOfCopyColumn.
•
void removeColumn() throws NotesException
Neu seit Domino R6.5. Löscht die am weitesten rechts liegende Spalte (höchste Ordnungsnummer) einer Ansicht. •
void removeColumn(int position) throws NotesException Neu seit Domino R6.5. Löscht die Spalte an der Position position (beginnend
ab 1 nummeriert). Gibt es die Spalte an der genannten Position nicht, wird eine •
Neu seit Domino R6.5. Löscht die Spalte mit dem Titel titleOfColumn. Existiert keine Spalte mit dem angegebenen Titel, wird keine Aktion durchgeführt. Vermeiden Sie null-Werte und leere Strings für titleOfColumn. •
int getColumnCount() throws NotesException
Liefert die Anzahl der Spalten eines Views. • •
Vector getColumnNames() throws NotesException Liefert die Titel der Spalten einer Ansicht als Vector aus Strings. int getHeaderLines() throws NotesException
Die Titel einer Ansicht werden im Notes Client standardmäßig einzeilig dargestellt, können aber auch mehrere Zeilen in Anspruch nehmen. getHeaderLines zeigt diese Anzahl der Zeilen an. •
int getRowLines() throws NotesException
Liefert die Anzahl der Ausgabezeilen, die pro Datensatzzeile einer Ansicht angezeigt werden. •
int getSpacing() throws NotesException void setSpacing(int spacing) throws NotesException
Das Spacing bestimmt für die Darstellung im NotesClient den Zeilenabstand zwischen einzelnen Datensatzzeilen einer Ansicht. Der Parameter spacing kann Werte in Abstufungungen von 1/4 Zeilenabstand von ein- bis zweizeilig annehmen, repräsentiert durch die Konstanten SPACING_SINGLE, SPA-
10.2 Programmatische Manipulation des Designs von Views
375
Abb. 10-1 Kategorisierte Ansicht
Bestimmt die Hintergrundfarbe der gesamten Ansicht für die Darstellung im Notes Client. Der Parameter colorConstant kann die in Kapitel 8 im Zusammenhang mit dem ColorObject beschriebenen und in RichTextStyle definierten Konstanten für Farben annehmen. •
boolean isCategorized() throws NotesException
Zeigt an, ob eine Ansicht eine kategorisierte Spalte enthält. Abb. 10-1 zeigt ein Beispiel einer kategorisierten Ansicht. Im Beispiel ist die erste Spalte kategorisiert (s. Kap. 3.3.2 und 10.5). Nur sortierte Spalten können kategorisiert werden. Hierbei fasst Domino mehrere identische Einträge einer Spalte als Gruppe zusammen und zeigt (optional) ein so genanntes Twistie (dreieckiges Symbol) an, mit dem die Gruppe auf- oder zugeklappt werden kann. Zwar handelt es sich hier lediglich um ein Gestaltungsmerkmal zur Anzeige, das zunächst nur im Notes Client zum Tragen kommt, aber auch bei Such-Operationen durch die API-Funktionen müssen Kategorisierungen berücksichtigt werden. So können z.B. innerhalb einer Spalte Subkategorien gestaffelt angezeigt werden, indem diese Spalte Werte enthält, die einen Backslash „\“ enthalten. Domino fasst dann Untergruppen ebenfalls zu einer Gruppe zusammen und zeigt ein Twistie an und zwar werden alle Einträge, für die der Teil rechts vom Backslash identisch ist, als jeweilige Untergruppe angezeigt und alle Untergruppen, für die der Teil links vom Backslash gleich ist, werden als Gruppe angezeigt.
376
10 View Wenn also eine Ansicht Dokumente anzeigt, die in der kategorisierten Spalte Werte der Art „Organisation\Nord“ und „Organisation\Süd“ enthalten, so wird Domino die Kategorie „Organisation“ anzeigen und darunter die Kategorien „Nord“ und „Süd“ gruppieren. Optisch wird dies zusätzlich durch Einrückung kenntlich gemacht. Wenn eine derartig kategorisierte Spalte durchsucht wird, ist zu bedenken, dass bei exakter Suche nach Werten der Art „Organisation\Nord“ einschließlich Backslash gesucht werden muss. Eine nicht exakte Suche nach Werten, die mit „Nord“ beginnen, ist nicht möglich, da ja die Werte in der Spalte tatsächlich nicht mit Nord, sonder mit „Organisation“ beginnen.
•
boolean isHierarchical() throws NotesException
Eine hierarchische Darstellung einer Ansicht erfolgt für Haupt- und deren Antwortdokumente. Enthält eine Ansicht Haupt- und Antwortdokumente und liefert isHierarchical == true, so werden Haupt- und Antwortdokumente ähnlich wie in kategorisierten Ansichten angezeigt, wobei die Antwortdokumente den Hauptdokumenten untergeordnet werden. •
boolean isCalendar() throws NotesException
Da der Lotus Notes Client ein Collaboration Tool ist, das selbstverständlich neben dem Mail-Client einen Kalender enthält, benötigt Domino ein Gestaltungselement, mit dem Kalender dargestellt werden können. Hierfür besitzen Ansichten eine spezielle Eigenschaft, durch die sie als Kalender anzeigt werden. Dokumente werden dann nicht linear untereinander, sonder dem jeweiligen Tag zugeordnet angezeigt. Damit Dokumente korrekt in einer Kalenderansicht angezeigt werden, muss die erste Spalte der Ansicht unsichtbar sein, zu einem DateTimeWert evaluieren und aufsteigend sortiert sein. Dieser Wert ordnet einen Ansichteneintrag dem Kalendertag zu. Die zweite Spalte muss ebenfalls aufsteigend sortiert sein und auf einen Zeit-Wert evaluieren und spezifiziert die Dauer eines Ereignisses. Auch diese Spalte sollte versteckt werden. •
Für eine Datenbank kann ein Default View definiert werden. Dieser wird, sofern z.B. in einem Frameset nichts anderes definiert ist, als erstes beim Öffnen angezeigt. •
boolean isFolder() throws NotesException
Ordner und Ansichten werden in ihrer Repräsentation als Java-Objekt identisch behandelt. Allerdings haben Ordner z.B. keine SELECT-Formel. isFolder liefert true, wenn es sich um einen Ordner handelt. Näheres zu Ordnern finden Sie in Kapitel 3.3ff. •
boolean isPrivate() throws NotesException
Benutzer, die keine Designer-Rechte in der ACL haben, können dennoch so genannte private Ansichten erstellen, sofern ihnen dies in der ACL ausdrücklich erlaubt wurde (queryAccessPrivileges in Database liefert Database. DBACL_CREATE_PRIV_FOLDERS_VIEWS) •
Wie jedes Designelement kann eine Ansicht einen Alias haben. getAliases liefert diese Werte als Vector aus Strings, setAliases setzt ihn, wobei vorhandene Werte überschrieben werden. Es werden nur die Aliasnamen ausgegeben, nicht der Klarname. •
Wenn eine Ansicht auf Basis einer Ansichtenvorlage angelegt wurde, z.B. über
•
createView(String viewName, String selectFormula, View templateView), dann hinterlegt Domino im Designelement der Ansicht ein Item $ViewInheritedFrom, in dem der Name der Ansicht geführt wird, von der die Ansicht abgeleitet wurde. getViewInheritedName liefert diesen Wert. boolean isProhibitDesignRefresh() throws NotesException void setProhibitDesignRefresh(boolean setProhibit) throws NotesException
Wie in Kapitel 9 erläutert, können Datenbanken auf Datenbankschablonen basieren. Die Gestaltung einer solchen Schablone kann regelmäßig automatisiert oder manuell neu geladen werden. Daher hat jedes Gestaltungselement ein Flag, über das bestimmt werden kann, dass bei einem solchen Gestaltungsupdate dieses Gestaltungselement nicht verändert werden soll. isProhibitDesignRefresh und setProhibitDesignRefresh steuern dieses Flag.
10.3
Allgemeine Properties und Methoden
Das View-Objekt stellt in Java verschiedene getter und setter zur Verfügung, über die allgemeine Parameter gelesen und geschrieben werden können. Hierzu gehören natürlich zunächst verschiedene Zeitstempel, die sich auf das Design-Element „Ansicht“ beziehen und beschreiben, wann die Gestaltung einer Ansicht verändert oder die Ansicht zum ersten Mal erstellt wurde. Interessant ist die seit Version 6.5 in Java zur Verfügung stehende Möglichkeit die SELECT-Formel programmatisch zu manipulieren. Auch hier die Warnung vor unerwarteten Nebenwirkungen. Schließlich ist die SELECT-Formel das „Herz“ der Ansicht. • •
DateTime getCreated() throws NotesException Liefert ein DateTime-Objekt, das anzeigt, wann eine Ansicht erstellt wurde. DateTime getLastModified() throws NotesException Liefert ein DateTime-Objekt, das die letzte Designänderung einer Ansicht an-
zeigt. Beachten Sie, dass hier nur Änderungen an dem Objekt selbst gemeint sind und nicht etwa Änderungen, die dadurch zustande kommen, dass aufgrund von Veränderungen im Datenbestand, wenn z.B. neue Dokumente erstellt wurden, die Ansicht ein anderes Ergebnis anzeigt.
Gibt die Datenbank zurück, in der sich die Ansicht befindet. Das zurückgegebene Database-Objekt ist ein und dasselbe, über das die Ansicht geöffnet wurde. In Kapitel 14 wird noch ausführlich auf das Thema Recycling eingegangen werden. An dieser Stelle soll als Vorgriff darauf hingewiesen werden, dass für sämtliche Domino-Java-Objekte deren Parent-Objekte nicht recycelt werden dürfen. Würde für die Ansicht die Datenbank getParent() recycelt, wird automatisch auch der View mit recycelt. Als Faustregel gilt: Es dürfen immer nur die Objekte, die im Programmcode aktiv erzeugt wurden, recycelt werden. Ein sauberes Recycling erhalten Sie, indem nach getaner Arbeit zunächst der View recycelt wird und danach die Datenbank, über den dieser geladen wurde. In jedem Fall sollte das Recycling nur nach einer Überprüfung der Objekte auf null erfolgen und in einem finally-Block untergebracht sein. •
String getUniversalID() throws NotesException
Wie jede Note (s. Kap. 2.2.1) einer Domino-Datenbank hat auch eine Ansicht eine Universal ID, die als String durch getUniversalID zurückgegeben wird. Eine Universal ID ist eine 32-stellige Hexadezimalzahl, d.h. der zurückgegebene String enthält nur die Zeiten 0..9 und A..Z. •
boolean isConflict() throws NotesException
Zeigt an, ob eine Kalender-Ansicht65 für die Überprüfung nach Zeit-Konflikten aktiviert ist. Diese Eigenschaft kann programmatisch nicht verändert werden. •
boolean isModified() throws NotesException
Diese Methode zeigt an, ob die Ansicht, die gerade für den aktuellen Benutzer angezeigt wird, dem aktuellen Stand der Ansicht auf dem Server entspricht, für den Fall, dass eventuell ein anderer Benutzer Dokumente so verändert oder erstellt hat, dass die Ansicht andere, mehr oder weniger Dokumente anzeigen müsste, also einen Refresh benötigt. Die Methode isModified liefert immer true, falls sie in einer Remoteverbindung eingesetzt wird und immer false, falls sie in einem Agenten oder einer Standalone-Anwendung (lokale Notes-Session) eingesetzt wird. Dies hat seinen Ursprung darin, dass diese Methode ihren Ursprung in der korrespondierenden UI-Methode hat. Folgt man den Ausschlüssen, für die diese Methode keine sinnvollen Ergebnisse liefert (keine Agenten, keine Remoteverbindungen, keine lokale Notes-Session), so bleiben nur noch Anwendungsbereiche, für die nur LotusScript eingesetzt werden kann, wie z.B. eine QueryOpen-Formel einer Ansicht. •
boolean isQueryView() throws NotesException
Neu seit Domino R7. Zeigt an, ob eine Ansicht ein Query View ist. Diese Methode ist nur sinnvoll für Datenbanken im NSFDB2-Format (s. Kap. 13.4.6). 65
Für eine Ansicht kann im Domino Designer im Kontext-Menü der Ansicht die Eigenschaft „Style“ einen der beiden Werte „Standard Outline“ oder „Calendar“ erhalten. Wird Style auf „Calendar“ gestellt, so wird die Ansicht nicht wie gewohnt als Liste im Notes Client dargestellt, sondern in der so genannten Kalenderansicht. Der persönliche Kalender im persönlichen Mail File ist ein Beispiel hierfür. Siehe auch isCalendar() in View.
Neu seit Domino R6.5. Setzt die SELECT-Formel einer Ansicht. Auch hier die Warnung vor unerwarteten Nebenwirkungen. Schließlich ist die SELECT-Formel das „Herz“ der Ansicht mit reichlich Nebenwirkungen bei Veränderung (Neuaufbau von Indizes). •
Neu seit Domino R6.5. Liefert die SELECT-Formel einer Ansicht. •
void remove() throws NotesException
Löscht eine Ansicht endgültig aus der Datenbank. Beachten Sie, dass das ViewObjekt, in dem Sie remove() aufrufen, sehr wohl noch recycelt werden muss, denn das Java-Objekt ist nur eine logische Instanz der physischen Implementierung der Ansicht in der Datenbank. Wird eine Ansicht gelöscht, werden auch beim nächsten Compact der Datenbank die (internen) Ansichtenindizes gelöscht. Das Löschen einer Ansicht hat nicht das Löschen von Dokumenten zur Folge. •
Ansichten können so konfiguriert werden, dass nur bestimmte Benutzer sie benutzen dürfen. Dies wird, wie auch für Dokumente üblich, über Leserfelder geregelt. Für jede Ansicht kann über setReaders ein Vector mit berechtigten Benutzern eingestellt werden. Beachten Sie unbedingt, dass für Leserfelder die Langform von Benutzernamen verwendet werden muss. Anders als in manchen anderen Situationen übersetzt hier Domino die Kurzform nicht in die Langform. Sie müssen also für Benutzernamen immer die Form „CN=User Name/O=Organisation“ oder „CN=User Name/OU=Organisational Unit/O=Organisation/C=Country“ usw. verwenden, wenn Sie Leserfelder verwenden. Dies gilt übrigens nicht nur an dieser Stelle für setReaders, sondern an jeder Stelle in Domino, an der programmatisch Leserfelder durch das API verändert werden. Wenn Sie dies nicht berücksichtigen, erzeugen Sie Ansichten, die von keinem Benutzer gelesen werden können, da bei der Überprüfung der Berechtigung eine Kurzform in der Art „User Name/Organisation“ sehr wohl in die Langform übersetzt wird. Die Langform des Benutzernamens findet sich aber dann nicht im Leserfeld. Um die Berechtigungen für eine Ansicht wieder zurückzusetzen, verwenden Sie setReaders (null). Abb. 10-2 zeigt die Eigenschaften des Design-Dokuments (der Design Note) für eine Ansicht, bei der das $Readers-Feld korrekt Abb. 10-2 $Readers-Feld im Design-Dogesetzt ist. kument für View
Diese Methoden arbeiten analog zu den gleichnamigen Methoden in Document (s. Kap. 7.1.4.2).
10.4
View und Document
Jede Ansicht kann als (sortierte) Sammlung von Referenzen auf Dokumente aufgefasst werden. Dementsprechend stellt das Java-Objekt View Methoden wie z.B. getFirstDocument und getNextDocument zur Verfügung, mit denen durch diese Elemente geschritten werden kann. Wie in Kapitel 7.3 erläutert, können Dokumenten Antwortdokumente zugeordnet sein. Diese Antwortdokumente können in Ansichten so angeordnet werden, dass sie dem übergeordneten Dokument zugeordnet sind. Ein Dokument kann innerhalb einer Ansicht seine Antwortdokumente mit getChild und dann mit getNextSibling referenzieren. Genauer wird auf das Verhalten von kategorisierten Ansichten im nächsten Kapitel eingegangen. Ein Antwortdokument kann sein übergeordnetes Dokument über die Methode getParentDocument erreichen. Einzelne Dokumente oder eine DocumentCollection von Dokumenten können in Ansichten über getDocumentByKey oder getAllDocumentsByKey gefunden werden. Bedenken Sie, dass die folgenden beschriebenen Methoden ein Document zurückgeben, also keinen Aufschluss darüber erlauben, welche Spaltendarstellung das Dokument in der Ansicht zur Folge hat, sondern lediglich eine Referenz auf den „Container“ Document erlauben. Werden Informationen über eine bestimmte Zeile der Ansicht einschließlich der in dieser Zeile angezeigten Spaltenwerte benötigt, steht ein weiteres Set von vergleichbaren Methoden zur Verfügung, die so genannte ViewEntry-Objekte zurückgeben. Ein ViewEntry erlaubt zwar zusätzlich eine Referenz auf das zugehörige Dokument, dient aber primär dazu, die Zeile der Ansicht (wie sie sich z.B. auch im NotesClient darstellt) zu referenzieren. Ein ViewEntry kann neben Dokumenten auch z.B. Kategorien repräsentieren, die ebenfalls als Zeile einer Ansicht angezeigt und auch programmatisch so aufgefasst werden. ViewEntries können am besten wie in Kapitel 10.5 beschrieben mit einem ViewNavigator verarbeitet werden. Zusätzlich kann eine Ansicht über die Methode FTSearch gefiltert werden. Nach der Anwendung von FTSearch verhält sich eine Ansicht so, als ob es lediglich die 66
Die genauen Randbedingungen für die Funktion dieser Methoden sind dem Autor unbekannt. In einer Testumgebung mit Domino 6.5.2 und Domino 7.0 konnte hier keine Funktion erkannt werden.
10.4 View und Document
381
durch FTSearch gefundenen Dokumente in der Ansicht gibt. Näheres zur Suche mit FTSearch finden Sie in Kapitel 15. Im Einzelnen stellen sich die Methoden wie folgt dar: •
Liefert das erste bzw. letzte Dokument in einer Ansicht. Falls eine Ansicht zuvor durch FTSearch gefiltert wurde, liefert diese Methode das erste bzw. letzte Dokument des gefilterten Sets. Gibt es keine Dokumente in der Ansicht, so liefern diese Methoden null. Ist ein View-Objekt älter, so kann es vorkommen, dass das Objekt nicht den aktuellen Stand der Ansicht repräsentiert, so dass mittels refresh() in View die Ansicht aktualisiert werden muss. Dies wird insbesondere dann auftreten, wenn während der Verarbeitung der eigene Prozess oder dritte Prozesse Dokumente derart ändern, dass sie nach der Änderung in der Ansicht auftauchen bzw. nicht mehr auftauchen. •
Liefert das nächste bzw. vorige Dokument zum Dokument aus dem Parameter. Gibt es kein nächstes bzw. voriges Dokument wird null zurückgegeben. Die Methoden in ViewNavigator (s. Kap. 10.5) liefern effizientere Methoden zur Behandlung von Dokumenten in Ansichten. •
Lädt das n-te Dokument einer Ansicht beginnend mit einer Zählung bei 1. Bei kategorisierten Ansichten werden nur Dokumente der obersten Ebene zurückgegeben. Zur Vorsicht muss geraten werden, wenn die Ansicht Dokumente mit Leserfeldern enthält, die zur Folge haben, dass der aktuelle Benutzer diese Dokumente nicht sehen darf. Wird solch ein Dokument an n-ter Stelle angefordert, wird das nachfolgende Dokument zurückgegeben. So kann es sein, dass bei einer Veränderung von n durchaus mehrmals das gleiche Dokument zurückgegeben wird, wenn z.B. für die Dokumente n und n+1 keine Leseberechtigung vorliegt, wird für n, n+1 und n+2 jedesmal das Dokument n+2 zurückgegeben. Erst n+3 wird, dann das nachfolgende Dokument zurückgeben. Diese Methode ist nicht performant und sollte nicht in Schleifen eingesetzt werden. •
Document getChild(Document parentDoc) throws NotesException Document getParentDocument(Document childDoc) throws NotesException Die Methode getChild liefert das erste Antwortdokument auf ein gegebenes Dokument und umgekehrt liefert geParentDocument wieder das Hauptdokument. Sollen nach getChild weitere Antworten referenziert werden, kann getNextSibling verwendet werden. Alle Antworten eines Hauptdokuments können mit getResponses in Document als unsortierte DocumentCollection geladen werden.
382
10 View Hat ein Dokument kein Antwortdokument, bzw. ist das Dokument selbst kein Antwortdokument, so geben getChild bzw. getParentDocument null zurück.
Diese Methoden liefern die so genannten Geschwister eines Dokuments. Für Anwortdokumente (siehe getChild) liefern diese Methoden die benachbarte Antwort auf dasselbe Hauptdokument, bzw. null, sofern es keine weitere Antwort gibt. Hat eine Antwort selbst wieder Antworten, muss auch hier zunächst wieder die erste Antwort per getChild geladen werden. Werden die Methoden auf Hauptdokumente angewendet, so liefern die Methoden die benachbarten Hauptdokumente. Grundsätzlich dienen die Methoden dazu, insbesondere in kategorisierten Ansichten, Dokumente derselben Ebene anzusteuern. In Ansichten, die durch FTSearch gefiltert wurden, geben diese Methoden das jeweils vorige oder nächste Dokument innerhalb der Ansicht zurück und zwar unabhängig vom Level. •
Document getDocumentByKey(Vector vector) throws NotesException Document getDocumentByKey(Vector vector, boolean exactSearch) throws NotesException Document getDocumentByKey(Object dominoObject) throws NotesException Document getDocumentByKey(Object dominoObject, boolean exactSearch) throws NotesException Sortierte Spalten in Ansichten können mit getDocumentByKey durchsucht werden, wobei als Suchschlüssel String-, Number-, DateTime- oder DateRange-Objekte oder ein Vector solcher Objekte zum Einsatz kommen kann. Wird ein solches Objekt in der entsprechenden Signatur (Parameter dominoObject) direkt übergeben, so wird in der ersten Spalte danach gesucht. Wird ein Vector aus String, Number, DateTime oder DateRange übergeben, so wird
in der ersten Spalte nach dem ersten Element des Vectors, in der zweiten nach dem zweiten Element des Vectors usw. gesucht.67 Zurückgegeben wird der erste Treffer innerhalb der Ansicht. Wurde kein Treffer gefunden, ist der Rückgabewert null. Der Parameter exactSearch ist optional und erzwingt eine exakte Übereinstimmung für die Suche. Default ist für diesen Parameter der Wert false. Die Suche ist in jedem Fall nicht abhängig von Groß- und Kleinschreibung.
67
Es wird empfohlen, nicht mehr als 4 Spalten für die Suche zu verwenden.
10.4 View und Document
383
Bei exactSearch == false oder falls dieser Parameter weggelassen wird68, wird nach einer Übereinstimmung gesucht, für die gilt, dass der Suchparameter mit dem Anfang des Spaltenwertes (unabhängig von Groß- und Kleinschreibung) übereinstimmt. Wortvarianten werden bei dieser Suche ebensowenig gefunden wie Zeilen, die nicht mit dem Suchparameter beginnen. Ob eine Ansicht Zeilen enthält, die den Suchparameter enthalten (ohne damit zu beginnen), kann mit dieser Methode nicht überprüft werden. Wird ein Objekt übergeben, so kann dieses ein Objekt der Klasse String, lotus.domino.DateTime, lotus.domino.DateRange oder Number sein. Für Number-, DateTime- und DateRange-Objekte hat der Parameter exactSearch keine Bedeutung. •
•
DocumentCollection getAllDocumentsByKey(Vector vector) throws NotesException DocumentCollection getAllDocumentsByKey(Vector vector, boolean exactSearch) throws NotesException DocumentCollection getAllDocumentsByKey(Object dominoObject) throws NotesException DocumentCollection getAllDocumentsByKey (Object dominoObject, boolean exactSearch) throws NotesException Die Methode getAllDocumentsByKey arbeitet wie getDocumentByKey, gibt jedoch alle gefundenen Treffer zurück. Allerdings ist diese DocumentCollection nicht sortiert. Für entsprechend der Ansicht sortierte Resultsets verwenden Sie getEntryByKey und getAllEntriesByKey. void refresh() throws NotesException boolean isAutoUpdate() throws NotesException void setAutoUpdate(boolean flag) throws NotesException Das Objekt View reflektiert jeweils den Zustand, den die zugehörige Ansicht zum
Zeitpunkt des Erzeugens des Objektes hatte. Falls sich durch eine Veränderung oder das Erstellen oder Löschen von Dokumenten das ResultSet einer Ansicht verändert, wird das Objekt diese Änderung nur dann widerspiegeln, wenn die Ansicht ihre Änderungen automatisch aktualisiert. Dies wird durch isAutoUpdate == true angezeigt und kann (temporär) durch setAutoUpdate(false) deaktiviert werden. Aus Performancegründen kann es sinnvoll sein die automatische Aktualisierung abzuschalten, sofern die Verarbeitung nicht darauf angewiesen ist, auch während der Verarbeitung Änderungen zu reflektieren. Durch refresh() kann eine Aktualisierung erzwungen werden. Bei Remoteverbindungen wird der Cache deaktiviert, falls isAutoUpdate() == true und bei der Verwendung von refresh() zurückgesetzt.
68
Eine nicht exakte Suche kann durchaus unerwartete Ergebnisse liefern. Beachten Sie, dass z.B. der Suchparameter „Dienst“ auch ein Dokument mit dem entsprechenden Spaltenwert „DIENSTAG“, nicht aber z.B. „Verdienst“ oder „verdienen“ findet. Für die Suche nach Zahlen ist dies unkritisch. Hier gilt nur die exakte Suche.
384 •
10 View int FTSearch(String searchQuery) throws NotesException int FTSearch(String searchQuery, int max) throws NotesException Anders als die Volltextsuche FTSearch in Database liefert die FTSearch in View keine DocumentCollection. Stattdessen wird die Ansicht gefiltert und das ResultSet des Objektes View so reduziert, dass darin nur noch Referenzen enthal-
ten sind, die dem Ergebnis der Suche entsprechen. Rückgabewert der Methode ist die Anzahl der gefundenen Dokumente, die durch den Parameter max begrenzt werden kann, wobei der Wert 0 für diesen Parameter die Suche nicht einschränkt. Wird FTSearch mehrfach nacheinander auf eine Ansicht angewendet, so wird zuvor das Resultset allerdings jeweils wieder zurückgesetzt, d.h. die Suche bezieht sich immer auf den ungefilterten Originalzustand. Um das Ergebnis einer Suche weiter einzuschränken, können die Ergebnisse von getAllDocumentsByKey oder getAllEntriesByKey ihrerseits mit FTSearch durchsucht werden. Die Filterung der Ansicht durch FTSearch ist sehr durchgängig. Selbst ein wiederholtes getView in Database lädt nicht die komplette Ansicht neu. Erst ein ausdrückliches clear() in View oder das Recyceln des View-Objektes und ein nachfolgendes getView lädt die Ansicht entsprechend ihrem Originalzustand. Eine bereits gefilterte Datenbank wird nicht aktualisiert, wenn der Datenbank neue Dokumente hinzugefügt werden, die auch in der gefilterten Ansicht sichtbar sein müssten. Um solche Dokumente zu erfassen, muss sogar der Volltextindex aktualisiert werden, damit der komplette Prozess von Laden der Ansicht und Durchführen der FTSearch wiederholt werden kann. FTSearch sollte nur angewendet werden, wenn isFTIndexed() in Database true liefert, andernfalls ist die Suche ineffizient und nicht alle Funktionen der Suchsyntax liefern die erwarteten Ergebnisse, da für einige Suchoperationen ausdrücklich der Volltextindex benötigt wird. Hierzu gehören z.B. die Schlüsselwörter PARAGRAPH oder SENTENCE, mittels derer mehrere Suchwörter so verknüpft werden können, dass sie im gleichen Absatz bzw. Satz gefunden werden müssen. Für FTSearch kommt eine spezielle Syntax zum Einsatz, die über searchQuery spezifiziert wird (s. Kap. 15). •
void clear() throws NotesException Wurde eine Ansicht durch FTSearch gefiltert, kann sie durch clear wieder zu-
rückgesetzt werden, so dass das View Objekt wieder alle Dokumente anzeigt, die der SELECT Formel der Ansicht entsprechen.
10.5
ViewNavigator, View und ViewEntry
Ansichten wurden bisher als Selektionselement für Notes-Dokumente beschrieben. In Kapitel 10.2 wurde dann anhand verschiedener Methoden näher auf kategorisierte Ansichten eingegangen. Aus Sicht des Java-Objekts View gibt es nun zwei Betrachtungsweisen auf die Elemente einer Ansicht. Einerseits die bereits bekannte Betrachtung als Sammlung von Dokumenten. Folgerichtig können die Dokumente einer Ansicht über die Methoden getFirstDocument, getNextDocument etc. durch-
10.5 ViewNavigator, View und ViewEntry
385
schritten werden. Bei kategorisierten Ansichten gibt es neben den einzelnen „Zeilen“, die die Dokumente repräsentieren, noch weitere Einträge, die Kategorieüberschriften oder Spaltensummen darstellen. Hier kommen die Objekte ViewEntry und ViewNavigator zum Einsatz. Eine Ansicht besteht demnach aus einzelnen ViewEntry-Objekten, die jeweils eine Zeile repräsentieren. Ein ViewEntry kann vom Typ Dokument, Kategorie, Summe oder Speicherkonflikt sein. Dies kann anhand der Methoden isDocument(), isCategory(), isTotal(), isConflict() in ViewEntry überprüft werden. Die Ansicht V_viewNavigator_k10 aus der Beispieldatenbank hat nach Durchlauf des Beispielcodes aus Listing 10-1 – auf den Code wird später noch genauer eingegangen – insgesamt dreizehn ViewEntry-Einträge (s. Abb. 10-1). An erster Stelle befindet sich ein ViewEntry für die Kategorie „Erste Kategorie“, gefolgt von den beiden ViewEntry für die Dokumente „1. Erstes Dokument“ und „2. Zweites Dokument“ und dem Antwortdokument „Antwort auf Zweites Dokument“. Das Antwortdokument wird in dieser Ansicht besonders behandelt. Für die zweite Spalte wurde im Domino Designer (s. Abb. 10-3) die Eigenschaft „Show responses only“ aktiviert. Dies hat zur Folge, dass Werte in dieser Spalte nur für Antwortdokumente angezeigt werden. Außerdem werden für Antwortdokumente keine weiteren Spaltenwerte für weiter rechts liegende Spalten angezeigt. Alternativ zu dieser Eigenschaft der Spalte kann alternativ in einer „normalen“ Spalte mit der @Formel @IsResponseDoc zwischen Hauptdokumenten und Antwortdokumenten unterschieden werden. Diese Technik wurde in der dritten Spalte angewendet.
Abb. 10-3 Kategorisierte Ansicht im Domino Designer
386
10 View
Best Practice Verwenden Sie ViewNavigator für komplexe Navigationsoperationen innerhalb einer Ansicht. Insbesondere die „goto“-Methoden sind effizient, da sie lediglich einen Cursor bewegen, ohne aufwändig Daten laden zu müssen. Verwenden Sie getAllDocumentsByKey, wenn lediglich die Dokumente als Treffer in beliebiger Reihenfolge benötigt werden. Verwenden Sie getAllEntriesByKey, wenn die Treffer in sortierter Reihenfolge benötigt werden. Je nach Datenbasis ist die Performance dieser Methoden sehr unterschiedlich. Auch die Verwendung von FTSearch kann selbst bei der Einbeziehung von Performanceüberlegungen sinnvoll sein (s. Kap. 15).
Die Kategorisierung dieser Ansicht erfolgt über die erste Spalte. In dieser Spalte wird das Feld F_category der Dokumente angezeigt. Zusätzlich hat die Spalte die Eigenschaften „Sort: Ascending“ und „Type: Categorized“. Domino gruppiert alle Dokumente, für die das Feld F_category identisch ist. Enthält eine kategorisierte Spalte Werte, die einen Backslash „\“ enthalten, so wird die Kategorie an der Position des Backslashes geteilt und es werden mehrere Sub-Kategorien angezeigt. Dementsprechend ist das fünfte ViewEntry in dieser Ansicht vom Typ „Kategorie“, das sechste, siebte, neunte, elfte und zwölfte vom Typ Dokument und das achte und zehnte ebenfalls vom Typ Kategorie, wobei diese beiden ViewEntry die Unterkategorien anzeigen. Das dreizehnte ViewEntry ist eine Summe. Diese entsteht dadurch, dass die vierte Spalte mit dem Titel „Anzahl Docs je Kategorie“ die Eigenschaft Totals aktiviert und auf den Wert „Totals“ gesetzt wurde. Dies bewirkt, dass alle Zahlen in dieser Spalte je Kategorie summiert und angezeigt werden. Alternativ könnten als Total z.B. auch Durchschnitte und Prozente je Dokument oder Kategorie automatisch berechnet werden. Eine derartige Summenspalte ist in der programmatischen Bearbeitung über ViewEntries recht einfach zu handhaben. Für (angehende) Notes-Programmierer sei hinzugefügt, dass dies leider nicht für die @Formelprogrammierung in Ansichten gilt. Summenspalten enthalten so genannten „besonderen Text“, was nichts anderes bedeutet, als dass die Spalte zwar als Text auf dem Bildschirm angezeigt wird, aber in keiner Weise weiterverarbeitet werden kann. Dies gilt selbstverständlich nicht für die Werte des Vectors aus getColumnValues() (s.u.). In ViewEntry liefert die Methode getColumnValues() einen Vector mit den Anzeigewerten für alle Spalten in der aktuellen Zeile. Der Vector liefert für jede Spalte jeweils den angezeigten Wert als String oder den leeren String "". Dabei wird keine Spalte ausgelassen, d.h. für jede Zeile hat dieser Vector die gleiche Anzahl
10.5 ViewNavigator, View und ViewEntry
387
von Elementen. Die Kategorie wird in diesem Beispiel folgerichtig immer im ersten Element des Vectors und z.B. die Summen immer im vierten Element des Vectors angezeigt. Auch wenn in der Ansicht für die Dokumente nicht jeweils die Kategorie angezeigt wird (das war ja die Idee der Kategorisierung), liefert getColumnValues für diese Einträge im ersten Element des Vectors nochmals die Kategorie. Leider ist getColumnValues ähnlich inkonsequent für Spalten mit der Eigenschaft „Show responses only“. Für solche Spalten werden im Spaltenvektor für alle Dokumente Werte angezeigt, nicht wie im Notes Client nur für Antwortdokumente. Zusätzlich hat ViewEntry die Methode getDocument, die für Einträge, die ein Dokument repräsentieren, die Referenz auf das Document, für alle anderen aber null liefert. Eine Besonderheit stellt das Antwortdokument auf das Dokument „2. Zweites Dokument“ dar. Da für die Ansicht die Eigenschaft „Show response documents in a hierarchy“ aktiviert wurde, wird es in jedem Fall hierarchisch unterhalb des Hauptdokuments angezeigt. Dies gilt sogar, wenn das Antwortdokument eine andere Kategorie als das Hauptdokument hat. Eine weitere Besonderheit gilt für das Antwortdokument. Die Methode isConflict() liefert (leider) auch für Antwortdokumente true. Zusätzlich liefert natürlich auch die Methode isDocument() true. In der Kategorisierung stellt das Antwortdokument eine eigene Ebene dar. Gleiches gilt für Speicherkonflikte, die intern ebenfalls als Antwortdokumente behandelt werden. Die einfachste Methode mit den ViewEntries einer Ansicht zu arbeiten ist die ViewEntryCollection, die ähnlich der DocumentCollection ViewEntries als Collection sammelt. Eine ViewEntryCollection aller ViewEntry einer Ansicht liefert getAllEntries(). Diese kann mit getFirstEntry(), getLastEntry(), getNextEntry() oder getPrevEntry() durchschritten werden. In der Klasse View stehen hierfür folgende Methoden zur Verfügung: •
10 View Sortierte Spalten in Ansichten können mit getEntryByKey durchsucht werden, wobei als Suchschlüssel String-, Number-, DateTime- oder DateRange-Objekte oder ein Vector solcher Objekte zum Einsatz kommen kann. Wird ein solches Objekt in der entsprechenden Signatur (Parameter dominoObject) direkt übergeben, so wird in der ersten Spalte danach gesucht. Wird ein Vector aus String, Number, DateTime oder DateRange übergeben, so wird in der ersten Spalte nach dem ersten Element des Vectors, in der zweiten nach dem zweiten Element des Vectors usw. gesucht.69 Zurückgegeben wird das zugehörige ViewEntry mit dem ersten Treffer und zwar anhand der Reihenfolge der Ansicht. Wurde kein Treffer gefunden, ist der Rückgabewert null. Der Parameter exactSearch ist optional und erzwingt eine exakte Übereinstimmung für die Suche. Default ist für diesen Parameter false. Die Suche ist in jedem Fall nicht abhängig von Groß- und Kleinschreibung. Bei exactSearch == false oder falls dieser Parameter weggelassen wird70, wird nach Zeilen in der Ansicht gesucht, deren Anfang mit dem Suchparameter (unabhängig von Groß- und Kleinschreibung) übereinstimmt. Wortvarianten werden bei dieser Suche ebensowenig gefunden wie Zeilen, die den Suchparameter lediglich enthalten, anstatt damit zu beginnen. Wird ein Object übergeben, so kann dies ein Objekt der Klasse String, lotus.domino.DateTime, lotus.domino.DateRange oder Number sein. Für Number-, DateTime- und DateRange-Objekte hat der Parameter exactSearch keine Bedeutung.
• •
•
69 70
ViewEntryCollection getAllEntries() throws NotesException ViewEntryCollection getAllEntriesByKey(Vector vector) throws NotesException ViewEntryCollection getAllEntriesByKey(Vector vector, boolean exactSearch) throws NotesException ViewEntryCollection getAllEntriesByKey(Object dominoObject) throws NotesException ViewEntryCollection getAllEntriesByKey (Object dominoObject, boolean exactSearch) throws NotesException Die Methoden getAllEntriesByKey arbeiten wie die entsprechenden Methoden getEntryByKey. Rückgabewert ist jedoch eine ViewEntryCollection mit allen gefundenen Treffern. Diese kann z.B. mit getFirstEntry oder getNextEntry durchschritten werden. Sie ist – anders als bei der Suche mit getAllDocumentsByKey – sortiert und zwar entsprechend der Reihenfolge der Ansicht. Wird kein Treffer gefunden, ist die ViewEntryCollection null. int getTopLevelEntryCount() throws NotesException
Es wird empfohlen, nicht mehr als 4 Spalten für die Suche zu verwenden. Eine nicht exakte Suche kann durchaus unerwartete Ergebnisse liefern. Beachten Sie, dass z.B. der Suchparameter „Dienst“ auch ein Dokument mit dem entsprechenden Spaltenwert „DIENSTAG“, nicht aber z.b. „Verdienst“ oder „verdienen“ findet.
10.5 ViewNavigator, View und ViewEntry
389
Gibt die Anzahl der Einträge in der obersten Hierarchieebene zurück. Ist die erste Spalte einer Ansicht kategorisiert, ist dies die Anzahl der Kategorien. Antwortdokumente werden hier nicht mitgezählt, auch wenn sie in flachen Hierarchien in der obersten Ebene angezeigt werden. •
int getEntryCount() throws NotesException
Liefert die Anzahl der Dokumente einer Ansicht einschließlich Antwortdokumenten. ViewEntry-Einträge für Kategorien oder Summen werden hier nicht mitgezählt. Eine Vereinfachung der Behandlung von ViewEntries stellt der so genannte ViewNavigator dar. Ein ViewNavigator wird in View über createViewNav und verwandte Signaturen erstellt. Ein ViewNavigator kann ähnlich einer ViewEntryCollection mit den Methoden getFirst, getNext oder getNth (int i) ein ViewEntry geladen werden. Zusätzlich verwaltet der ViewNavigator einen internen Cursor, der die aktuelle Position der Navigationsoperationen markiert, und kennt zusätzlich verschiedene „goto“-Methoden, z.B. gotoFirst, gotoNext, gotoPrev mit denen sich der Cursor positionieren lässt. Hierdurch kann schnell innerhalb einer Ansicht navigiert werden, ohne (!) dass das eigentliche ViewEntry oder gar Document geladen werden müsste. Dies kann dann im Nachgang duch getCurrent() erreicht werden. Ein weiterer Vorteil des ViewNavigators ist, dass er es erlaubt sehr genau innerhalb der Hierarchien einer kategorisierten Ansicht zu navigieren. Hierfür stehen z.B. für benachbarte Geschwister die Methoden getChild, getNextSibling, getPrevSibling und getParent bzw. gotoChild, gotoNextSibling, gotoPrevSibling und gotoParent zur Verfügung. Die „next“ und „previous“ Methoden stehen jeweils mit zwei Signaturen zur Verfügung, entweder ohne Parameter, wobei sie dann das nächste / vorige Element abhängig vom aktuellen Cursor liefern, oder mit einem ViewEntry als Parameter, so dass die Navigationsoperation abhängig von diesem ViewEntry vorgenommen wird. Darüber hinaus können sogar Kategorien direkt z.B. mit gotoNextCategory angesprungen werden. Insgesamt stehen in ViewNavigator folgende Methoden zur Verfügung: boolean gotoFirst() throws NotesException; boolean gotoFirstDocument() throws NotesException; boolean gotoNext() throws NotesException; boolean gotoNextCategory() throws NotesException; boolean gotoNextDocument() throws NotesException; boolean gotoPrev() throws NotesException; boolean gotoPrevCategory() throws NotesException; boolean gotoPrevDocument() throws NotesException; boolean gotoLast() throws NotesException; boolean gotoLastDocument() throws NotesException; boolean gotoPos(String hierarchie, char separatorChar) throws NotesException; boolean gotoChild() throws NotesException; boolean gotoNextSibling() throws NotesException;
Der Parameter viewentry ist immer eine Referenz, auf die die Navigation bezogen werden soll. i ist die Position, die angesprungen werden soll, obj ist entweder ein ViewEntry oder ein Document. Ein Element im ViewNavigator kann über einen Hierarchiestring mit getPos und gotoPos angesprungen werden. Als Parameter wird ein String übergeben, der für jeden Hierarchielevel die Position angibt. So wird z.B. mit dem String „3.7.1“ in der obersten Hierarchie der dritte Eintrag gewählt,
10.5 ViewNavigator, View und ViewEntry
391
dort in der darunter befindlichen Hierarchie der siebte und dort der erste. Dieser Eintrag im dritten Hierarchielevel wird zurückgegeben, entweder als ViewEntry (getPos) oder indem der Cursor an die entsprechende Stelle gesetzt wird (gotoPos). Sollte ein Eintrag nicht erreichbar sein, geben die „get“-Methoden null zurück und die „goto“-Methoden false. Neben den Methoden zur Navigation stehen in ViewNavigator noch folgende Methoden zur Verfügung: •
int getCacheSize() void setCacheSize(int numOfCachedViewEntries) throws NotesException;
Für DIIOP-Verbindungen kann die Größe des verwendeten Caches festgelegt werden. Angegeben wird die Anzahl der zu cachenden ViewEntries. Erlaubt sind laut Handbuch Werte von 0 bis 128 und Default 128. Es konnte jedoch festgestellt werden, dass Domino als Default 64 (R6.5.3) wählt und Werte von 0 bis 256 zulässt. Der Cache ist deaktiviert für numOfCachedViewEntries==0. Für lokale Sessions wird setCacheSize ignoriert und getCacheSize liefert immer 0. •
int getMaxLevel() void setMaxLevel(int maxDepth) throws NotesException
Wie beim Erzeugen eines ViewNavigator (s.u.), kann nachträglich die maximale Tiefe der Hierarchien festgelegt werden. Default ist 30. Ein Wert von 0 für maxDepth liefert die oberste Ebene. Für DIIOP-Verbindungen ist zu beachten, dass für einen ViewNavigator, der mit einer bestimmten Hierarchietiefe angelegt wurde, bestenfalls die Hierarchietiefe veringert werden kann. Ein nachträgliches Vergrößern der Hierarchietiefe hat keinen Einfluss auf die im ViewNavigator vorgefundenen ViewEntries. Bei lokalen Sessions ist auch ein nachträgliches Erweitern der Hierarchietiefe möglich, so dass der ViewNavigator nach der Erweiterung auch die tieferen Hierarchieebenen enthält. Zulässige Werte für maxDepth sind 0..30. •
View getParentView()
Liefert die Ansicht, aus der der ViewNavigator erzeugt wurde. •
int getCount()
Für lokale Sessions liefert diese Methode die Anzahl der ViewEntry im ViewNavigator. Für lokale oder DIIOP-Sessions liefert diese Methode für solche ViewNavigator, die mittels eines ViewEntry, eines Document oder einer Kategorie erzeugt wurden, die Anzahl der ViewEntry im ViewNavigator. Wurde ein ViewNavigator mit createViewNavMaxLevel und DIIOP erzeugt, liefert die Methode getCount immer die Anzahl aller Entries einer Ansicht. Ein ViewNavigator kann direkt in View erstellt werden und kann entweder auf der gesamten Ansicht, auf einem spezifizierten ViewEntry oder Document-Objekt basieren oder mittels einer benannten Kategorie erzeugt werden. Im Einzelnen sind dies folgende Methoden in View:
392
•
•
•
10 View
ViewNavigator createViewNav() throws NotesException ViewNavigator createViewNav(int numOfCachedViewEntries) throws NotesException Erzeugt einen ViewNavigator über alle Einträge einer Ansicht. Über numOfCachedViewEntries kann der Cache bei DIIOP-Verbindungen eingestellt werden (s.o. setCacheSize in ViewNavigator). ViewNavigator createViewNavMaxLevel(int maxDepth) throws NotesException ViewNavigator createViewNavMaxLevel (int maxDepth, int numOfCachedViewEntries) throws NotesException Erzeugt einen ViewNavigator über alle Einträge einer Ansicht, jedoch limitiert auf eine maximale Hierarchietiefe maxDepth. Siehe setMaxLevel in ViewNavigator. Über numOfCachedViewEntries kann der Cache bei DIIOP-Verbindungen eingestellt werden (s.o. setCacheSize in ViewNavigator). ViewNavigator createViewNavFrom(Object viewEntryOrDoc) throws NotesException ViewNavigator createViewNavFrom (Object viewEntryOrDoc, int numOfCachedViewEntries) throws NotesException Erzeugt einen ViewNavigator aller ViewEntry, die sich in der Reihenfolge der Ansicht nach einem als Parameter viewEntryOrDoc übergebenen ViewEntry oder Document befinden, einschließlich des übergebenen Objekts. Wider Erwarten arbeitet diese Methode wie folgt: Es werden alle ViewEntry in den ViewNavigator übernommen, die in der Reihenfolge der Ansicht nach
dem Objekt kommen. Die Definition ist also zu verstehen als „Ab dem Objekt“ und nicht als „Unterhalb des Objekts“. Das heißt im Klartext, dass wenn z.B. als Objekt eine ViewEntry einer Kategorie der obersten Ebene übergeben wird, nicht etwa nur diese Kategorie und deren abhängige enthalten sind, sondern alle (!) Einträge, die nach diesem ViewEntry entsprechend der Reihenfolge der Ansicht folgen. Sollen nur Einträge mit höherem Level unterhalb eines bestimmten Objektes selektiert werden, müssen die Methoden createViewNavFromChildren oder createViewNavFromDescendants verwendet werden. Diese Methode ist geeignet alle Einträge einer Ansicht ab einem bestimmten Punkt auszuwählen. Über numOfCachedViewEntries kann der Cache bei DIIOP-Verbindungen eingestellt werden (s.o. setCacheSize in ViewNavigator). •
ViewNavigator createViewNavFromChildren (Object viewEntryOrDoc) throws NotesException ViewNavigator createViewNavFromChildren (Object viewEntryOrDoc, int numOfCachedViewEntries) throws NotesException Erzeugt einen ViewNavigator mit allen ViewEntry, die sich im Level direkt
unterhalb eines Objektes befinden. Das als Parameter übergebene Objekt wird nicht mit in den ViewNavigator aufgenommen.
10.5 ViewNavigator, View und ViewEntry
393
Diese Methode ist geeignet, in einer Ansicht alle primären Einträge einer Kategorie oder alle direkten Antwortdokumente eines Dokuments ohne eventuelle Subkategorien oder weitere Antwort-aufAntwort-Dokumente zu wählen. Über numOfCachedViewEntries kann der Cache bei DIIOP-Verbindungen eingestellt werden (s.o. setCacheSize in ViewNavigator). •
ViewNavigator createViewNavFromDescendants (Object viewEntryOrDoc) throws NotesException ViewNavigator createViewNavFromDescendants (Object viewEntryOrDoc, int numOfCachedViewEntries) throws NotesException Erzeugt einen ViewNavigator mit allen ViewEntry, die sich im Level unterhalb eines Objektes befinden, einschließlich aller ViewEntry unterhalb dieser Einträge. Das als Parameter übergebene Objekt wird nicht mit in den ViewNavigator aufgenommen.
Diese Methode ist geeignet, in einer Ansicht alle primären Einträge einer Kategorie oder alle Antwortdokumente eines Dokuments einschließlich eventueller Subkategorien oder weiterer Antwort-aufAntwort-Dokumente zu wählen. Über numOfCachedViewEntries kann der Cache bei DIIOP-Verbindungen eingestellt werden (s.o. setCacheSize in ViewNavigator). •
ViewNavigator createViewNavFromCategory (String categoryName) throws NotesException ViewNavigator createViewNavFromCategory (String categoryName, int numOfCachedViewEntries) throws NotesException Diese Methode arbeitet wie createViewNavFromDescendants, wobei statt ei-
nes Objektes eine benannte Kategorie übergeben wird. Sub-Kategorien, die durch die Verwendung eines Backslash „\“ erzeugt wurden, können mit dieser Methode nicht selektiert werden. In Listing 10-1 werden verschiedene ViewNavigator-Objekte erzeugt und ihr Verhalten demonstriert. Um die Demonstration reproduzierbar zu machen, werden nach dem anfänglichen Erzeugen von Session, Database und View über die Me-
package djbuch.kapitel_10; import lotus.domino.*; import djbuch.kapitel_06.*; public class ViewNavigatorDemo extends NotesThread { private static final String PASSWORD = "geheim"; // DIIOP-Server u. -User, null für lokale Session private static final String HOST = null; private static final String USER = null; public static void main(String[] args) { ViewNavigatorDemo vnd = new ViewNavigatorDemo(); vnd.start(); } public void runNotes() { View view = null; Session session = null; ViewNavigator viewNav = null; try {
alte Testdokumente gelöscht und neue Dokumente angelegt, die thode setup() in der Ansicht V_viewNavigator_k10 in einer hierarchischen Struktur dargestellt werden. In setup() werden neue Dokumente über createDoc erstellt und über das Item F_Category unterschiedlichen Kategorien zugeordnet. Das Item F_title wird mit einem Titel belegt, damit später die Dokumente über diesen Titel wieder selektiert werden können, um als Wurzel für einen ViewNavigator zur Verfügung zu stehen. Das Item F_marker_k6 wird mit dem Wert 1 belegt, um bei jedem Aufräumen durch cleanup diese Testdokumente selektieren zu können. In setup werden zwei Arten von Dokumenten erstellt. Zum einen werden einfache Dokumente erstellt, die lediglich aus den Items für Kategorie und Titel bestehen . Die Kategorie selbst (und das zugehörige ViewEntry) wird nicht ausdrücklich erstellt, sondern entsteht durch die Spalteneigenschaft der Ansicht, die die Felder F_Category der Dokumente kategorisiert anzeigt (s. auch Kap. 3.3.2). Zusätzlich wird als Beispiel eines Antwortdokuments ein solches erstellt , wobei createDoc angewiesen wird, das Antwortdokument als Antwort auf einen zuvor selektierten Parent (Hauptdokument) zu beziehen. Hierfür kommt die Methode makeResponse in Document zum Einsatz . Im gesamten Beispielcode wird bereits eine bisher noch nicht eingeführte Klasse „GC“ verwendet (nicht zu verwechseln mit der Klasse sun.misc.GC). Sie wird ausführlich im Kapitel über das Recycling erläutert und kapselt im Wesentlichen die recycle-Aufrufe der Domino-Java-Objekte. In der Methode setup kommt eine der wichtigsten Regeln beim Recycling zum Tragen: Jedes Domino-Java-Objekt, das aufgrund einer Neu-Zuweisung seine Referenz auf das ursprüngliche Domino-Objekt verliert (subsequente Zuweisungen), muss recycelt werden. Die Variable doc wird in setup verwendet, um neue Dokumente über createDoc zu erzeugen. Jeder neue Aufruf von createDoc und die Zuweisung zu doc macht die Referenz auf das zuvor erzeugte Document (aus Sicht von Java) ungültig, muss also recycelt werden. Für das zu erzeugende Antwortdokument werden Antwortdokument und Hauptdokument gleichzeitig benötigt und können erst nach Erstellung des Antwortdokuments recycelt werden . Nachdem nun die benötigten Beispieldokumente erzeugt wurden, muss das View-Objekt aktualisiert werden, das vor dem Erstellen der Dokumente erzeugt wurde. Die erste Demonstration erzeugt mittels createViewNav() in View einen ViewNavigator über die gesamte Ansicht und gibt diese mit der Hilfsmethode printNavigator aus. Die Ausgabe zeigt alle ViewEntry der Ansicht einschließlich der Kategorien des Antwortdokuments und der Summenspalte und ähnelt der Darstellung im NotesClient (s. Abb. 10-1): Kategorie: Erste Kategorie Dokument: 1. Erstes Dokument Dokument: 2. Zweites Dokument Dokument: Antwort auf Zweites Dokument Kategorie: Zweite Kategorie Dokument: 6. Sechstes Dokument Dokument: 7. Siebtes Dokument
10.5 ViewNavigator, View und ViewEntry Kategorie: Sub Dokument: 3. Kategorie: Sub Dokument: 4. Dokument: 5. Total: 7.0
399
eins Drittes Dokument zwei Viertes Dokument Fünftes Dokument
Auch der ViewNavigator muss, sofern er nicht mehr benötigt wird, recycelt werden. Um spätere Fehler zu vermeiden liefert die Methode recycle in GC immer null zurück, so dass ein Objekt einfach in einem Zug recycelt und null gesetzt werden kann . Dies hilft Fehler zu vermeiden, da NullPointerExceptions leicht erkannt werden können, jedoch das Verhalten bei der (versehentlichen) Benutzung von recycelten Objekten nicht vorhersehbar ist. Die zweite Demonstration zeigt die Verwendung von Navigatoren, für die die Hierarchietiefe beschränkt ist. Für i == 0 ist hier z.B. erkennbar, dass lediglich die oberste Ebene, also die ViewEntry für die beiden Kategorien „Erste Kategorie“ und „Zweite Kategorie“ ausgegeben wird. Die Beispiele 3 bis 5 wurden zur Vereinfachung in der Methode demoChildrenDescendants gekapselt und demonstrieren das Verhalten der Methoden createViewNavFrom, createViewNavFromChildren und createViewNavFromDescendants und zwar jeweils für die beiden Möglichkeiten, als Parameter ein ViewEntry oder ein Document zu übergeben. Der Parameter type bestimmt, welche der drei Methoden zur Erzeugung eines ViewNavigator verwendet werden soll . Für die Demonstration A, für die ein ViewEntry als Wurzel zum Einsatz kommen soll, wird dieses mit einer Hilfsmethode getElement erzeugt . Diese Methode durchschreitet einen neu erzeugten ViewNavigator über alle Elemente einer Ansicht . Wird ein Dokument gefunden, dessen Titel (Feld F_title ) mit dem Parameter key übereinstimmt, so wird das korrespondierende ViewEntry zurückgegeben. Wird eine Kategorie mit diesem Namen gefunden, so wird dieses ViewEntry zurückgegeben. Die Methode getElement ist vorteilhaft für die gewählte Art der Demonstration, da über die statischen Arrays SUB_ENTRIES Kategorien und Dokumente gezielt angesprochen werden können. Variieren Sie die Einträge in SUB_ENTRIES , um das Verhalten der createViewNav...-Methoden für die verschiedenen Elemente einer Ansicht zu testen und zu verstehen. Die Demonstration B in demoChildrenDescendants arbeitet ähnlich der Demonstration A, jedoch werden Dokumente als Wurzel verwendet. Auch hier kommt ein Array zum Einsatz, um die verschiedenen Szenarien leicht durchspielen zu können, jedoch werden als Parameter Objekte der Klasse Document übergeben . Diese werden anhand ihres Titels in der Methode getDocument ausgewählt. Diese Methode ist übrigens ein typisches Beispiel für die Selektion von Dokumenten über eine Ansicht und getDocumentByKey. Beachten Sie, dass die Methode zwar im finally den View recycelt, nicht aber das Document. Dieses wird noch als Rückgabewert benötigt. Durch getDocument erzeugte Document-Objekte müssen von der aufrufenden Methode recycelt werden.
400
10 View
Anhand der Ausgaben der Demonstrationen 3 bis 5 lässt sich erkennen, dass es keinen Unterschied macht, ob als Wurzel für einen ViewNavigator ein Document oder das dieses Document repräsentierende ViewEntry als Parameter für die createViewNav...-Methoden verwendet wird. Die Ausgaben verdeutlichen das unterschiedliche Verhalten der verschiedenen createViewNav...-Methoden. createViewNavFrom zeigt alle einträge ab einer bestimmten Stelle und gibt dementsprechend z.B. für das Dokument mit Titel „2. Zweites Dokument“ aus: Unterhalb von Document 2. Zweites Dokument Dokument: 2. Zweites Dokument Dokument: Antwort auf Zweites Dokument Kategorie: Zweite Kategorie Dokument: 6. Sechstes Dokument Dokument: 7. Siebtes Dokument Kategorie: Sub eins Dokument: 3. Drittes Dokument Kategorie: Sub zwei Dokument: 4. Viertes Dokument Dokument: 5. Fünftes Dokument Total: 7.0
Sollen nur die Kinder eines Dokuments (Antwortdokumente) oder einer Kategorie (Elemente dieser Kategorie) erreicht werden, wird createViewNavFromChildren benötigt, wobei nur die direkten Nachkommen angezeigt werden, z.B.: Children unterhalb von Document 2. Zweites Dokument Dokument: Antwort auf Zweites Dokument
Sollen alle Elemente unterhalb eines Eintrages angezeigt werden, kommt createViewNavFromDescendants zum Einsatz, z.B.: Descendants Dokument: Dokument: Dokument:
unterhalb von Entry Erste Kategorie 1. Erstes Dokument 2. Zweites Dokument Antwort auf Zweites Dokument
Für Kategorien kann anstelle von createViewNavFromDescendants auch createViewNavFromCategory verwendet werden . Wie die Ausgabe der im Beispiel verwendeten Schleife zeigt, können hierbei keine Sub-Kategorien als Parameter verwendet werden: Navigator der Kategorie Zweite Kategorie\Sub eins Dokument: 6. Sechstes Dokument Dokument: 7. Siebtes Dokument Kategorie: Sub eins Dokument: 3. Drittes Dokument Kategorie: Sub zwei Dokument: 4. Viertes Dokument
10.6 Locking
401
Dokument: 5. Fünftes Dokument
Am Ende der Demonstration müssen natürlich noch die verwendeten Domino-Objekte dem Recycling zugeführt werden . Die erzeugten Dokumente werden erst beim nächsten Start und Aufruf von setup gelöscht , so dass Sie zur Begutachtung zur Verfügung stehen.
10.6
Locking
Das Design Locking für einen View arbeitet analog den in Kapitel 7.7 beschriebenen Mechanismen des Document Locking. Lediglich muss die Datenbank statt für Document Locking für Design Locking aktiviert sein (isDesignLockingEnabled in Database). Ein typischer Anwendungsfall für das Design Locking sind Anwendungen, die Gestaltungsänderungen an Agent, View oder Form vornehmen und z.B. eine Versionskontrolle oder Check-in/check-out-Mechanismen etablieren. In View wie in Agent und Form stehen für das Locking die folgenden Methoden zur Verfügung: • •
Die Sammlungen DocumentCollection und ViewEntryCollection wurden bereits vielfach als ResultSet von Lookup-Operationen vorgestellt. Sie dienen dazu, Untermengen von Dokumenten der Datenbank oder aus Ansichten zu sammeln, können aber auch aus verschiedenen vordefinierten Dokumentenmengen, wie getAllDocuments in Database, getAllEntries in View oder getUnprocessedDocuments in AgentContext erzeugt werden.
402
10 View
Best Practice DocumentCollection wird verwendet, wenn unsortierte Dokumentensammlungen benö-
tigt werden, die nicht durch Ansichten abgebildet sind. ViewEntryCollection wird verwendet, wenn sortierte Dokumentensammlungen oder
Spaltenberechnungen benötigt werden. stampAll ist die Methode der Wahl, wenn für eine Sammlung von Dokumenten gleichzeitig
ein einzelnes Item auf einen bestimmten Wert gesetzt werden soll. View wird verwendet, wenn eine Ansicht zur Verfügung steht, die die benötigten Dokumente
bereits vorselektiert enthält. Achtung bei der Veränderung von Dokumenten und beim Einsatz von Zeitberechnungen. setAutoUpdate(false) und isAutoUpdate verwenden! ViewNavigator wird verwendet, wenn Unterkategorien selektiert werden müssen, oder
wenn nicht nur Dokumente, sondern auch ViewEntries für Kategorien oder Summen referenziert werden müssen.
Die beiden ähneln sich zwar sehr stark, unterscheiden sich aber dennoch in der Anwendung. Je nachdem, ob die Spalten und deren Spaltenberechnungen einer Ansicht oder direkt die Dokumente benötigt werden, kann zwischen DocumentCollection und ViewEntryCollection gewählt werden. In der Regel werden beide nicht direkt erzeugt, sondern sind das Ergebnis von Suchoperationen bzw. stellen die Repräsentation bestimmter vordefinierter Mengen dar (z.B. getAllEntries in View). Der typische Anwendungsfall ist die Suche über einen Ansichtenindex, der immer über die Suche über sortierte Spalten mit den Methoden getAllEntriesByKey oder getAllDocumentsByKey durchgeführt wird (s. Kap. 10.4f und 15). Zunächst ist die Verwendung via getAllEntriesByKey oder getAllDocumentsByKey bzw. getEntryByKey oder getDocumentByKey gleichermaßen einfach und aus Performancegesichtspunkten absolut gleichwertig, sofern man nur den Zeitaufwand für die Durchführung dieser Suchen betrachtet. Allerdings ist sehr genau zu unterscheiden, welche Daten tatsächlich benötigt werden und welcher Art und Menge die durchsuchten Daten sind. Werden sortierte Resultsets benötigt, oder sollen Spaltenwerte aus der zu durchsuchenden Ansicht und insbesondere die Ergebnisse der dort angewendeten Spaltenberechnungen gelesen werden, kommt nur die ViewEntryCollection in Betracht, da diese ein Abbild einer Teilmenge von Elementen (Zeilen) einer Ansicht darstellt und getAllEntriesByKey im Gegensatz zu der Suche via getAllDocumentsByKey entsprechend der Reihenfolge der durchsuchten Ansicht sortierte Ergebnisse liefert.
10.7 DocumentCollection und ViewEntryCollection
403
Für beide gilt, dass die Referenz auf die enthaltenen Dokumente zur Verfügung steht, bei der ViewEntryCollection indirekt über die Methode getDocument() in ViewEntry. Anders als ein ViewNavigator enthält eine ViewEntryCollection keine Einträge für Kategorien oder Summen. Um die Elemente einer ViewEntry- oder DocumentCollection zu durchschreiten, stellen beide entsprechende Funktionen wie getFirstEntry, getFirstDocument und getNextEntry oder getNextDocument zur Verfügung. Hier jedoch zeigen sich deutliche Performanceunterschiede. Um an die eigentlichen Daten, also an die Items der Dokumente des Resultsets heranzukommen, werden diese bei der Verwendung von DocumentCollection wie gewohnt über getItemValue usw. aus den zurückgegebenen Dokumenten geladen. Bei der ViewEntryCollection können alle Spaltenwerte gleichzeitig über getColumnValues als Vector bezogen und wie in Vector üblich, über elementAt referenziert werden. Versuche haben gezeigt, dass das reine Durchschreiten und das Laden eines einzigen Items bzw. Spaltenwertes einer ViewEntryCollection rund doppelt so viel Zeit in Anspruch nimmt wie das Durchschreiten einer DocumentCollection. Werden jedoch viele Items des jeweiligen Document benötigt und müssen diese jeweils mit getItemValue oder einer verwandten Methode geladen werden, verschiebt sich das Verhältnis mit der Anzahl der zu lesenden Items in Richtung der ViewEntryCollection. Im Versuchsaufbau war ein Gleichstand ungefähr bei 10 bis 20 Items, die gelesen werden sollten, erreicht. Diese Angaben sollten aber weniger als absolute Werte dienen, sondern lediglich einen Anhaltspunkt liefern, welche Voraussetzungen welchen Einflus auf die Performance haben. Das direkte Durchschreiten eines Views ist übrigens aus Performancesicht ungefähr vergleichbar mit dem Durchschreiten einer DocumentCollection. Bei diesen Überlegungen sind zwei weitere Punkte zu berücksichtigen, zumindest bei ressourcenkritischen Anwendungen. Ansichten belegen hauptsächlich Speicher, der von Domino verwaltet wird, also außerhalb der Reichweite von Java, so dass dieser nicht direkt durch Ihre Anwendung kontrolliert werden kann. Je komplexer Ansichten aufgebaut sind, desto größer wird der Hauptspeicherbedarf für eine solche Ansicht sein. Zusätzlich ist zu beachten, dass aufwändige Spaltenberechnungen die Laufzeit negativ beeinflussen werden. Finden sich Zeitberechnungen in den Spalten, muss Domino ständig seine Indizes aktualisieren, da sich die Zeit ja bekanntlich regelmäßig ändert :-). Werden solche Berechnungen benötigt, macht es unter Umständen Sinn, entsprechende Daten redundant in den Dokumenten zu speichern und eventuell regelmäßig durch einen periodischen Agenten aktuell zu halten. Zusammenfassend kann festgehalten werden, dass für die ViewEntryCollection die Verarbeitungsdauer mit der Anzahl der Spalten in der Ansicht zunimmt, wobei dieser Einfluss verhältnismäßig gering ist. Für die DocumentCollection nimmt die Verarbeitungszeit zu, wenn viele Items in den gefundenen Dokumenten gelesen werden sollen. Zusätzlich wird dieses Lesen der Items durch die Anzahl der Items in einem Dokument, mit steigender Anzahl negativ, beeinflusst, ebenso wie die Datenmenge in den Items einen Einfluss auf die Verarbeitungsgeschwindigkeit hat. Eine entsprechen-
404
10 View
de Gegenüberstellung der einzelnen Faktoren wurde in Tabelle 10-1 zusammengestellt. Wenn Sie sich für die praktische Seite dieser Übersicht interessieren, können Sie sich die Klasse CollectionComparison im Package djbuch.kapitel_10 näher ansehen. Diese ist ähnlich wie die SearchComparison aus Kapitel 15.7 aufgebaut und führt verschiedene Loop-Operationen über Ansichten und Collections aus. Bisher wurden DocumentCollection und ViewEntryCollection als Ergebnismenge vorgestellt. Darüber hinaus gibt es die Möglichkeit diesen Collections Elemente hinzuzufügen oder zu entfernen. Mit einem kleinen Trick ist es sogar möglich, sowohl die ViewEntryCollection als auch die DocumentCollection neu zu erstellen, so dass unabhängig von Abfragen manuell Dokumente hinzugefügt oder entfernt und somit frei definierte Zusammenstellungen von Dokumenten erstellt werden können. Eine leere DocumentCollection erhalten Sie, wenn das Ergebnis einer Suche kein Ergebnis liefert. Am einfachsten lässt sich dies mit dem Code emptyDC = myDatabase.search("1=0"); realisieren, da eine Suche ohne Ergebnisse automatisch eine leere DocumentCollection zurückgibt. Ähnliches gilt für die ViewEntryCollection, die aber immer von einer Ansicht abhängt. Hier muss eine Suche nach einem sicher nicht vorliegenden Spaltenindex in der Art emptyViewEntryCollection = view.getAllEntriesByKey("~~~DiesenSpaltenWertGibtEsNicht~~~",true); durchgeführt werden. Nun können via addDocument(Document doc) bzw. addEntry (Object viewEntryOrDoc) Einträge zu diesen neuen leeren Collections hinzugefügt werden, wobei der Parameter von addEntry ein ViewEntry oder ein Document sein kann. Interessant ist hierbei, dass ein auf diese Weise an eine ViewEntryCollection angefügtes Dokument wie alle Elemente der ViewEntryCollection behan-
delt wird, einschließlich der dort definierten Spaltenberechnungen, die sich dann in getColumnValues niederschlagen. Voraussetzung ist, dass das angefügte Element auch über die Ansicht gefunden würde, sonst führt addEntry keine Aktion durch.
Werden Dokumente bzw. ViewEntries aus den Collections entfernt, ist es notwendig, dass die zugehörigen Java-Objekte direkt aus der Collection erzeugt wurden. Wird dies nicht beachtet, wird eine entsprechende NotesException geworfen. Dementsprechend ist folgender Code nicht zulässig: doc=getDocFromSomeWhere(); dc.addDocument(doc); dc.deleteDocument (doc);
Stattdessen muss in der dritten Zeile das zu entfernende Dokument über die DocumentCollection neu geladen werden, z.B. über einen der Befehle getFirstDocument, getNthDocument etc. Ein einfaches Beispiel sieht (verkürzt) wie folgt aus: db = session.getDatabase("", "djbuch/djbuch.nsf"); dc = db.search("1=0"); doc = db.getAllDocuments().getLastDocument(); System.out.println (dc.getCount() + "/" +
10.7 DocumentCollection und ViewEntryCollection
405
Tabelle 10-1 DocumentCollection und ViewEntryCollection im Vergleich
Primäres Anwendungsgebiet
Ergebnisse entsprechend der Ansicht sortiert
Zugriff auf Document und damit alle Items Zugriff auf Kategorien oder Summen Erstellung unabhängig einer Ansicht möglich
Spaltenberechnungen möglich
Kann ein einzelnes Item aller enthaltenen Dokumente ändern Hinzufügen und Entfernen von Einträgen möglich Suche per getAllEntriesByKey/getAllDocumentsByKey Nettozeit für das Durchschreiten der Collection
DocumentCollection
ViewEntryCollection
Schnelle Referenz auf (in der Regel) unsortierte Ergebnismengen Nein, nur falls die DocumentCollection Ergebnis einer FTSearch ist, werden die Ergenisse entsprechend der SortOptions, bzw. nach Score sortiert Ja
Referenz auf sortierte Ergebnismengen oder auf Spaltenberechnungen Ja
Nein
Nein
Ja. Einige vordefinierte DocumentCollections wie z.B. getAllDocuments in Database stehen zur Verfügung Nein, es stehen nur die Werte der Items der gefundenen Dokumente zur Verfügung
Nein
Ja, per stampAll()
Ja, es stehen sowohl die Werte der Items, als auch die Spaltenwerte der Ansicht zur Verfügung Ja, per stampAll()
Ja
Ja
performant
performant
performant
performant, aber selbst bei wenigen Spalten in der Ansicht langsamer als bei DocumentCollection leicht negativer Einfluss
Einfluss auf Performance mit kein Einfluss zunehmender Anzahl der Spalten in der Ansicht Einfluss auf Performance mit negativ, wg. Einfluss von zunehmender Anzahl der zu getItemValue lesenden Items der gefundenen Dokumente
Einfluss auf Performance durch Größe der Items im Dokument / in der Ansichtenspalte
Ja
negativ
kein Einfluss, sofern auf die Ansichtenspalten per getColumnValues zurückgegriffen wird. Wird über getDocument das Dokument geladen, kommen hier natürlich wie für die DocumentCollection die Zeiten für getItemValue hinzu. kaum. Die Anzeige in Spalten ist allerdings auf 32.000 Zeichen je Eintrag limitiert.
Mit dieser Technik können also Domino-Dokumente in Collections gesammelt und gewissermaßen im Hauptspeicher zwischengespeichert werden. Allerdings sind sie weiterhin an die hiermit verbundene Session gebunden. Aus Sicht von Java ist dies in einigen Situationen nicht befriedigend oder zumindest nicht ausreichend, insbesondere wenn Daten gecached oder über mehrere Threads referenziert werden müssen. Da Domino-Objekte ohnehin nicht über mehrere Threads gültig sind, sind hier andere Techniken notwendig, die in Kapitel 13.2 und 13.3 vorgestellt werden. Neben den beschriebenen Einsatzgebieten gibt es noch einige praktische Methoden, die die Arbeit mit Sammlungen von Dokumenten erleichtern. Dies sind vor allem die Methoden stampAll, mit der ein einzelnes Item der gesamten Sammlung in einem Schritt auf einen bestimmten Wert gesetzt werden kann, putAllInFolder und removeAllFromFolder, mit denen Referenzen der Dokumente in einen Ordner gelegt werden können (s. Kap. 3.3) und removeAll, mit der sämtliche Dokumente der Collection auf einmal und endgültig aus der Datenbank gelöscht werden. Einen Überblick über sämtliche Methoden in DocumentCollection und ViewEntryCollection finden Sie im Folgenden. Alle Methoden stehen übrigens für beide Klassen identisch bzw. analog zur Verfügung.
10.7.1 •
Methoden in DocumentCollection
int getCount() throws NotesException Liefert die Anzahl der Elemente in der DocumentCollection und 0, falls die
Collection keine Elemente enthält.
71
Der hier verwendete wiederholte Aufruf von getAllDocuments ist nicht performant und wurde hier nur der Einfachheit halber gewählt.
10.7.1 Methoden in DocumentCollection •
• •
•
407
String getQuery() throws NotesException Gibt die Suchquery zurück, mittels derer eine DocumentCollection entstanden ist. So z.B. für search oder FTSearch in Database, aber auch für FTSearch in DocumentCollection. Falls keine Suche im eigentlichen Sinne oder ein Lookup ohne Suchquery zugrunde lag, wie z.B. bei getAllDocumentsByKey, wird ein leerer String zurückgegeben. Database getParent() throws NotesException Liefert das Database Objekt, in dem die DocumentCollection erstellt wurde. Document getFirstDocument() throws NotesException Die Klasse DocumentCollection verwaltet einen internen Cursor, der die Position innerhalb der Collection anzeigt. getFirstDocument gibt das erste Document der DocumentCollection zurück und setzt den Cursor auf die erste Position. Gibt null zurück, falls die DocumentCollection leer ist. Document getLastDocument() throws NotesException Gibt das letzte Document der DocumentCollection zurück und setzt den Cursor auf die letzte Position. Gibt null zurück, falls die DocumentCollection leer
ist. •
Document getNextDocument() throws NotesException Document getNextDocument(Document document) throws NotesException Gibt das nächste Dokument nach dem Cursor innerhalb der DocumentCollection entsprechend der Reihenfolge in der DocumentCollection zurück und setzt den Cursor auf diese Position. Wird als Parameter ein Document übergeben, so wird die Position nach diesem Document gewählt, wobei das Dokument aus der DocumentCollection stammen muss. Um diese Bedingung zu
erfüllen, genügt es nicht, dass es sich um eine Referenz auf dasselbe Domino-Dokument handelt, sondern es muss sich um ein und dasselbe Objekt handeln. Andernfalls wird die NotesException „Document is not from this collection“ geworfen. Gibt es keine weiteren Dokumente in der DocumentCollection, gibt diese Methode null zurück. •
•
Document getPrevDocument() throws NotesException Document getPrevDocument(Document document) throws NotesException Arbeitet wie getNextDocument und gibt das jeweils vorige Dokument bzw. null zurück, falls es kein voriges Dokument gibt. Document getNthDocument(int pos) throws NotesException Gibt das Document an Position pos in der Reihenfolge der DocumentCollection zurück. Die Dokumente sind beginnend mit 1 nummeriert. Werden Doku-
mente zur Collection hinzugefügt oder entfernt, wird neu, fortlaufend und beginnend bei 1 nummeriert. Gibt es weniger als pos Dokumente in der Collection, wird null zurückgegeben. Diese Methode ist nicht performant und sollte nicht bei der Verwendung von Schleifen zum Einsatz kommen. •
10 View Die Methoden getNextDocument (Document), getPreviousDocument (Document) und deleteDocument (Document) erfordern, dass als Parameter ein und dieselben Objekte übergeben werden, die auch in der Collection gesammelt wurden. Anhand von getDocument (Document) kann als Parameter ein Dokument übergeben werden, das zwar durch ein anderes Java-Objekt repräsentiert wird, aber auf dasselbe Domino-Dokument verweist. Rückgabewert ist entweder null, falls das Dokument nicht in der Collection enthalten ist, oder das Objekt, wie es in der Collection geführt wird und das auf das gewünschte Domino-Dokument verweist.
•
void addDocument(Document document) throws NotesException void addDocument(Document document, boolean immediate) throws NotesException Fügt ein Document einer Collection hinzu. Wirft eine NotesException, falls das Document bereits in der Collection enthalten ist. Für DIIOP-Verbindungen kann über den Parameter immediate erzwungen werden, dass die Änderungen
sofort durchgeführt werden, so dass die Überprüfung auf doppelte Einträge sofort durchgeführt und die entsprechende NotesException geworfen werden kann. •
void deleteDocument(Document document) throws NotesException Entfernt ein Document aus einer Collection. Das Dokument selbst bleibt in der
Datenbank hiervon unberührt. Als Parameter dürfen nur Objekte aus der Collection übergeben werden, sonst wird eine NotesException geworfen. Siehe getNextDocument (Document) und getDocument(Document). •
Führt eine Volltextsuche auf der Collection aus und reduziert die Menge der in der Collection enthaltenen Dokumente auf die gefundenen Treffer. Nach FTSearch ist die DocumentCollection entsprechend dem internen SearchScore sortiert und isSorted liefert true. Der SearchScore eines Dokuments kann nach einer FTSearch durch getFTSearchScore() geladen werden. Enthält die Collection keine Dokumente, wirft diese Methode eine NotesException. Der Parameter query übergibt die Suchanfrage in derselben Syntax wie für FTSearch in View oder Database. Näheres siehe in Kapitel 15. Die Trefferzahl kann auf maxResults Dokumente eingeschränkt werden. Per Default werden maximal 5000 Dokumente gefunden. •
Eine gute Möglichkeit, Referenzen auf Dokumente in Domino zu speichern, sind Ordner. Über putAllInFolder werden für alle Dokumente der Collection Referenzen in einen Ordner mit Namen folderName gelegt. folderName kann, wie in Domino üblich, den Backslash enthalten, durch den veranlasst wird, dass Ordner in Ordnern erstellt werden (z.B. „Animals\Birds“ erzeugt den Ordner Birds im Ordner Animals). Ist createOnFail == true, wird ein neuer Ordner erzeugt, sofern er noch nicht existiert. Default ist true. Für createOnFail==false wird keine Aktion durchgeführt, wenn es den/die entsprechende(n) Ordner nicht gibt. •
Entfernt die Dokumente der Collection aus dem Ordner. Hierbei werden, wie für Ordner üblich, lediglich die Referenzen aus dem Ordner entfernt. Die eigentlichen Dokumente in der Datenbank bleiben unberührt. Befinden sich Dokumente der Collection nicht im Ordner, wird dies ignoriert. •
Löscht alle Dokumente der Collection endgültig aus der Datenbank. Über den Parameter enforce kann erzwungen werden, dass auch Dokumente gelöscht werden, die gerade in Bearbeitung sind. •
Für alle Dokumente der Collection wird der Inhalt des Items mit dem Namen itemName durch den Wert value ersetzt. Zulässige Klassen für das Objekt value sind Integer, Double, String, DateTime oder Vector aus Integer, Double, String oder DateTime. Dokumente müssen nach der Verwendung nicht ausdrücklich gespeichert werden. Die Änderungen durch stampAll werden implizit gespeichert. Bereits geladene Objektreferenzen auf derart geänderte Dokumente müssen neu geladen werden, um die Änderungen nachzuvollziehen. Änderungen an bereits geladenen Dokumenten müssen vor der Anwendung von stampAll gespeichert werden, um Speicherkonflikte zu vermeiden. •
void updateAll() throws NotesException
Diese Methode findet nur innerhalb von Domino-Agenten-Anwendung. Sie markiert alle Dokumente der Collection als verarbeitet, so dass sie nicht mehr durch getUnprocessedDocuments, unprocessedFTSearch oder unprocessedSearch in AgentContext geladen werden. Kann nicht für DIIOP-Verbindungen verwendet werden. •
•
boolean isSorted() throws NotesException Liefert true, falls die DocumentCollection mindestens ein Element enthält und als Ergebnis aus einer FTSearch entstanden ist, die z.B. auf die Datenbank oder die DocumentCollection selbst angewendet wurde. DateTime getUntilTime() throws NotesException
410
10 View Neu seit Domino R7. Wurde eine DocumentCollection über getModifiedDocuments in Database erzeugt, so gibt getUntilTime die aktuelle Datenbankzeit zu dem Zeitpunkt zurück, als getModifiedDocuments ausgeführt wurde. Somit kann für einen nachfolgenden Aufruf von getModifiedDocuments getUntilTime als DateTimeParameter für das Attribut „notOderThan“ verwendet werden (s. Kap. 7.1.4.3).
10.7.2 •
Methoden in ViewEntryCollection
int getCount() throws NotesException Liefert die Anzahl der Dokumente in der ViewEntryCollection, und zwar
ohne Einträge für Kategorien oder Summmen und 0, falls die Collection keine Elemente enthält. •
String getQuery() throws NotesException Gibt die Suchquery zurück, mittels derer eine ViewEntryCollection entstanden ist; z.B. wenn eine ViewEntryCollections über getAllEntries aus einem View gewonnen wird, der zuvor durch FTSearch gefiltert wurde oder nach Anwendung von FTSearch in ViewEntryCollection.
Falls keine Suche im eigentlichen Sinne oder ein Lookup ohne Suchquery zugrunde lag, wie z.B. bei getAllEntriesByKey, wird ein leerer String zurückgegeben. •
View getParent() throws NotesException
Gibt die Ansicht zurück, über die die Collection erstellt wurde. •
•
•
ViewEntry getFirstEntry() throws NotesException Die Klasse ViewEntryCollection verwaltet einen internen Cursor, der die Position innerhalb der Collection anzeigt. getFirstEntry gibt das erste ViewEntry der ViewEntryCollection zurück und setzt den Cursor auf die erste Position. Gibt null zurück, falls die ViewEntryCollection leer ist. ViewEntry getLastEntry() throws NotesException Gibt das letzte ViewEntry der ViewEntryCollection zurück und setzt den Cursor auf die letzte Position. Gibt null zurück, falls die ViewEntryCollection leer ist. ViewEntry getNextEntry() throws NotesException ViewEntry getNextEntry(ViewEntry viewentry) throws NotesException Gibt das nächste ViewEntry nach dem Cursor innerhalb der ViewEntryCollection entsprechend der Reihenfolge in der ViewEntryCollection zurück und setzt den Cursor auf diese Position. Wird als Parameter ein ViewEntry übergeben, so wird die Position nach diesem ViewEntry gewählt, wobei das ViewEntry aus der ViewEntryCollection stammen muss. Um diese Bedin-
gung zu erfüllen, genügt es nicht, dass es sich um eine Referenz auf das selbe Domino-Dokument handelt, sondern es muss sich um ein und dasselbe ViewEntry-Objekt handeln. Andernfalls wird die NotesException „Entry is not from this collection“ geworfen. Gibt es keine weiteren ViewEntry in der ViewEntryCollection, gibt diese Methode null zurück.
10.7.2 Methoden in ViewEntryCollection •
411
ViewEntry getPrevEntry() throws NotesException ViewEntry getPrevEntry(ViewEntry viewentry) throws NotesException Arbeitet wie getNextEntry und gibt das jeweils vorige ViewEntry bzw. null
zurück, falls es kein voriges Dokument gibt. •
ViewEntry getNthEntry(int pos) throws NotesException Gibt das ViewEntry an Position pos in der Reihenfolge der ViewEntryCollection zurück. Die ViewEntry sind beginnend mit 1 nummeriert. Werden ViewEntry zur Collection hinzugefügt oder entfernt, wird neu, fortlaufend und beginnend bei 1 nummeriert. Gibt es weniger als pos ViewEntry in der Collection, wird null zurückgegeben. Diese Methode ist nicht performant und sollte
•
ViewEntry getEntry(Object obj) throws NotesException Die Methoden getNextEntry (ViewEntry), getPreviousEntry (ViewEntry) und deleteEntry (ViewEntry) erfordern, dass als Parameter ein
nicht bei der Verwendung von Schleifen zum Einsatz kommen.
und dieselben Objekte übergeben werden, die auch in der Collection gesammelt wurden. Anhand von getEntry (Object) kann als Parameter ein ViewEntry oder Document übergeben werden, das zwar durch ein anderes Java-Objekt repräsentiert wird, aber auf dasselbe Domino-Dokument verweist. Rückgabewert ist entweder null, falls das ViewEntry bzw. Document nicht in der ViewEntryCollection enthalten ist, oder das Objekt, wie es in der Collection geführt wird und das auf das gewünschte Domino-Dokument verweist. •
void addEntry(Object obj) throws NotesException void addEntry(Object obj, boolean immediate) throws NotesException Fügt ein ViewEntry einer Collection hinzu. Wirft eine NotesException, falls das ViewEntry bereits in der Collection enthalten ist. Wird als Parameter ein Document übergeben, so wird dieses Document in ein ViewEntry gekapselt und der ViewEntryCollection hinzugefügt. Bemerkenswert ist, dass dieses neue ViewEntry das Document so repräsentiert, wie für die zugrunde liegende
Ansicht üblich. Folglich werden z.B. Spaltenwerte für dieses Dokument über getColumnValues berechnet. Allerdings können der ViewEntryCollection nur Dokumente hinzugefügt
werden, die auch in der zugrunde liegenden Ansicht enthalten sind. Andere Parameter werden ignoriert. Für DIIOP-Verbindungen kann über den Parameter immediate erzwungen werden, dass die Änderungen sofort durchgeführt werden, so dass die Überprüfung auf doppelte Einträge sofort durchgeführt werden und die entsprechende NotesException geworfen werden kann. •
void deleteEntry(ViewEntry viewentry) throws NotesException Entfernt ein ViewEntry aus einer Collection. Das über das ViewEntry referen-
zierte Dokument selbst bleibt in der Datenbank hiervon unberührt. Als Parameter dürfen nur Objekte aus der Collection übergeben werden, sonst wird eine NotesException geworfen. Siehe getNextEntry (ViewEntry) und getEntry(Object).
Führt eine Volltextsuche auf der Collection aus und reduziert die Menge der in der Collection enthaltenen ViewEntry auf die gefundenen Treffer. Nach FTSearch ist die ViewEntryCollection entsprechend dem internen SearchScore sortiert. Der SearchScore eines Dokuments kann nach einer FTSearch durch getFTSearchScore() geladen werden. Enthält die Collection keine ViewEntry, wirft diese Methode eine NotesException. Der Parameter query übergibt die Suchanfrage in derselben Syntax wie für FTSearch in View oder Database. Näheres siehe in Kapitel 8. Die Trefferzahl kann auf maxResults Dokumente eingeschränkt werden. Per Default werden maximal 5000 Dokumente gefunden. •
Eine gute Möglichkeit, Referenzen auf Dokumente in Domino zu speichern, sind Ordner. Über putAllInFolder werden für alle der über die ViewEntryCollection referenzierten Dokumente Referenzen in einen Ordner mit Namen folderName gelegt. folderName kann, wie in Domino üblich, den Backslash enthalten, durch den veranlasst wird, dass Ordner in Ordnern erstellt werden (z.B. „Animals\Birds“ erzeugt den Ordner Birds im Ordner Animals). Ist createOnFail == true, wird ein neuer Ordner erzeugt, sofern er noch nicht existiert. Default ist true. Für createOnFail==false wird keine Aktion durchgeführt. •
void removeAllFromFolder(String folderName) throws NotesException Entfernt die über die ViewEntryCollection referenzierten Dokumente aus
dem Ordner. Hierbei werden, wie für Ordner üblich, lediglich die Referenzen aus dem Ordner entfernt. Die eigentlichen Dokumente in der Datenbank bleiben unberührt. Befinden sich Dokumente der Collection nicht im Ordner, wird dies ignoriert. •
void removeAll(boolean enforce) throws NotesException Löscht alle über die ViewEntry der Collection referenzierten Dokumente endgültig aus der Datenbank. Über den Parameter enforce kann erzwungen wer-
den, dass auch Dokumente gelöscht werden, die gerade in Bearbeitung sind.
10.8 Schleifen – View, DocumentCollection und ViewEntryCollection •
413
void stampAll(String itemName, Object value) throws NotesException Für alle über die ViewEntry der Collection referenzierten Dokumente wird der Inhalt des Items mit dem Namen itemName durch den Wert value ersetzt. Zulässige Klassen für das Objekt value sind Integer, Double, String, DateTime oder Vector aus Integer, Double, String oder DateTime. Dokumen-
te müssen nach der Verwendung nicht ausdrücklich gespeichert werden. Die Änderungen durch stampAll werden implizit gespeichert. Bereits geladene Objektreferenzen auf derart geänderte Dokumente müssen neu geladen werden, um die Änderungen nachzuvollziehen. Änderungen an bereits geladenen Dokumenten müssen vor der Anwendung von stampAll gespeichert werden, um Speicherkonflikte zu vermeiden. •
void updateAll() throws NotesException
Diese Methode findet nur innerhalb von Domino-Agenten-Anwendung. Sie markiert alle über die ViewEntry der Collection referenzierten Dokumente als verarbeitet, so dass sie nicht mehr durch getUnprocessedDocuments, unprocessedFTSearch oder unprocessedSearch in AgentContext geladen werden. Kann nicht für DIIOP-Verbindungen verwendet werden.
10.8
Schleifen – View, DocumentCollection und ViewEntryCollection
Wurden Dokumentensammlungen über eine Suche, manuelle Operationen oder schlicht über die Definition einer Ansicht zusammengestellt, gilt es, diese Dokumente zu verarbeiten, sei es lesend oder schreibend. Typische Schleifen in DocumentCollection (Kapitel 10.4 und 10.7), ViewEntryCollection (Kapitel 10.5 und 10.7), ViewNavigator (Kapitel 10.5) und View (Kapitel 10ff.) werden mit den jeweiligen Navigationsfunktionen getFirstDocument oder getFirstEntry und getNextDocument oder getNextEntry usw. realisiert. Nicht immer ist es notwendig, überhaupt eine Schleife über die gesammelten Dokumente zu durchschreiten. Insbesondere die Methode stampAll in ViewEntryCollection und DocumentCollection erlauben es, für die gesamte Collection den Inhalt eines einzelnes Items für alle Dokumente permanent zu verändern. Sie sollte, sofern dies zu dem Anwendungsfall passt, immer bevorzugt werden. Ähnlich können mit putAllInFolder, removeAllFromFolder oder removeAll Operationen auf der gesamten Sammlung ausgeführt werden, ohne jedes einzelne Dokument öffnen zu müssen. Schwieriger wird es bei Operationen, die direkt auf einem View ausgeführt werden sollen. Im Gegensatz zu den drei Collections DocumentCollection, ViewEntryCollection und ViewNavigator besitzt die Ansicht, wie könnte es anders sein, eine SELECT-Formel, die programmatisch und dynamisch bestimmt, welche Elemente sie enthält.
414
10 View
Dementsprechend wird sich die Ansicht dynamisch verändern, wenn Dokumente hinzugefügt, gelöscht oder geändert werden. Da Domino-Bestandteile der Ansichten cached, insbesondere bei lokalen Sessions, werden diese Veränderungen allerdings nicht immer sofort im bereits geladenen View-Objekt sichtbar werden. Zwar gibt es die Möglichkeit, mit setAutoUpdate(true) den View für einen sofortiges Update zu aktivieren, aber auch diese Updates stehen nicht garantiert sofort zur Verfügung. Für viele Anwendungsfälle, wie z.B. einen Batchlauf, der periodisch noch nicht verarbeitete Dokumente in die Verarbeitung nimmt, wird es jedoch gar nicht notwendig sein, dynamische Veränderungen an der Ansicht sofort wahrzunehmen, oft wird dies sogar eher störend sein, da diese Veränderungen sich negativ oder zumindest zufällig, z.B. auf Abbruchbedingungen, auswirken können. Daher sei an dieser Stelle empfohlen, per setAutoUpdate(false) bewusst auf die Aktualisierung zu verzichten. Dies führt nicht nur zu vorhersehbaren Ergebnissen, sondern wirkt sich positiv auf die Performance aus. Nach der Verarbeitung kann die Ansicht mit refresh() manuell aktualisiert werden. Alternativ sollte in jedem Fall in Erwägung gezogen werden, ob nicht ohnehin mit einer DocumentCollection gearbeitet werden kann. Wenn (ausgewählte) Dokumente in einer Ansicht gelöscht werden sollen, indem diese in einer Schleife durchschritten wird, ist zu beachten, dass getNextDocument(Document) in View nur in der Signatur mit einem Document als Parameter zur Verfügung steht. Wird also die Schleifenvariable gelöscht, kann das nächste folgende Dokument nicht mehr ermittelt werden. Hierfür kann aber ein kleiner Trick angewendet werden, der ähnlich der Technik beim Recycling das zu löschende Dokument zwischenspeichert: nextDoc = view.getNextDocument(doc); doc.remove(true); GC.recycle(doc); doc = nextDoc;
Allerdings sollte soweit möglich überhaupt darauf verzichtet werden, die Dokumente über eine Schleife abzuarbeiten. Schließlich lassen sich mit removeAll(boolean) sämtliche Dokumente einer ViewEntryCollection löschen, die leicht aus der Gesamtmenge einer Ansicht über getAllEntries erzeugt werden kann. Wird also die Ansicht, bzw. eine FTSearch oder sonst eine Suche, wie z.B. getAllEntriesByKey, so definiert, dass sie exakt die zu löschenden Dokumente enthält, lassen sich diese über removeAll sehr viel effizienter löschen. Die einzelnen Techniken zur Schleifenverarbeitung werden in Listing 10-2 demonstriert. Die Klasse CollectionDemo erzeugt wie gewohnt eine Domino-Session. Durch eine kleine Abfrage wurde sie in die Lage versetzt, je nach Bedarf die Verbindung zur lokalen Maschine oder zu einem Server herzustellen. Dies macht es notwendig, das Beenden des NotesThread bei lokalen Aufrufen mandatorisch zu gestalten . Um für die Demonstration geeignete Dokumente in der Ansicht V_lookup_k6 der Beispieldatenbank djbuch/djbuch.nsf vorzufinden, erzeugt die Methode
10.8 Schleifen – View, DocumentCollection und ViewEntryCollection
10.8 Schleifen – View, DocumentCollection und ViewEntryCollection
417
if (query!=null) view.FTSearch(query); size=view.getAllEntries().getCount(); if (allInOne) { view.getAllEntries().removeAll(true); } doc = view.getFirstDocument(); while (doc != null) { System.out.println ("Dokument " + count++ + " / " + size); nextDoc = view.getNextDocument(doc); boolean someExtraCondition = doc.hasItem ("Item_0"); if (someExtraCondition) { doc.remove(true); } GC.recycle(doc); doc = nextDoc; } } finally { GC.recycle (doc); GC.recycle (nextDoc); GC.recycle(view); } return size; } private static final void dumpDoc (Document doc) throws NotesException { Item item = null; try { Vector allItems = doc.getItems(); for (int i = 0; i
Listing 10-2 Verarbeitung von Dokumenten in Schleifen
DemoTools.startup (Database db, int docCount) in der Datenbank db docCount Dokumente . Die einzelnen Methoden der Klasse zeigen verschiedene
Herangehensweisen an Dokumentensammlungen. Folgende Vorgehensweisen werden demonstriert: 1
Ausgabe aller Dokumente einer Datenbank über eine DocumentCollection
2 3
Ausgabe aller Dokumente einer Ansicht über das Objekt View Ausgabe einer reduzierten Menge von Dokumenten über FTSearch und View
418 4 5 6
10 View Individuelle Aktualisierung (eines Teils) der Dokumente einer Ansicht über eine Schleife über den View Aktualisieren (eines Teils) der Dokumente einer Ansicht über stampAll Löschen (eines Teils) der Dokumente einer Ansicht über removeAll oder über eine Schleife über View
Die Ausgabe aller Dokumente über eine DocumentCollection wird über getAllDocuments realisiert. Diese DocumentCollection aller Dokumente einer Datenbank wird über getFirstDocument und getNextDocument durchschritten, wobei das Recycling beachtet wird, indem zunächst über getNextDocument der Nachfolger bestimmt, dann das aktuelle Dokument recycelt und danach der Nachfolger ausgewählt wird. Die eigentliche Anzeige des Dokuments ist in der Methode dumpDoc gekapselt. Über eine Schleife aller Items des Dokuments im Vector getItems() werden diejenigen Items ausgegeben, die lediglich Text enthalten .72 Um eine Ansicht auszugeben , wird analog zu der DocumentCollection vorgegangen. In der Beispielmethode wird lediglich der Name der auszugebenden Ansicht übergeben, die dann durch die Methode geladen wird . In der Praxis kann es sinnvoll sein, eine Ansicht, die häufig benötigt wird, zentral zu laden und global zur Verfügung zu stellen, da Domino eine gewisse Zeit benötigt, um das Java-Objekt View zu erzeugen. Die Methode dumpView erlaubt es über den Parameter query, die Menge der im View geführten Dokumente über eine Volltextsuche einzuschränken, so dass diese Methode die beiden Anwendungsfälle (1) „Alle Dokumente einer Ansicht ausgeben“ und (3) „Eine Teilmenge der Dokumente einer Ansicht ausgeben“ verarbeiten kann. Beachten Sie, dass sich nach der Anwendung von FTSearch die Ansicht exakt so verhält, als ob die SELECT-Formel der Ansicht lediglich die eingeschränkte (gefilterte) Menge von Dokumenten ausgewählt hat. In der Verarbeitung gibt es keine Unterschiede, egal ob die Ansicht gefiltert wurde oder nicht. Allerdings muss nach einer Filterung über FTSearch beachtet werden, die Ansicht über clear() wieder in den Ursprungszustand zurückzuversetzen, soll sie später weiterverwendet werden. Da die Ansicht die Anzahl der verarbeiteten Dokumente zurückgeben soll, muss diese unter Zuhilfenahme der ViewEntryCollection getAllEntries() berechnet werden, da die Ansicht selbst hierfür keine Methode zur Verfügung stellt. Dies ist für die Demonstration hilfreich, um erkennen zu können wieviele Dokumente in den Anwendungsfällen verarbeitet wurden, sollte aber in der Praxis vermieden werden, da getAllEntries teuer im Hinblick auf die Performance ist. Die eigentliche Schleife läuft mit den üblichen Mitteln analog der Methode dumpDatabase. 72
Einzige Besonderheit dieser Methode ist, dass das Recycling der Items auf die lokale Ausführung beschränkt wird. Normalerweise soll zwar jedes Domino-Java-Objekt nach Gebrauch recycelt werden, aber offenbar gilt dies nicht für Items in IIOP-Sessions. Beim Recycling dieser Items scheint Domino das gesamte Dokument der Müllabfuhr zu übergeben (Stand Domino R6.5.3), so dass das Recycling von Items hier unterbleiben muss.
10.8 Schleifen – View, DocumentCollection und ViewEntryCollection
419
Wichtige Techniken in Schleifen Recycling – Wird eine Referenz auf ein Domino-Java-Objekt freigegeben, muss in jedem Fall zuvor ein recycle angewendet werden. Dies ist insbesondere in Schleifen der Fall. getNextDocument statt getNthDocument verwenden. stampAll bzw. removeAll verwenden, sofern möglich.
Die Definition des View und das Laden über getView ist eine teure Operation, die möglicherweise am besten in einer globalen Variable aufgehoben ist. Autorefresh über setAutoUpdate möglichst abschalten. refresh erst am Ende der Verarbeitung verwenden, sofern eine Batchverarbeitung durch-
geführt wird, die nicht auf die dynamische Aktualisierung der Ansicht während der Verarbeitung angewiesen ist. Andernfalls muss setAutoUpdate(true) gesetzt und für jede (!) Schleifeniteration ein refresh verwendet werden, um sicher sein zu können, dass das View-Objekt den aktuellen Stand abbildet. Letztere Variante wird nicht empfohlen und ist
nicht performant.
Die nächsten bei den Anwendungsfällen (4) „Individuelle Aktualisierung der Dokumente einer Ansicht“ und (5) „Globale Aktualisierung der Dokumente einer Ansicht“ werden in der Methode updateView demonstriert. Optional kann auch hier die Ansicht gefiltert werden. In den meisten Anwendungsfällen kann über setAutoUpdate die automatische Aktualisierung für dieses View-Objekt temporär ausgeschaltet werden. Hierdurch bleibt die Menge der Dokumente in der Ansicht während der Verarbeitung unverändert und es wird ein Performancegewinn erreicht. Muss für alle Dokumente der Ansicht lediglich ein Item auf einen bestimmten Wert gesetzt werden, so kann und sollte die Methode stampAll verwendet werden. Müssen individulle Werte für jedes einzelne Dokument gesetzt werden, so wird analog zu dumpView eine Schleife gebildet. Nach Beendigung der Updates sollte die Ansicht über refresh() aktualisiert werden. Auch beim Löschen von Dokumenten (Anwendungsfall (6)), kann dies gesammelt über removeAll erreicht werden, sofern tatsächlich alle Dokumente der (optional gefilterten) Ansicht gelöscht werden müssen. Sind vor dem Löschen noch zusätzliche Bedingungen zu erfüllen, so muss auch hier eine Schleife verwendet werden. Um über getNextDocument (Document) auch tatsächlich das nächste Dokument erreichen zu können, muss dieses vor dem Löschen des aktuellen Dokuments zwischengespeichert werden .
420
10 View
Nie wieder Ärger mit Recycling in Schleifen Durch die Kapselung der Schleifenverarbeitung und die Auslagerung des Schleifenrumpfes in eine separate Klasse lässt sich eine Schleife in Domino so abstrahieren, dass das gesamte, für die Schleife notwendige Recycling implizit ausgeführt wird. Durch diese erledigt die Schleifenverarbeitung ihr Recycling selbst und es kann nie wieder vergessen werden.
Auch für die Löschschleife sollte das Abschalten der Aktualisierung per setAutoUpdate und ein nachträglicher Refresh je nach Anwendungsfall in Erwägung gezogen werden.
10.9
Kapselung von Schleifen
Wie im vorigen Kapitel gezeigt wurde, tragen Schleifen in DocumentCollection, aber auch in View und Database durch die Notwendigkeit des Recyclings immer ExtraCode mit sich, der nicht mehr benötigte Schleifendokumente recycelt. Um sich der Notwendigkeit zu entledigen, sich um dies kümmern zu müssen, kann der hierfür notwendige Code gekapselt werden. Erreicht wird dies, indem zunächst die eigentliche Businesslogik des Schleifenrumpfes in eine eigene Klasse ausgelagert wird, gewissermaßen eine Kommandoklasse, die das innerhalb der Schleife auszuführende Kommando enthält. Diese muss eine Basisklasse implementieren, um universell verwendbar zu werden und eine Execute-Methode besitzen, die den Code der Businesslogik auslöst. Zusätzlich wird es eine Klasse geben, die für die Ausführung der Schleife und das Recycling sorgt und während der Ausführung für jedes Durchschreiten der Schleife die Execute-Methode ausführt. Sind die benötigten Klassen erst einmal vorbereitet, wird die Schleife plötzlich sehr einfach und transparent in der Ausführung. In der Umsetzung wurde anstelle eines Interfaces eine abstrakte Klasse als Basis gewählt, so dass diese bereits alle benötigten Basisfunktionen der Kommandoklasse mitbringen kann. Die eigentliche Ausführung reduziert sich dann auf folgenden einfachen Code: DJLoopCommandDumpDoc command = new DJLoopCommandDumpDoc (view); DJLoopExecutor exec = new DJLoopExecutor (command); exec.doLoop();
Die Variable command enthält den in der Schleife auszuführenden Code. Der DJLoopExecutor exec kümmert sich um die Schleife und triggert für jede Schleifeniteration die Methode command.execute an, in der die Businesslogik definiert ist. Sämtliche innerhalb von DJLoopExecutor benötigten Domino-Objekte werden dort auch recycelt, so dass der Benutzer sich hierum nicht mehr kümmern muss. Die Ansicht view als externe Variable, die von außen der Variable command hinzugefügt
10.9 Kapselung von Schleifen
421
Abb. 10-4 Klassendiagramm DJLoopCommand und DJLoopExecutor
wird, bleibt allerdings recyclingpflichtig. Sie ist die Basis der Schleife, die die gegebene Ansicht durchschreitet und für jede Iteration die in command definierte Aktion ausführt. Die Klasse DJLoopCommandDumpDoc erweitert die abstrakte Basisklasse DJLoopCommand. Diese Klasse ist so allgemein angelegt, dass eine Erweiterung hiervon zusammen mit der Klasse DJLoopExecutor flexibel genug ist, mit einer Database, einem View oder einer DocumentCollection instanziert zu werden. Listing 10-3 nutzt diese Technik und ist eine überarbeitete Version der gleichnamigen Methode dumpView aus Listing 10-2. Sämtlicher Code, der innerhalb der Schleife ausgeführt wurde, wird in die Klasse DJLoopCommandDumpDoc aufgenommen. Diese Klasse muss die Klasse DJLoopCommand (Listing 10-4) erweitern. Sie wird in diesem Beispiel mit einer Ansicht instanziert, wobei diese aus Sicht von DJLoopCommandDumpDoc extern ist, also recyclingpflichtig bleibt. Lesen Sie hierzu auch Kapitel 14.2, § 6. Eine Übersicht der fertigen Demoanwendung finden Sie in Abb. 10-4, die den Zusammenhang zwischen der abstrakten Basisklasse DJLoopCommand, DJLoopCommandDumpDoc und DJLoopExecutor aufzeigt. Ist das DJLoopCommand-Objekt erst einmal auscodiert, muss es nur noch in einen DJLoopExecutor gesteckt und die Schleife durch doLoop() ausgeführt werden. Der gesamte Überhang aus doc, nextDoc, aber auch size oder count konnte in DJLoopCommandDumpDoc gekapselt werden. Listing 10-4 zeigt, wie einfach die Umsetzung durch die vorbereitete Basisklasse DJLoopCommand ist. Die Businesslogik wird in die Klasse execute gesteckt, die die einzige Methode ist, die zwingend (neben dem Konstruktor) benötigt wird. Da die ursprüngliche Busi-
422
10 View
private static final int dumpView (Database db, String viewName, String query) throws NotesException { View view = null; try { view = db.getView(viewName); if (view==null) return -1; if (query!=null) view.FTSearch(query); view = db.getView ("V_Dokumente_k6"); DJLoopCommandDumpDoc command = new DJLoopCommandDumpDoc (view); DJLoopExecutor exec = new DJLoopExecutor (command); exec.doLoop(); return command.getSize(); } finally { GC.recycle(view); } }
Listing 10-3 Gekapselte Schleife
nesslogik aus Listing 10-2 verlangte, dass ein Counter und die Maximalgröße angezeigt werden, wurden diese Werte im Konstruktor und durch die Methode getSize() bereitgestellt. Hier wird auch schon die Flexibilität dieses Design Patterns deutlich: Während die Logik der Schleifenverarbeitung in DJLoopExecutor und DJLoopCommand gekapselt ist, kann sich in der eigenen Erweiterung des Schleifenrumpfes die Entwicklung auf die eigentliche Businesslogik konzentrieren. Um die Flexibilität noch weiter zu erhöhen, sieht die Ausarbeitung von DJLoopExecutor vor, dass die Verarbeitung der Schleife abhängig von einer Bedingung unterbrochen werden darf. Hierfür ist für die Methode execute der Rückgabewert boolean definiert. Somit ist es möglich, abhängig von der aktuellen Schleifenvariable (dies ist übrigens der Parameter doc der Methode execute) optional false als Ergebnis von execute zurückzugeben, um die Verarbeitung zu stoppen. Im aktuellen Beispiel wird diese Möglichkeit nicht genutzt und immer true zurückgegeben. public class DJLoopCommandDumpDoc extends DJLoopCommand { private final int size; private int count=0; public DJLoopCommandDumpDoc(View view) throws NotesException { super(view); size = view.getAllEntries().getCount(); } public boolean execute(Document doc) throws NotesException { System.out.println ("Dokument " + count++ + " / " + size); DemoTools.dumpDoc(doc); return true; } public int getSize() { return size; } }
Einen Einblick in die Verarbeitung durch DJLoopExecutor bietet Listing 10-5. Diese Klasse entscheidet anhand von getType in DJLoopCommand, ob die Basis der Schleife eine DocumentCollection, eine ganze Datenbank (dies wird über getAllDocuments() in Database realisiert) oder eine Ansicht ist. Für die Verarbeitung einer Schleife über eine Ansicht besitzt die Klasse die private Methode doLoopView und dementsprechend für die Verarbeitung einer DocumentCollection die Methode doLoopCollection. Beide Methoden implementieren die eigentliche Schleife und dort finden sich auch wieder die gewohnten Codeelemente, bestehend aus der While-Schleife , den beiden Dokumenten doc und nextDoc , die außerhalb des Gültigkeitsbereiches der Schleife definiert werden (müssen), um später wieder recycelt werden zu können und dem „Durchschalten“ zwischen doc und nextDoc und dem zughörigen Schleifen-Recycling . Die Referenz auf die Businesslogik wird erreicht, indem in dieser Schleife die execute-Methode des Kommandos command aufgerufen wird, mit dem die Klasse DJLoopExecutor instanziert wurde. Hier findet auch die Abfrage statt, durch die ein konditionales Abbrechen der Schleife erreicht werden kann. Somit sind alle Bestandteile einer Schleife gekapselt und Sie müssen sich nie wieder Gedanken um das Recycling innerhalb von Schleifen machen!
10.10
Zusammenfassung
Während der View in Domino zuständig ist für die selektive Darstellung von Dokumenten, werden Views wiederum durch die ViewEntryCollection, den ViewNavigator und das ViewEntry handhabbar gemacht. Parallel dazu steht die DocumentCollection als – entgegen der ViewEntryCollection – unsortierte Menge von Dokumenten zur Verfügung.
424
10 View
Mit diesen Objekten und ihren API-Funktionen können Ansichten beherrscht werden bis hin zur programmatischen Steuerung von Kategorien und Gestaltungsobjekten. Während in diesem Kapitel der Schwerpunkt auf den API-Funktionen lag, wird in Kapitel 15 erneut auf Ansichten und Collections eingegangen, dort jedoch mit dem Schwerpunkt auf die Durchsuchbarkeit von Datenbanken und Ansichten.
11
11 RichText
In diesem Kapitel: RichText API RichText Framework – RichTextItem, RichTextNavigator, RichTextRange, RichTextSection, RichTextTable, RichTextTab, RichTextDocLink, RichTextParagraphStyle, RichTextStyle Daten in Domino-Tabelle umwandeln Domino-Tabelle exportieren
426
11 RichText
RichText wird in Domino verwendet, um gestalteten Text aufzunehmen. Als Basisspeicherobjekt der Textverarbeitung in Domino (s. Kap. 2.2.4.8) wird er über eine Sammlung von Java-Klassen bearbeitet und kann komplexe Strukturen und Styles aufnehmen. Hierzu gehören Text, gestalteter Text, Styles zur Einbindung von Schriften, Schrift- und Textstilen, Tabellen, eingebettete OLE-Objekte, Bilder und Dateien (s. Kap. 7.4), Seiten- und Paragraphenwechsel. Ebenso nimmt RichText die für Domino typischen Abschnitte (s. Kap. 3.2.7) auf, in denen ganze Textblöcke in einer Art Kapitel zusammengefasst und über ein Twistie gesteuert auf- und zugeklappt werden können. Abschnitte können zusätzlich mit speziellen Benutzerrechten ausgestattet werden.
11.1
Arbeiten mit RichText
Erst seit Domino R6 gibt es ein ausführliches Framework, mit dem die meisten der in einem RichTextItem gespeicherten Elemente gelesen und meist auch geschrieben werden können. Dieses Framework besteht aus den Klassen RichTextItem, RichTextNavigator, RichTextParagraphStyle, RichTextRange, RichTextSection, RichTextStyle, RichTextTab, RichTextTable und RichTextDoclink, wobei das RichTextItem das eigentliche Speicherobjekt darstellt, in dem innerhalb des Document die Richtext-Daten gespeichert werden und das daher die Klasse Item erweitert, also grundsätzlich über die dort bekannten Methoden verfügt. Ein RichTextItem kann als Container weiterer Elemente aufgefasst werden. Diese Elemente werden als oberstes Strukturierungsmerkmal durch Paragraphen unterteilt und können von den Typen sein, die durch die Konstanten RTELEM_TYPE_TABLE, RTELEM_TYPE_TEXTRUN, RTELEM_TYPE_TEXTPARAGRAPH, RTELEM_TYPE_DOCLINK, RTELEM_TYPE_SECTION, RTELEM_TYPE_TABLECELL, RTELEM_TYPE_FILEATTACHMENT, RTELEM_TYPE_OLE, RTELEM_TYPE_TEXTPOSITION und RTELEM_TYPE_TEXTSTRING in RichTextItem repräsentiert werden.
Die Bedeutung der einzelnen Konstanten und deren Typus zeigt Tabelle 11-1. Die einzelnen Elemente werden über die Methode findFirstElement(int type) in RichTextNavigator gefunden, wobei type eine der obigen Konstanten annimmt. Ein RichTextNavigator kann aus einem RichTextItem über createNavigator in RichTextItem erzeugt werden. Eine Übersicht über alle Klassen des RichText-Frameworks und ihre Funktion finden Sie in Tabelle 11-2. Das Konzept des Richtextes durchdringt Domino sehr tief. So wird z.B. das Layout einer Maske in einem RichTextItem gespeichert; schließlich stehen in einer Maske dieselben Elemente zur Verfügung, die auch sonst im RichText zur Verfügung stehen, sieht man von der Definition einiger maskenspezifischer Elemente, wie Felder oder Layoutebenen, ab. Ein Dokument, das mit einer Maske dargestellt wird, kann als Gesamtheit in ein RichTextItem gerendert werden und wird dort abgebildet, wie es als Gesamtheit aus Maske und Dokumentendaten dargestellt würde. Ebenso kann ein einzelnes RichTextItem in ein solches eingefügt werden.
11.1 Arbeiten mit RichText
427
Tabelle 11-1 Verschiedene Elemententypen in RichTextItem Integerkonstante in RichTextItem Wert
Eine Tabelle im RichTextItem. Sie wird durch die Klasse RichTextTable abgebildet und kann über appendTable in RichTextItem erzeugt werden. Die Elemente der Tabelle können über einen RichTextNavigator referenziert werden. Laufender Fließtext im RichTextItem. Ein Absatz in einem RichTextItem. Domino kennt eigene Links, die auf Datenbanken, Ansichten, Dokumente oder Anchor innerhalb von Dokumenten verweisen können. Dieses Element wird durch die Klasse RichTextDoclink abgebildet. Ein Abschnitt im RichTextItem. Er wird durch die Klasse RichTextSection abgebildet. Eine Zelle einer Tabelle. Ein Dateianhang (s. Kap. 6.2.7). Ein OLE-Attachment (s. Kap. 6.2.7). Ein Element von diesem Typ ist kein eigentliches Element, sondern ein Offset. Ein Element von diesem Typ ist kein eigentliches Element sondern repräsentiert die Textposition des durch eine Suche innerhalb eines RichTextItems gefundenen Textes.
In der GUI-Programmierung ist für RichTextItems zu beachten, dass diese nicht aus der Backendrepräsentation im Document neu geladen werden, wenn diese dort geändert wird. Daher ist es notwendig, wenn z.B. ein Java-Agent bestehende Dokumente verändert, diese zu schließen und erneut zu öffnen, sofern sie gerade von einem Benutzer geöffnet sind. RichText kennt in Domino zwei Zustände. Er kann entweder als generischer Domino-RichText gespeichert sein oder als MIME Entity. Diese Doppelung wurde in Domino eingeführt, als auch das Internet und insbesondere die MIME E-Mail Einzug in Domino erhielt. Seit Domino R5.0.2 steht die Klasse MIMEEntity zur Verfügung, die aber erst seit Version 6 reichlich Methoden und die Klasse MIMEHeader kennt, um MIME Entities zu manipulieren. Ein MIME Entity Item kann abhängige MIME Entities haben, wodurch es selbst zum Parent und das abhängige Item zum Child wird. Hierdurch werden Multipart Messages abgebildet. MIME Entities können dazu genutzt werden, RichText in HTML umzuwandeln, wobei diese Umwandlung einigen Einschränkungen unterliegt. Hierzu muss in der Maske das Feld, in das ein Benutzer seinen RichText eingibt, mit der Eigenschaft „Store contents as HTML and MIME“ versehen werden. Ein Dokument, das mindes-
Repräsentiert das Item vom Typ RichTextItem im Domino-Document Stellt einen Navigator dar, der es ermöglicht die einzelnen Elemente eines RichTextItems anzusteuern, um diese zu bearbeiten. Stellt grundlegende Layoutinformationen für einen Paragraphen zur Verfügung, im wesentlichen für Textausrichtung, Zeilen- und Randabstände, Verhalten beim Seitenwechsel und Tabulatoren. Definiert einen Bereich von Elementen innerhalb eines RichText Repräsentiert einen Abschnitt eines RichTextes Stellt grundlegende Schriftstilinformationen, wie Fettdruck oder Unterstreichungen zur Verfügung und wird jeweils einleitend bei einem Wechsel dieser Stile innerhalb von Fließtext verwendet. Ein Tabulator Repräsentiert eine Tabelle und stellt Methoden zum Anfügen von Zeilen oder der Manipulation von Stilinformationen von Zellen zur Verfügung. Repräsentiert einen Link
tens ein MIME Entity Item enthält, wird zusätzlich durch das Item $NoteHasNativeMIME mit dem Wert "1" markiert. Enthält ein Dokument mehrere MIME Entities, wird hierdurch ein Multipart MIME Entity dargestellt. Domino nutzt die MIME Entity Items, um z.B. erhaltene HTML-E-Mails abzuspeichern. Eine einfache Möglichkeit RichText in HTML umzuwandeln, z.B. durch eine Methode, gibt es Stand heute leider nicht. Als Workaround kann in Erwägung gezogen werden, dass Domino durchaus in der Lage ist, diese Konvertierung on the fly durchzuführen. Domino erledigt diese Aufgabe nämlich ganz selbstverständlich bei der Anzeige von RichText im Browser. Dies kann ausgenutzt werden, indem HTTP-Anfragen von einer Java-Anwendung an Domino gestellt werden. Sie benötigen hierfür • •
•
eine Maske, die lediglich das RichTextItem anzeigt; eine Ansicht, die Lookups erlaubt und hierfür eindeutige Schlüssel der zu konvertierenden Dokumente – z.B. die Universal ID – in der ersten Spalte sortiert anzeigt und außerdem eine FormFormula enthält, die dafür sorgt, dass durch die Ansicht aufgerufene Dokumente auch in dieser Maske dargestellt werden; und eine Java-Methode, die in der Lage ist HTTP-Aufrufe durchzuführen und das Ergebnis z.B. als StringBuffer zurückgibt.
11.2 RichTextItem
429
Ihre Dokumente können Sie dann über URLs der Art http://www.InternerDomainnameDesServers.de/optionalerPfad/dominoDatenbank.nsf/LookupView/ UniversalIDDesDokuments von Ihrer Java-HTTP-Methode abholen lassen. Bedenken Sie hierbei unbedingt, dass z.B. für Domino-Web-Anwendungen zusätzliche HTTP Threads benötigt werden und Sie hierdurch im schlimmsten Fall die HTTP-Zugriffe verdoppeln. Außerdem sollten sie in Ihrer Java-HTTP-Methode den im HTTP-Protokoll vorgesehenen so genannten UserAgent (und eventuell den Referer) sinnvoll setzen, um in den Access Logs (für die Statistiker unter Ihnen) diese technischen Aufrufe von echten HTTP-Aufrufen unterscheiden zu können. Schließlich werden Sie über Umsetzung von SSL und automatisierte HTTP Logins für diese Aufrufe nachdenken müssen, wenn Ihre Domino-Datenbank dies erfordert.
11.2
RichTextItem
Ein RichTextItem ist die Basis des RichText Frameworks und erweitert Item. Aus ihm kann über createNavigator ein RichTextNavigator und über createRange ein RichTextRange erstellt werden, um in den Elementen zu navigieren. RichTextItem ist die älteste Klasse, um RichText abzubilden und kennt daher selbst einige Methoden, insbesondere um Elemente an den RichText anzuhängen.
11.2.1 •
Methoden in RichTextItem
void addNewLine() throws NotesException void addNewLine(int lineCount) throws NotesException void addNewLine(int lineCount, boolean newParagraph) throws NotesException Fügt dem RichText einen oder mehrere (lineCount) Zeilenwechsel hinzu. Nor-
malerweise wird durch einen Zeilenwechsel auch ein Paragraphenwechsel eingeleitet. Dies kann durch Setzen von newParagraph==false unterbunden werden. In der Darstellung ist ein Zeilenwechsel mit oder ohne Paragraphenwechsel nicht zu unterscheiden. Die Speicherstruktur des RichTextes sieht vor, dass ein Paragraph maximal 32 KByte groß sein darf (ohne eventuell enthaltene Bilder oder sonstige Attachments). Dies muss beim Schreiben von RichTextItems berücksichtigt werden. Ingesamt gibt es für RichTextItems keine Größenbeschränkung. Per Default fügt Domino dem RichText Elemente am Ende des RichTextItems hinzu. Der Einfügepunkt kann durch die Methoden beginInsert und endInsert in RichTextItem verändert werden. •
11 RichText Fügt ein oder mehrere (tabCount) Tabulatorzeichen an den RichText an. Per Default fügt Domino dem RichText Elemente am Ende des RichTextItems hinzu. Der Einfügepunkt kann durch die Methoden beginInsert und endInsert in RichTextItem verändert werden.
•
void addPageBreak() throws NotesException void addPageBreak(RichTextParagraphStyle richtextparagraphstyle) throws NotesException Fügt dem RichTextItem einen Seitenwechsel hinzu. Durch den optionalen Parameter richtextparagraphstyle kann der ParagraphStyle festgelegt wer-
den, mit dem der erste Paragraph der neuen Seite begonnen werden soll. Gleichbedeutend mit dem Setzen des ParagraphStyle durch appendParagraphStyle. Ein RichTextParagraphStyle kann durch createRichTextParagraphStyle in Session erzeugt werden. Per Default fügt Domino dem RichText Elemente am Ende des RichTextItems hinzu. Der Einfügepunkt kann durch die Methoden beginInsert und endInsert in RichTextItem verändert werden. •
void appendDocLink(Document linkedDocument) throws NotesException void appendDocLink(Document linkedDocument, String mouseOverComment) throws NotesException void appendDocLink(Document linkedDocument, String mouseOverComment, String linkText) throws NotesException void appendDocLink(Database linkedDatabase) throws NotesException void appendDocLink(Database linkedDatabase, String mouseOverComment) throws NotesException void appendDocLink(Database linkedDatabase, String mouseOverComment, String linkText) throws NotesException void appendDocLink(View linkedView) throws NotesException void appendDocLink(View linkedView, String mouseOverComment) throws NotesException void appendDocLink(View linkedView, String mouseOverComment, String linkText) throws NotesException Erzeugt einen Link im RichTextItem zu einem Dokument, einer Datenbank oder einer Ansicht. Ein entsprechendes Document-, Database- oder View-
Objekt wird beim Erstellen als erster Parameter übergeben. Der optionale Parameter mouseOverComment lässt in der Statuszeile am unteren Rand des Fensters diesen Hinweistext erscheinen, wenn mit der Maus (in der Benutzung im Notes Client) auf den erstellten Link gezeigt wird. Dies funktioniert allerdings nur, wenn das Dokument sich nicht im Bearbeiten-Modus befindet. Wird der optionale Parameter linkText verwendet, so wird statt einem Symbol für das Dokument, die Datenbank oder die Ansicht dieser Text angezeigt und verlinkt.
11.2.1 Methoden in RichTextItem
431
Per Default fügt Domino dem RichText Elemente am Ende des RichTextItems hinzu. Der Einfügepunkt kann durch die Methoden beginInsert und endInsert in RichTextItem verändert werden. •
void appendRTItem(RichTextItem richtextitem) throws NotesException Fügt ein RichTextItem in ein anderes ein. Ein RichTextItem kann nicht in
sich selbst eingefügt werden. Per Default fügt Domino dem RichText Elemente am Ende des RichTextItems hinzu. Der Einfügepunkt kann durch die Methoden beginInsert und endInsert in RichTextItem verändert werden. •
void appendStyle(RichTextStyle richtextstyle) throws NotesException Fügt einen RichTextStyle in das RichTextItem ein. Dieser kann durch createRichTextStyle in Session erzeugt werden. Er enthält Schriftstilin-
formationen. Per Default fügt Domino dem RichText Elemente am Ende des RichTextItems hinzu. Der Einfügepunkt kann durch die Methoden beginInsert und endInsert in RichTextItem verändert werden. •
void appendParagraphStyle(RichTextParagraphStyle richtextparagraphstyle) throws NotesException Fügt einen RichTextParagraphStyle in das RichTextItem ein. Durch die-
sen können Layoutinformationen wie z.B. Zeilenabstände für den nachfolgenden Paragraphen manipuliert werden. Per Default fügt Domino dem RichText Elemente am Ende des RichTextItems hinzu. Der Einfügepunkt kann durch die Methoden beginInsert und endInsert in RichTextItem verändert werden. •
void appendText(String text) throws NotesException Fügt Text an das RichTextItem an. Wurde zuvor ein RichTextParagraphStyle oder ein RichTextStyle eingefügt, wird der Text entsprechend
den dort gespeicherten Informationen angezeigt. Per Default fügt Domino dem RichText Elemente am Ende des RichTextItems hinzu. Der Einfügepunkt kann durch die Methoden beginInsert und endInsert in RichTextItem verändert werden. •
Fügt ein Objekt, z.B. eine Datei, in den RichText ein bzw. gibt entsprechende Objekte zurück (s. Kap. 7.4). Per Default fügt Domino dem RichText Elemente am Ende des RichTextItems hinzu. Der Einfügepunkt kann durch die Methoden beginInsert und endInsert in RichTextItem verändert werden. •
String getFormattedText(boolean useBlanksNotTabs, int wrapLineAt, int maxLen) throws NotesException
432
11 RichText Wandelt den Inhalt des RichTextItems in einen String um. Hierbei gehen alle Layout- und Stilinformationen verloren. Eingebettete Objekte werden ignoriert, aus Tabellen wird lediglich der enthaltene Fließtext übertragen. Die Maximalzahl der zurückgegebenen Zeichen kann durch maxLen limitiert werden, ist aber grundsätzlich auf maximal 32000 Zeichen beschränkt. Im Richtext befindliche Tabulatoren können durch useBlanksNotTabs==true durch Leerzeichen ersetzt werden. Zeilenumbrüche auf einer festgelegten Breite können durch wrapLineAt erzwungen werden.
• •
String getUnformattedText() throws NotesException Wandelt den Inhalt des RichTextItems in einen unformatierten String um. void appendTable(int lines, int columns) throws NotesException void appendTable(int lines, int columns, Vector labels) throws NotesException void appendTable(int lines, int columns, Vector labels, int marginLeft, Vector columnStyles) throws NotesException Neu seit Domino R6.5. Fügt eine Tabelle mit lines Zeilen und columns Spal-
ten ein73. Wird der optionale Parameter labels verwendet, so wird eine Tabelle mit der Eigenschaft „Show only one row at a time“ erstellt. Eine Tabelle mit dieser Eigenschaft wird mit Karteikartenreitern angezeigt, wobei jede Zeile einen eigenen Karteikartenreiter erzeugt. Anders gesprochen entsteht eine Tabelle, die, anstatt mehrere Zeilen zu zeigen, mehrere Tabellenreiter hat. Der Parameter labels muss exakt dieselbe Anzahl von Elementen haben wie die Tabelle Zeilen (Parameter lines)74. Zusätzlich kann der Abstand der gesamten Tabelle vom linken Rand durch marginLeft festgelegt werden. Dieser sollte als Vielfaches von RichTextParagraphStyle.RULER_ONE_CENTIMETER oder RichTextParagraphStyle.RULER_ONE_INCH übergeben werden, wobei diese Konstanten auch mit nicht ganzzahligen Werten multipliziert und danach nach int konvertiert werden können. Der Parameter columnStyles muss exakt75 dieselbe Anzahl von Elementen enthalten, wie die Tabelle Spalten hat, und Objekte der Klasse RichTextParagraphStyle enthalten. Diese RichTextPargraphStyle-Objekte bestimmen die Darstellung jeder Spalte. Alternativ kann null als Parameter übergeben werden. Wird als marginLeft ein Wert kleiner als RichTextParagraphStyle. RULER_ONE_INCH verwendet, so wird die Tabelle nach links verschoben erscheinen, da Domino einen Default-Offset von einem Inch in Masken vorsieht. 73
74 75
In einer Testumgebung unter Domino R6.5.3 unter Windows wurde vereinzelt beobach-
tet, dass bei der Verwendung von DIIOP nur Tabellen mit zwei Spalten erzeugt wurden. Andere Werte können zu Abstürzen führen. Andere Werte können zu Abstürzen führen.
11.2.1 Methoden in RichTextItem •
433
void beginInsert(Base nodeElement) throws NotesException void beginInsert(Base nodeElement, boolean insertAfter) throws NotesException void endInsert() throws NotesException Neu seit Domino R6.5. Mit beginInsert und endInsert wird die Möglich-
keit eröffnet, nicht nur am Ende eines RichTextItems Elemente anzufügen. Hierfür wird ein Einfügevorgang mit beginInsert eröffnet und durch endInsert wieder abgeschlossen. Der Einfügevorgang muss immer mit endInsert abgeschlossen werden. Der Parameter nodeElement gibt den Punkt an, vor dem mit dem Einfügen begonnen werden soll. Optional kann bei insertAfter==true auch nach dem nodeElement eingefügt werden. Nachfolgende Operationen, die dem RichTextItem Elemente hinzufügen, wie z.B. appendTable, werden an der duch beginInsert festgelegten Stelle statt am Ende des RichTextItems durchgeführt. Zulässige Objekte für nodeElemente sind Domino-Objekte der Klassen EmbeddedObject, RichTextDocLink,RichTextRange, RichTextSection oder RichTextTable, für die der Einfügepunkt an der Stelle dieser Elemente im RichText begonnen wird, und RichTextNavigator, für das die Einfügeoperation am Cursor dieses Navigators innerhalb des RichTextItems begonnen wird. Für beide Fälle muss beachtet werden, dass die Elemente sich auf das aktuelle RichTextItem beziehen. Es muss unbedingt beachtet werden, dass die Methoden beginInsert und endInsert nur in dieser Reihenfolge und immer paarweise angewendet werden. •
RichTextNavigator createNavigator() throws NotesException Neu seit Domino R6.5. Erzeugt einen RichTextNavigator (s. Kap. 11.3), mit
•
RichTextRange createRange() throws NotesException Neu seit Domino R6.5. Erzeugt ein Objekt der Klasse RichTextRange (s. Kap. 11.4). RichTextRange wird dazu verwendet, bestimmte Teile eines RichTextI-
dem durch die Elemente des RichTextItems navigiert werden kann.
tems abzubilden. Der RichTextRange ist zunächst leer. Sein Umfang kann durch setBegin und setEnd festgelegt werden. Durch getNavigator in RichTextRange kann der so erzeugte Teilabschnitt eines RichTextItem wiederum in einen RichTextNavigator abgebildet werden, so dass mit dieser Teilmenge gearbeitet werden kann, wie das auch mit dem gesamten RichTextItem möglich ist. •
int getNotesFont(String fontName, boolean createConstantOnFail) throws NotesException
Neu seit Domino R6.5. Fonts werden in Domino durch Integerkonstanten verwaltet. getNotesFont liefert die Konstante zu einem gegebenen Fontnamen fontName oder erzeugt einen neuen Wert im Range von 5 bis 254, falls createConstantOnFail==true ist.
434
11 RichText Vorgegebene Konstanten sind RichTextStyle.FONT_ROMAN, RichTextStyle.FONT_HELV und RichTextStyle.FONT_COURIER für die Standardfonts und RichTextStyle.STYLE_NO_CHANGE um die Beibehaltung des aktuellen Fonts zu kennzeichnen. Diese Konstanten werden in RichTextStyle in der Methode setFont (int) verwendet.
Neu seit Domino R6.5. Erzeugt einen neuen, leeren Abschnitt (s. Kap. 3.2.7) im RichTextItem. Ein Abschnitt wird durch beginSection erstellt, durch nachfolgende Operationen zum Anfügen von Elementen gefüllt und muss mit endSection abgeschlossen werden. Der Parameter title gibt die Überschrift an, die in der Titelleiste des Abschnittes angezeigt werden soll, titleStyle legt die Schriftstilinformationen für diese Titelleiste fest, colorOfTwisitie die Farbe des Twisties76 links der Titelleiste und schließlich wird durch expand angegeben, ob der neue Abschnitt initial geöffnet dargestellt wird. •
void endSection() throws NotesException
Neu seit Domino R6.5. Schließt die Erstellung eines Abschnittes ab. •
void update() throws NotesException
Neu seit Domino R6.5. Die Manipulation von Elementen in einem RichTextItem werden nicht synchron ausgeführt und sind performanceoptimiert. Um sicherzustellen, dass alle Operationen abgeschlossen sind, kann die endgültige Verarbeitung mit update angefordert werden. •
void compact() throws NotesException Neu seit Domino R6.5. Der für das RichTextItem verwendete Speicher in der
NSF-Datei kann aufgrund der unterschiedlichen Typen und Größen der möglichen Elemente fragmentieren. compact defragmentiert den für das RichTextItem verwendeten Speicher. Ein paar typische Operationen zeigt das folgende Codefragment: rti = doc.createRichTextItem("body"); //Text, NewLines and Tabs einfuegen rti.appendText("1" + DUMMY_TEXT); rti.addNewLine(1); rti.appendText("2" + DUMMY_TEXT); rti.addNewLine(1,false); rti.addTab(2); rti.appendText("3" + DUMMY_TEXT); rti.addNewLine(1); rtstyle = session.createRichTextStyle(); rtstyle.setBold(1); 76
Als Twistie wird in Domino ein kleines dreieckiges Symbol bezeichnet, mit dem Abschnitte, aber auch z.B. Kategorien auf- und zugeklappt werden können.
11.3 RichTextNavigator
435
rti.appendStyle(rtstyle); rti.appendText("4" + DUMMY_TEXT); rtstyle.setBold(0); rtstyle.setFont(RichTextStyle.FONT_COURIER); rti.appendStyle(rtstyle); //Doclinks mitten im RichText einfuegen, mittels beginInsert rtNavigator = rti.createNavigator(); rtNavigator.findFirstElement(RichTextItem .RTELEM_TYPE_TEXTPARAGRAPH); rtNavigator.findNextElement(); rti.beginInsert(rtNavigator,true); rti.appendText("zwischendrin"); rti.addNewLine(); rti.appendText("Link zum Dokument: "); rti.appendDocLink(doc, "Link zum Dokument"); rti.endInsert();
Jeder Text, der an ein RichTextItem über appendText angehängt wird, erhält keinen Zeilenumbruch am Ende. Dieser muss über addNewLine angefügt werden, wobei per Default nach jedem neuen NewLine ein neuer Absatz begonnen wird, was jedoch durch einen zusätzlichen boolschen Parameter unterbunden werden kann. Soll viel Text an ein RichTextItem angehängt werden, ist es wichtig, häufigen Gebrauch von addNewLine zu machen, da der Fließtext innerhalb eines Paragraphen auf 32KB beschränkt ist. Sind im anzuhängenden Text Newline Character enthalten, müssen diese durch die Anwendung von addNewLine ersetzt werden. Die Zeichen (char)10 und (char)13 werden als nicht darstellbare Zeichen im RichText angezeigt, nicht etwa als Zeilenumbrüche, wie man dies erwarten würde. Sollen Textstile im RichText verwendet werden, werden diese fortlaufend im Richtext untergebracht und zeigen jeweils den Wechsel im Stil an (z.B. „Fliesstext“->“<<Wechsel auf Bold>>“ -> Fliesstext -> „Wechsel auf nicht-Bold“ ...). Soll mitten im vorhandenen Inhalt eines RichTextes editiert weden, muss der Cursor des RichTextItems durch den Aufruf von beginInsert an die gewünschte Stelle gestellt werden. Jedes beginInsert muss durch endInsert wieder beendet werden. Die für Domino typischen Links auf Dokumente, Ansichten oder Datenbanken können programmatisch durch den Befehl appendDocLink erzeugt werden, wobei dieser Methode einfach das Objekt übergeben wird , auf das verlinkt werden soll. Das gesamte Beispiel finden Sie in der Klasse RichTextDemo im Code zum Buch im Package djbuch.kapitel_11.
11.3
RichTextNavigator
Ein RichTextNavigator ist ein Objekt, das aus dem RichTextItem mit createNavigator erzeugt wird und das es ermöglicht, alle Elemente eines RichTextItems zu referenzieren. Die Navigation erfolgt immer anhand des Typus eines Elements innerhalb des RichTextItems, indem über verschiedene „find“-Operationen ein
436
11 RichText
Element eines bestimmten Typus selektiert werden kann. RichTextNavigator kennt einen internen Cursor, der durch diese Methoden auf die verschiedenen Elementpositionen gesetzt werden kann. Der Typus wird als Integerkonstante übergeben und kann durch die in Tabelle 11-1 aufgelisteten Konstanten in RichTextItem bestimmt werden. Hierbei sind für die Find-Operationen die Konstanten RTELEM_TYPE_TABLE, RTELEM_TYPE_TEXTRUN, RTELEM_TYPE_TEXTPARAGRAPH, RTELEM_TYPE_ DOCLINK, RTELEM_TYPE_SECTION, RTELEM_TYPE_TABLECELL, RTELEM_TYPE_FILEATTACHMENT und RTELEM_TYPE_OLE zulässige Werte für den Parameter type. Für die Get-Operationen sind lediglich die Konstanten RTELEM_TYPE_TABLE, RTELEM_TYPE_DOCLINK, RTELEM_TYPE_SECTION, RTELEM_TYPE_FILEATTACHMENT und RTELEM_TYPE_OLE erlaubt, da nur für diese Konstanten korre-
spondierende Objektklassen existieren. Einige dieser Methoden geben keinen Typus der Elemente an, so dass dann das jeweils nächste (etc.) Element des zuvor gewählten Typus selektiert wird. Zusätzlich gibt es vergleichbare „get“-Methoden, die nicht den Cursor auf die Position des Elementes stellen, sondern das Element als Rückgabewert zurückgeben. Über die find-Methoden lässt sich einfach überprüfen, ob das gewünschte Element bzw. die gewünschte Position auch erreichbar ist. Daher sind diese Methoden zu bevorzugen. Ein typischer Code könnte wie folgt aussehen: if (!rtNavigator.findFirstElement (RichTextItem.RTELEM_TYPE_SECTION)) return; do { rtsection = (RichTextSection)rtNavigator.getElement(); System.out.println (rtsection.getTitle()); } while (rtNavigator.findNextElement (RichTextItem.RTELEM_TYPE_SECTION));
Im Beispiel wird über die Methoden findFirstElement und findNextElement navigiert. Der einleitende Aufruf von findFirstElement überprüft bereits, ob es ein Element vom gewünschten Typ gibt . Nur falls dies der Fall ist, werden in einer do-while-Schleife alle Elemente des gewünschten Typus verarbeitet, indem diese über die Methode getElement() geladen werden . Da findNextElement einen boolean zurückgibt, der anzeigt, ob es überhaupt ein weiteres Element gibt, kann diese Methode als Abbruchbedingung für die Schleife genutzt werden, wobei gleichzeitig der Cursor des RichTextNavigator im Erfolgsfall weitergesetzt wird . Ein RichTextNavigator wird mit createNavigator in RichTextItem erzeugt oder durch getNavigator in RichTextRange bezogen. RichTextNavigator ist neu seit Domino R6.5.
11.3.1 Methoden in RichTextNavigator
437
RichTextNavigator beherrschen Die Methoden in RichTextNavigator basieren entweder auf einem ausdrücklich spezifizierten Typus eines Elements oder beziehen sich auf den zuvor einleitend implizit festgelegten Typus. Die Typen müssen bei der Anwendung der Methoden konsequent kontrolliert werden, da sie relativ schlecht intern überwacht werden. Ein Aufruf getFirstElement (TypusA) gefolgt von getNextElement (TypusB) führt sicher zu Problemen. Es wird zwischen der Operation innerhalb des Navigators auf Basis der „find“-Methoden und den getter-Methoden unterschieden. Diese arbeiten autark, oder anders formuliert, die getter-Methoden setzen nicht den Cursor für die find-Methoden. Auch hier sollte auf eine Stringenz bei der Benutzung des Typus geachtet werden. Eine gemischte Anwendung beider Methoden sollte vermieden werden. Besser nacheinander anwenden. Bei Verwendung der find-Methoden kann jederzeit über getElement() das aktuelle Element als Objekt bezogen werden. Nicht jedes Element kann als Objekt geladen werden und hat auch nicht zwingend eine Repräsentation in einer Java-Klasse. Sämtliche Parameter sollten auf Plausibilität überprüft werden. Nullparameter sind zu vermeiden. Am besten wird zunächst mit findNextElement überprüft, ob es weitere Elemente gibt, bevor versucht wird, sie zu laden, da offenbar im Fehlerfall (kein weiteres Element vorhanden) nicht immer sicher Exceptions geworfen werden. Die find-Methoden sollten bevorzugt genutzt werden.
11.3.1 •
Methoden in RichTextNavigator
boolean findNthElement(int type, int position) throws NotesException Neu seit Domino R6.5. Setzt den Cursor des RichTextNavigator auf das Element vom Typus type an der Position position, wobei nur Elemente diesen
Typs gezählt werden. Als Position muss ein Wert größer als 0 angegeben werden. Der Rückgabewert zeigt an, ob das gewünschte Element gefunden werden konnte. •
boolean findFirstElement(int type) throws NotesException boolean findLastElement(int type) throws NotesException Neu seit Domino R6.5. Setzt den Cursor des RichTextNavigator auf das erste bzw. letzte Element vom Typus type.
438
11 RichText Der Rückgabewert zeigt an, ob das gewünschte Element gefunden werden konnte.
•
boolean findNextElement() throws NotesException boolean findNextElement(int type) throws NotesException boolean findNextElement(int type, int position) throws NotesException Neu seit Domino R6.5. Setzt den Cursor des RichTextNavigator auf das nächste Element im Navigator. Wird kein Typ über den Parameter type spezifi-
ziert, wird nach Elementen vom Typus des zuletzt navigierten Elements gesucht. Wird zusätzlich der Parameter position angegeben, wird eine entsprechende Anzahl von Elementen übersprungen. Für position==1 wird das nächste Element diesen Typs gewählt. Der Rückgabewert zeigt an, ob das gewünschte Element gefunden werden konnte. •
Base getElement() throws NotesException
Neu seit Domino R6.5. Gibt das aktuelle Element zurück, auf das der Cursor des RichTextNavigator zeigt. Verweist der Cursor des RichTextNavigator auf eine ungültige Position, wird eine NotesException „Position is not valid“ geworfen. •
•
•
Base getNthElement(int type, int position) throws NotesException Neu seit Domino R6.5. Gibt das Element vom Typ type an der Position position zurück. Die Parameter type und position müssen gültige Werte sein. Base getFirstElement(int type) throws NotesException Base getLastElement(int type) throws NotesException Neu seit Domino R6.5. Gibt das erste bzw. letzte Element vom Typ type zurück. Base getNextElement() throws NotesException Base getNextElement(int type) throws NotesException Base getNextElement(int type, int position) throws NotesException
Neu seit Domino R6.5. Gibt das nächste Element des aktuellen Typus zurück, bzw. das nächste Element vom Typ type. Wird zusätzlich der optionale Parameter position übergeben, wird eine entsprechende Anzahl von Elementen übersprungen, wobei für position==1 das nächste Element zurückgegeben wird. •
Neu seit Domino R6.5. Führt eine Suche nach dem Parameter searchString durch und gibt true zurück, falls dieser gefunden wurde. Optionale Suchparameter werden als Integerkonstante searchOptions übergeben, wobei die Optionen als Addition der gewünschten Optionskonstanten RT_FIND_CASEINSENSITIVE, RT_FIND_PITCHINSENSITIVE oder RT_FIND_ACCENTINSENSITIVE aus RichTextItem übergeben werden.
Neu seit Domino R6.5. Führt eine erneute Suche durch. Liefert nur sinnvolle Werte, wenn zuvor eine gleichlautende Suche mit getFirstString durchgeführt wurde. • •
RichTextNavigator Clone() throws NotesException Neu seit Domino R6.5. Erzeugt eine Dublette des RichTextNavigator. void setPosition(Base rtElement) throws NotesException
Neu seit Domino R6.5. Setzt den Cursor vor die Position des Elements. Zulässig sind Objekte vom Typ RichTextNavigator, RichTextRange, RichTextDocLink, RichTextTable oder EmbeddedObject. •
Setzt den Cursor hinter die Position des Elements. Zulässig sind Objekte vom Typ RichTextNavigator, RichTextRange, RichTextDocLink, RichTextTable oder EmbeddedObject. •
void setCharOffset(int charOffset) throws NotesException Neu seit Domino R6.5. Verschiebt den Cursor um einen Offset von charOffset
Zeichen. Der Parameter muss größer als 0 sein. Diese Methode kann nur angewendet werden, falls der RichTextNavigator auf ein Element vom Typ RTELEM_TYPE_TEXTRUN oder RTELEM_TYPE_TEXTPARAGRAPH zeigt oder zuvor eine Suche durchgeführt wurde (der RichTextNavigator verweist dann auf ein Element vom Typ RTELEM_TYPE_TEXTSTRING). Um den verschobenen Offset „sichtbar“ zu machen wird ein RichTextRange benötigt, der dann über seine Methode getTextParagraph () den Text ausgeben kann, z.B.: System.out.println (rtNavigator.findNextString("Link")); rtNavigator.setCharOffset(3); rtrange = rti.createRange(); rtrange.setBegin(rtNavigator); System.out.println (rtrange.getTextParagraph());
Dieser Code gibt lediglich den Buchstaben „k“ aus. Die ersten drei Buchstaben des Wortes „Link“ entfallen wegen der Offsetverschiebung. Dies ist ein typischer Anwendungsfall für einen RichTextRange, der sozusagen einen verschobenen Punkt innerhalb des RichTextNavigators referenzieren kann.
11.4
RichTextRange
Ein RichTextRange wird benötigt, um Teilbereiche eines RichTextItems abzubilden. Es wird außerdem benötigt, um bestimmte Textinhalte auszulesen, wie zum Beispiel Tabellenzellen. Ein RichTextRange wird aus dem RichTextItem erzeugt und
440
11 RichText
die Grenzen, für die der Bereich gültig sein soll, über setBegin und setEnd gesetzt. Sind diese Grenzen festgelegt, kann über die Methoden getTextRun und getTextParagraph der beinhaltete Text gelesen und die zugehörigen Styles ausgelesen oder gesetzt werden. Über den RichTextRange wird die hierarchische Objektstruktur des RichTextItems deutlich. Ein RichTextItem enthält z.B. ein Element vom Typ Tabelle, gefolgt von den zugehörigen Zellen. Die Zellen können in einen Range eingelesen werden, der auf einem Navigator basiert, der die Zellen enthält. Die Inhalte der Zellen werden wieder als Paragraph aufgefasst und über getTextParagraph in RichTextRange können nun die Texte ausgelesen werden. Besteht der RichTextRange aus lediglich einem Element, werden die Operationen direkt für dieses Element angewandt. Diese Technik ist empfehlenswert, um gezielt einzelne Elemente im RichTextItem zu manipulieren. RichTextRange ist neu seit Domino R6.5.
11.4.1 •
Methoden in RichTextRange
RichTextStyle getStyle() throws NotesException void setStyle(RichTextStyle richtextstyle) throws NotesException Neu seit Domino R6.5. Setzt den RichTextStyle des gesamten Textes im Ran-
ge bzw. gibt diesen zurück. Ist das letzte Element ungleich dem ersten und das letzte Element vom Typ RTELEM_TYPE_TABLE, RTELEM_TYPE_DOCLINK, RTELEM_TYPE_SECTION, RTELEM_TYPE_FILEATTACHMENT oder RTELEM_TYPE_OLE, wird der Style nicht für das letzte Element gesetzt. •
void remove() throws NotesException
Neu seit Domino R6.5. Löscht alle Elemente im RichTextRange. Das letzte Element wird nicht gelöscht, falls es von einem der Typen RTELEM_TYPE_TABLE, RTELEM_TYPE_DOCLINK, RTELEM_TYPE_SECTION, RTELEM_TYPE_FILEATTACHMENT oder RTELEM_TYPE_OLE ist und das RichTextRange nicht nur
aus einem Element besteht. Die Methode kann nicht über die logischen Grenzen der einzelnen Elemente hinaus angewandt werden, d.h. in der Regel müssen komplexe Elemente (Abschnitt, Tabelle usw.) einzeln gelöscht werden. •
void setBegin(Base element) throws NotesException void setEnd(Base element) throws NotesException Neu seit Domino R6.5. Setzt die Grenzen des RichTextRange auf das durch den Parameter element übergebene Element. Zulässig sind Objekte der Klassen RichTextNavigator, RichTextRange, RichTextDocLink oder RichTextTable. Wird ein Objekt vom Typ RichTextSection übergeben, wird dies ignoriert. Gesetzt wird die Position des Elements bzw. beim RichTextNavigator dessen aktuelle Position. Wird setBegin und setEnd für ein und dasselbe Element angewendet, besteht der RichTextRange genau aus diesem Element.
11.4.1 Methoden in RichTextRange
441
•
void reset(boolean resetBegin, boolean resetEnd) throws NotesException Neu seit Domino R6.5. Stellt die ursprünglichen Grenzen des RichTextRanges wieder her (nachdem diese durch setBegin und setEnd verändert wurden), wobei die Parameter resetBegin bzw. resetEnd den Wert true erhalten müs-
•
int findandReplace(String source, String replacement) throws NotesException int findandReplace(String source, String replacement, long options) throws NotesException Neu seit Domino R6.5. Ersetzt das nächste Vorkommen des Textes source im RichTextRange durch den String replacement und setzt die Cursorpositi-
sen, damit die jeweilige Grenze zurückgesetzt wird.
on auf dieses Vorkommnis. Als Optionen können Additionen aus den Konstanten RT_FIND_CASEINSENSITIVE, RT_FIND_PITCHINSENSITIVE, RT_ FIND_ACCENTINSENSITIVE, RT_REPL_PRESERVECASE und RT_REPL_ALL eingesetzt werden. • •
RichTextRange Clone() throws NotesException Neu seit Domino R6.5. Erzeugt eine Kopie des RichTextRange. int getType() throws NotesException
Neu seit Domino R6.5. Gibt den Typ des ersten Elements entsprechend den Konstanten aus RichTextItem (s. Tabelle 11-1) zurück. •
String getTextRun() throws NotesException Neu seit Domino R6.5. Liefert den Fließtext im RichTextRange vom Anfang
des Ranges bis zum Ende des Paragraphen oder bis zu einem Stilwechsel. Liefert den (ersten) Fließtext für Elemente vom Typ RTELEM_TYPE_TEXTRUN, RTELEM_TYPE_TEXTPARAGRAPH, RTELEM_TYPE_SECTION oder RTELEM_ TYPE_TABLECELL. Für RTELEM_TYPE_DOCLINK liefert die Methode den Linktext, sofern vorhanden, und ansonsten den leeren String, wie auch für die Elemente vom Typ RTELEM_TYPE_FILEATTACHMENT RTELEM_TYPE_OLE und RTELEM_TYPE_TABLE. Da die Elemente RTELEM_TYPE_TEXTPARAGRAPH, RTELEM_TYPE_SECTION oder RTELEM_TYPE_TABLECELL aus mehreren Fließtextelementen bestehen können, liefert die Methode nur den ersten Fließtext. •
•
String getTextParagraph() throws NotesException Neu seit Domino R6.5. Arbeitet wie getTextRun, liefert jedoch für die Elemente RTELEM_TYPE_TEXTPARAGRAPH, RTELEM_TYPE_SECTION oder RTELEM_TYPE_TABLECELL den gesamten Text des aktuellen Paragraphen. RichTextNavigator getNavigator() throws NotesException
Neu seit Domino R6.5. Liefert einen Navigator, der den Elementen aus dem RichTextRange entspricht.
442
11 RichText
11.5
RichTextSection
Werden Abschnitte (s. Kap. 3.2.7) in einem RichTextItem definiert, können diese seit Domino R6.5 in einem Java-Objekt abgebildet werden. Ein RichTextSectionObjekt wird über findFirstElement (RichTextItem.RTELEM_TYPE_SECTION) ausgewählt oder über getFirstElement(RichTextItem.RTELEM_ TYPE_SECTION) in RichTextNavigator zurückgegeben. Es wird über beginSection und endSection in RichTextItem neu angelegt. RichTextSection ist neu seit Domino R6.5.
Neu seit Domino R6.5. Bestimmt bzw. verändert den Zustand des Abschnittes, der bestimmt, ob er beim Öffnen im Notes Client auf- oder zugeklappt ist.
11.6
RichTextTable
Tabellen in RichTextItems sind in Domino sehr komplex, da sie nicht nur als Tabelle, sondern z.B. auch als Basiselement für Karteikartenreiter-gesteuerte Navigationselemente genutzt werden. Viele Eigenschaften können nach wie vor nur über den Notes Client verändert oder erstellt werden. Seit Domino R6.5 ist es immerhin möglich, die Tabelle als Element zu laden und neu im RichTextItem zu erstellen und die Zellen auszulesen, zumindest sofern die Struktur nicht zu komplex ist, da die Tabellenzellen über den RichTextNavigator geladen werden müssen und dieser bei mehreren Tabellen nicht zwischen diesen unterscheidet. Insbesondere beim Neuerstellen von Tabellen sind die neuen Methoden sehr hilfreich.
11.6.1 Methoden in RichTextTable
443
Tabellen werden über appendTable in RichTextItem neu erstellt, über den RichTextNavigator selektiert über einen RichTextRange abgebildet, so dass die Zellen gelesen bzw. verändert werden können. Ist eine Tabelle erst einmal erstellt, kann lediglich ihre Zeilenzahl einfach vergrößert oder verkleinert werden. Soll eine Spalte gelöscht werden, muss über einen RichTextNavigator jede Zelle der betreffenden Spalte angesteuert werden, hieraus ein RichTextRange erzeugt und dann diese Zelle gelöscht werden. RichTextTable ist neu seit Domino R6.5.
Neu seit Domino R6.5. Fügt einer Tabelle eine oder bei der Spezifikation von noOfNewRows mehrere Zeilen hinzu. Per Default wird nach der letzten Zeile eingefügt, bei Angabe von insertAfter nach der hierdurch angegebenen Zeile, bei einer Zählung beginnend mit 1. •
Neu seit Domino R6.5. Entfernt aus einer Tabelle eine oder bei der Spezifikation von noOfNewRows mehrere Zeilen. Per Default wird die letzte Zeile entfernt, bei Angabe von deleteAt nach der hierdurch angegebenen Zeile, bei einer Zählung beginnend mit 1. •
void remove() throws NotesException
Neu seit Domino R6.5. Entfernt die Tabelle aus dem RichTextItem. •
Neu seit Domino R6.5. Tabellen können mit farbigen Zellhintergründen dargestellt werden. Diese Farbe kann entweder vollflächig angewendet werden oder durch die Verwendung von sowohl setColor als auch setAlternateColor dazu verwendet werden, entweder die Zeilen oder die Spalten in abwechselnden Farben darzustellen. Damit die durch setColor oder setAlternateColor gesetzten Farben zur Anwendung kommen, muss durch setStyle eine dieser drei Eigenschaften (vollflächig, wechselnde Zeilen, wechselnde Spalten) für die Tabelle gesetzt werden.
444 •
11 RichText int getStyle() throws NotesException void setStyle(int i) throws NotesException Neu seit Domino R6.5. getStyle liefert und setStyle setzt verschiedene Style-Informationen für die Tabelle. Gesetzt werden können die Styles durch int Konstanten in RichTextTable. Diese können jeweils nur ausschließlich ver-
wendet und nicht durch etwa Addition gleichzeitig verwendet werden. Zur Verfügung stehen: –
TABLESTYLE_NONE – Die Hintergrundfarben der Tabelle werden zurückge-
–
TABLESTYLE_LEFTTOP – Die linke Spalte und die erste Zeile erhalten die
–
TABLESTYLE_TOP – Die erste Zeile erhält die primäre, alle anderen die al-
–
TABLESTYLE_LEFT – Die linke Spalte erhält die primäre, alle anderen die
–
TABLESTYLE_ALTERNATINGCOLS – Die Spalten werden in wechselnden
–
TABLESTYLE_ALTERNATINGROWS – Die Zeilen werden in wechselnden Far-
–
TABLESTYLE_RIGHTTOP – Die rechte Spalte und die erste Zeile erhalten die
setzt. primäre, alle anderen die alternative Farbe. ternative Farbe. alternative Farbe. Farben dargestellt, beginnend mit der alternativen Farbe. ben dargestellt, beginnend mit der alternativen Farbe.
– –
•
primäre, alle anderen die alternative Farbe. TABLESTYLE_RIGHT – Die linke Spalte erhält die primäre, alle anderen die alternative Farbe. TABLESTYLE_SOLID – Der gesamte Tabellenhintergrund wird auf die primäre Farbe gesetzt.
int getRowCount() throws NotesException int getColumnCount() throws NotesException
Neu seit Domino R6.5. Liefert die Anzahl der Zeilen und Spalten der Tabelle. •
Neu seit Domino R6.5. Setzt und liefert die Beschriftungen der Tabellenlabel. Siehe auch appendTable in RichTextItem. •
boolean isRightToLeft() throws NotesException void setRightToLeft(boolean readingOrder) throws NotesException Neu seit Domino R6.5. setRightToLeft (true) muss gesetzt werden, wenn
Schriften unterstützt werden sollen, für die die Leserichtung von rechts nach links läuft.
11.6.2 Im- und Export von RichText – Erzeugen von RichText 11.6.2
445
Im- und Export von RichText – Erzeugen von RichText
Tabellen werden im RichTextItem als eine Abfolge von Elementen bestehend aus einem Tabellenheader, der über die find-Methoden zum Typ RichTextItem. RTELEM_TYPE_TABLE bezogen werden kann und allen Zellen, die über die find-Methoden zum Typ RichTextItem.RTELEM_TYPE_TABLECELL bezogen werden können. Befinden sich mehrere Tabellen in einem RichTextItem, bestehen diese aus einer Abfolge eines Headers einer oder mehrerer Zellen, optionalen tabellenfremden Elementen, einem erneuten Header und einer oder mehrerer Zellen der zweiten Tabelle. Werden über einen RichTextNavigator die Tabellenzellen im RichTextItem referenziert, so erhält man alle (!) Zellen der beiden Tabellen. Zum Lesen der Tabelle kann über das geschickte Setzen von RichTextRange gezielt die Menge der Zellen einer der Tabellen erfasst werden. Soll eine Tabelle mit Inhalten belegt werden, kann diese Technik nicht angewendet werden, da derart gesetzte Ranges durch die notwendigen Operationen beginInsert und endInsert im RichTextItem gestört wird. Hierfür kann aber leicht durch geschicktes Auszählen ebenfalls eine beliebige der im RichTextItem enthaltenen Tabellen manipuliert werden. In den beiden Listings Listing 11-1 und Listing 11-2 befindet sich Beispielcode, in dem diese Problematiken diskutiert werden und darüber hinaus noch einige nützliche Methoden zum Umgang mit Tabellen aufgezeigt werden. Speziell in den beiden Methoden runImport und runExport wird aufgezeigt, wie sehr einfach und dynamisch Tabellen im RichTextItem erzeugt oder als Tabelle z.B. in eine Datei exportiert werden können. Um den Praxisbezug zu erhöhen wurde der Export anhand der Klassen aus dem jxl-Package von Andy Khan [AndyKhan, JExcel] so gestaltet, dass eine Konvertierung zwischen Excel- und RichTextTable ermöglicht wird. Aufgrund der gut angelegten und einfachen Objektstruktur dieses Pakets wird als angenehme Nebenwirkung erreicht, dass der Beispielcode sich auf die Bearbeitung des RichText konzentrieren kann ohne durch aufwändige Dateioperationen und Datenmanipulationen aufgebläht zu werden. Für die Erzeugung einer dynamischen RichText-Tabelle wird in Listing 11-1 zunächst ein Dokument benötigt, in dem ein RichTextItem erzeugt werden kann. Dies wird durch die Methode createDoc in DemoTools vereinfacht. Dieser Methode werden Parameter für die Datenbank, in der das Dokument erzeugt werden soll, der Name der Maske für das Dokument, eine Kategorie, ein Titel und ein optionales Mutterdokument, falls ein neues Antwortdokument erzeugt werden soll übergeben. Im Package befindet sich die Datei „demo.xls“, die als Basis für den Import dienen wird. Zur Abwicklung des Im- und Exportvorgangs wurde die Klasse ExcelToRichTextConverter (Listing 11-2) entwickelt, die zunächst instanziert wird , wobei das RichTextItem, in dem die neue Tabelle erzeugt werden soll und der Dateipfad der zu importierenden Datei übergeben wird.
446
11 RichText
package djbuch.kapitel_11; import lotus.domino.*; import djbuch.kapitel_06.*; public class RichTextTableDemo extends NotesThread { private static final String PASSWORD = "geheim"; private static final String HOST = null; // DIIOP-Host und -User private static final String USER = null; // oder null für lokal public static void main(String[] args) { RichTextTableDemo rtd = new RichTextTableDemo(); rtd.start(); } public void runNotes() { Document doc = null; Database db = null; Session session = null; RichTextItem rti = null; try { session = NotesFactory.createSession(HOST, USER, PASSWORD); db = session.getDatabase("", "djbuch/djbuch.nsf"); doc = DemoTools.createDoc(db,"FO_dokumentRT_k11","Wombat", "Ich beinhalte RichText", null); rti = doc.createRichTextItem("body"); String basePath = System.getProperty("user.dir") + "/src/djbuch/kapitel_11/"; basePath = DemoTools.replaceSubstring(basePath,"\\","/"); rti.appendText("Tabelle aus Excel (" + basePath+"demo.xls)"); rti.addNewLine(); ExcelToRichTextConverter etri = new ExcelToRichTextConverter (rti, basePath+"demo.xls"); etri.runImport(); doc.save (true,false); String unid = doc.getUniversalID(); GC.recycle(rti); GC.recycle(doc); doc = db.getDocumentByUNID(unid); rti = (RichTextItem) doc.getFirstItem("body"); etri = new ExcelToRichTextConverter (rti, basePath+"out.xls"); etri.runExport(1); GC.recycle (doc); } catch (Exception e) { e.printStackTrace(); System.err.println ((e instanceof NotesException?"id = " +((NotesException)e).id:"")); } finally { GC.recycle (rti); GC.recycle(doc); GC.recycle(db); GC.recycle(session); } } }
Listing 11-1 Lesen und Schreiben von Tabellen im RichTextItem
Nun muss lediglich die Methode runImport gestartet werden, um den Import zu starten. Hierbei wird im RichTextItem eine neue Tabelle erzeugt, die die Daten aus der Importdatei übernimmt und dieselbe Zeilen- und Spaltenzahl der Datei hat. Berücksichtigt werden nur Textinhalte. Die so erzeugte Tabelle im RichTextItem kann leider nicht direkt wieder exportiert werden, sondern muss zwischengespeichert werden, um nachfolgend als Basis
11.6.2 Im- und Export von RichText – Erzeugen von RichText
447
Abb. 11-1 Dynamisch erzeugte Tabelle im RichText
für den Export zu dienen. Dies liegt daran, dass RichTextItems ziemlich stark gepuffert sind, einschließlich der auf das RichTextItem angewandten Methoden. Auch ein update() schafft in diesem Beispiel keine Abhilfe. Die in diesem Beispiel erstellte Tabelle (Abb. 11-1) ist dennoch hervorragend geeignet, selbst wieder in eine Datei exportiert zu werden. Ein erneutes Laden des Dokuments – einfach über die Universal ID des Dokuments – flusht alle Puffer und lädt das Dokument und sein RichTextItem im durch den Import erzeugten endgültigen Zustand. Dabei ist es erforderlich, das Dokument zuvor nicht nur zu speichern sondern auch zu recyceln, damit Domino sämtliche Referenzen freigibt und nicht etwa versucht auf einen Cache des Dokuments zurückzugreifen. Auch für den Export muss wiederum nur ein ExcelToRichTextConverterObjekt instanziert und diesmal die Methode runExport gestartet werden. Diese erlaubt einen int-Parameter, durch den festgelegt werden kann, welche der im RichTextItem gefundenen Tabellen in ihrer Reihenfolge, beginnend mit 1, exportiert werden soll.
public int getTableCount () throws NotesException { RichTextNavigator rtn = richText.createNavigator(); boolean ok = rtn.findFirstElement(RichTextItem.RTELEM_TYPE_TABLE); if (!ok) return 0; int result = 1; while (ok) { ok = rtn.findNextElement(); if (ok) result++; } return result; } // ... setter und getter }
Listing 11-2 Import und Export von RichTextTable
Das Schließen des Threads durch stermThread entfällt hier übrigens, da die Ausführung als Erweiterung von NotesThread gewählt wurde . Wichtig ist allerdings das Recycling der Session, insbesondere, falls für die Parameter HOST und USER nicht Null-Werte gewählt wurden und somit eine DIIOP-Session aufgebaut wurde. Im Listing 11-2 finden Sie die Implementierung der Klasse ExcelToRichTextConverter, die die benötigten Methoden runImport und runExport und einige nützliche Helferlein enthält. Bei der Instanzierung der Klasse im Constructor werden zunächst einige Instanzvariablen gesetzt, die für die spätere Verarbeitung benötigt werden. Da für einige Operationen eine Domino-Session notwendig ist, wird diese über diverse Parent-Referenzen des RichTextItem rti – der Parent des RichTextItem ist ein Document, dessen Parent eine Database ist, die wiederum die Session als Parent kennt – geladen und in der Instanzvariable session gespeichert. Diese Technik ist praktisch, da so der gesamten Klasse eine Session zur Verfügung steht, ohne dass diese als Parameter übergeben werden müsste. Beim Recycling müssen Sie sich um keines dieser Objekte kümmern, schließlich gehören diese Objekte ja nicht Ihnen, sondern nur den Referenzen, die selbst für das Recycling verantwortlich sind. Die Methode runImport ist für die dynamische Erzeugung einer RichTextTabelle aus externen Daten (in unserem Beispiel die Excel-Datei) verantwortlich. Die excelspezifischen Operationen des jxl package basieren auf einem Workbook-Objekt , das die Excel-Datei repräsentiert und ein oder mehrere Sheet-Objekte enthalten kann. Auf diese Operationen soll nicht näher eingegangen werden, sie sprechen aber für sich selbst und stehen lediglich als Beispiel für eine externe Datenbasis Pate. Die neue Tabelle im RichTextItem wird gleich zu Beginn mit der Methode appendTable in RichTextItem in den Dimensionen der Datenbasis bezüglich der Spalten und Zeilen angelegt. Dies ist erforderlich, da das nachträgliche Hinzufügen von Zeilen und insbesondere Spalten unnötig aufwändig wäre. Die Methode geht davon aus, dass der Cursor des RichTextItem am Ende desselben steht, d.h. dass ein möglicherweise vor Aufruf der Methode erfolgter beginInsert bereits durch ei-
11.6.2 Im- und Export von RichText – Erzeugen von RichText
451
nen endInsert beendet wurde. Soll die Tabelle an einer anderen Stelle eingefügt werden, so muss die Kalkulation der Zellenposition angepasst werden und der Methode zusätzliche Informationen über eventuelle andere Tabellen mitgegeben werden. Bevor die neue Tabelle mit Inhalten gefüllt wird, soll noch anhand der Methode setColors demonstriert werden, wie Eigenschaften einer Tabelle verändert werden können. Hierfür wird zunächst ein RichTextTable-Objekt benötigt, das mit der ebenfalls in ExcelToRichText definierten Methode getTable bezogen werden kann. Da davon ausgegangen wird, dass die neue Tabelle an letzter Stelle erzeugt wurde, wird mit Hilfe der Methode getTableCount die Gesamtzahl der Tabellen und somit die letzte Position errechnet. basiert auf einem RichTextNavigator der ledigDie Methode getTable lich sämtliche Tabellenobjekte des RichTextes anhand von findFirst[Next]Element (RichTextItem.RTELEM_TYPE_TABLE) durchschreitet und das Tabellenobjekt der gewünschten Position zurückgibt. Ganz ähnlich geht getTableCount vor, nur dass sämtliche Tabellenobjekte durchschritten und hierbei gezählt werden. Damit die in setColors über setColor und setAlternateColor gesetzten Farben auch in der Tabelle dargestellt werden, muss über setStyle in RichTextTable noch festgelegt werden, wie diese beiden Farben auf die Tabelle angewendet werden sollen. Nachdem nun die Tabelle erstellt und mit Farben ausgestattet wurde, wird ein Loop über die Zellen der neuen Tabelle definiert. Bekannt ist, dass die Tabelle am Ende des RichTextes liegt, so dass auch die Zellen der Tabellen dort zu finden sind. Die Position der ersten Zelle dieser Tabelle berechnet sich aus der Gesamtzahl der Zellen minus dem Produkt aus Spalten und Zeilen der letzten (unserer neuen) Tabelle . Die Gesamtzahl der Zellen im RichTextItem wird über einen RichTextNavigator errechnet. An dieser Stelle kann angesetzt werden, wenn die neue Tabelle an einer beliebigen Stelle innerhalb des RichTextItems eingefügt werden soll. Über getTableCount ist die Gesamtzahl der Tabellen bekannt, über getTable können alle Tabellen geladen und die Anzahl der Zellen, die vor der neuen Tabelle liegen, errechnet werden. Für das Beispiel wird der RichTextNavigator zunächst auf die Position der ersten Zelle der neuen Tabelle gesetzt . Über zwei inneinander geschachtelte Schleifen zunächst über die Zeilen und dann die Spalten werden nun sämtliche Daten in die Tabelle geladen . Hierfür wird über beginInsert und endInsert der Cursor des RichTextItem so positioniert, dass ein appendText den Inhalt an der richtigen Stelle einfügt. Für jeden Durchgang der inneren Schleife wird durch findNextElement von Zelle zu Zelle gesprungen. An der Art, wie hier Inhalte in Zellen eingefügt werden, lässt sich gut erkennen, wie RichTextItems aufgebaut sind. Sie bestehen aus einer recht losen Abfolge von verschiedenen Objekten, die erst in ihrer Gesamtheit ein sinnvolles Konstrukt bilden. So ist eine Tabelle eine Abfolge von TabellenHeader und Tabellenzellen, eine Tabellenzelle eine Abfolge von Tabellenzellen und (enthaltenen) Objekten, hier einfachen Textparagraphen.
452
11 RichText
Bei komplexeren Strukturen wird letztendlich für die meisten Operationen auf Tabellen, insbesondere, wenn diese sich nicht am Rand des RichTextItem befinden, eine gewisse Menge an Kenntnis über den Aufbau des RichTextItem notwendig sein, um dieses sicher interpretieren zu können. lädt zunächst das gewünschte TabelDer Export der Tabelle in runExport lenobjekt und bestimmt dessen Dimensionen . Das Lesen der Tabelle gestaltet sich sehr einfach. Es wird aus dem Tabellenobjekt ein RichTextRange erzeugt, der genau diese Tabelle, also alle enthaltenen Elemente, beinhaltet und wiederum mittels getNavigator() in einen RichTextNavigator umgewandelt werden kann. Dieses Verfahren ist typisch für die Behandlung einzelner komplexer Elemente eines RichTextElements. Zunächst wird über einen RichTextRange die Menge der Elemente, die zu dem übergeordneten komplexen Objekt gehören, ausgewählt, dann wird dieser Range wieder in einen Navigator umgewandelt, um Referenzen auf die einzelnen Elemente erhalten zu können . Im Beispiel werden dann über diesen neuen RichTextNavigator alle Zellen der Tabelle angesprochen. Der Einfachheit halber wird auch hier die benötigte Schleife nicht direkt z.B. über ein do-while und findNextElement realisiert, sondern über zwei geschachtelte Schleifen , über die neu zu erzeugenden Zeilen und Spalten. Dies vereinfacht die Zuordnung der gelesenen Zelleninhalte zu den neu zu erzeugenden Zellen in der Exceltabelle. Jeder Schritt innerhalb dieser for-Schleifen muss natürlich für jede Zelle einen Aufruf von findNextElement auslösen. Um eine Zelle lesen zu können wird der RichTextRange cellContent über die Zelle definiert . Mittels getTextRun() kann der Inhalt des ersten Fliesßtextobjektes innerhalb dieses Ranges – also der Zelle – ausgelesen werden. Nun kann natürlich davon ausgegangen werden, dass eine Tabellenzelle mehrere Paragraphen enthalten kann. Hierfür müsste aus dem RichTextRange cellContent wieder ein RichTextNavigator bezogen werden, der dann mittels findFirst[Next]Element
(RichTextItem.RTELEM_TYPE_TEXTPARAGRAPH)
sämtliche Paragraphen dieser Zelle oder über Verwendung der übrigen Typen auch andere Daten auslesen kann. Eine Alternative bei der Bearbeitung sehr komplexer RichTextItems kann eventuell die Verwendung von XML und der Methode generateXML sein.
11.7
RichTextTab
Tabulatoren werden in Domino durch das Objekt RichTextTab repräsentiert. Während addTab ein tatsächliches Element in das RichTextItem einfügt (vergleichbar dem Betätigen der Tabulatortaste) und dort auch als RichTextTab über einen entsprechenden RichTextNavigator selektiert werden kann, können über Vector getTabs() in RichTextParagraphStyle zwar ebenfalls RichTextTab-Objekte referenziert werden, diese stellen aber keinen tatsächlichen Tab im Text dar, sondern beschreiben lediglich die im RichTextParagraphStyle definierten Positionen für zukünftig einzufügende Tabulatoren dar (vergleichbar mit den Tabulatorzeichen, die im Lineal eines geöffneten RichTextItems erstellt werden können). Ebenso erzeugt
11.7.1 Methoden in RichTextTab
453
setTab in RichTextParagraphStyle keinen Tabulator im Sinne eines Elementes im RichTextItem, sondern stellt die Position(en) für durch addTab nachfolgend erzeugte Tabulatoren ein. Hierfür muss der RichTextParagraphStyle angewendet werden (z.B. durch appendParagraphStyle). Gelöscht werden Tabulatoren über clear in RichTextTab oder clearAllTabs in RichTextParagraphStyle.
11.7.1 •
Methoden in RichTextTab
void clear() throws NotesException
Löscht den aktuellen Tabulator. •
int getPosition() throws NotesException
Liefert die horizontale Position, die der Tabulator markiert, in einer dominointernen Einheit, dem so genannten „twip“. Diese kann durch Division durch RichTextParagraphStyle.RULER_ONE_INCH oder RichTextParagraphStyle.RULER_ONE_CENTIMETER in Inch oder Zentimeter umgerechnet werden. •
int getType() throws NotesException
Ein Tabulator kann linksbündig, rechtsbündig, zentriert oder an der Dezimalstelle ausgerichtet sein. Diese Typen werden duch die Konstanten TAB_LEFT, TAB_RIGHT, TAB_CENTER oder TAB_DECIMAL in RichTextParagraphStyle repräsentiert.
11.8
RichTextDoclink
Ein Link in Domino kann in einem RichTextItem als RichTextDoclink (Abb. 11-2) erzeugt werden. Er wird als Element im RichText durch die Typen-Konstante RTELEM_TYPE_DOCLINK repräsentiert und über die Methoden appendDocLink in RichTextItem erzeugt. RichTextDoclinks werden entweder durch jeweils ein Symbol für einen Link auf ein Document, eine Datenbank oder eine Ansicht dargestellt oder durch einen verlinkten Text, sofern dieser beim Anlegen des Links übergeben wurde. Sofern ein solcher Text vorhanden ist, muss der RichTextDoclink über einen RichTextRange eingeschlossen werden, so dass über getTextRun der Text wieder gelesen werden kann. Die Klasse RichTextDoclink ist neu seit Domino R6.5
Neu seit Domino R6.5. Liefert oder setzt die Replik ID der Datenbank, die referenziert wird bzw. die das referenzierte Dokument oder den referenzierten View enthält. •
Neu seit Domino R6.5. Liefert bzw. setzt die Universal ID der referenzierten Ansicht. Die Universal ID ist die ID des Designdokuments (der Designnote), die die Ansicht beschreibt (und die z.B. im Domino Designer geöffnet werden kann). Falls keine Ansicht referenziert wird, liefert getViewUnID einen String, bestehend aus 32 Nullen. •
Neu seit Domino R6.5. Liefert bzw. setzt die Universal ID des referenzierten Dokuments. Falls kein Dokument referenziert wird, liefert getDocUnID einen String, bestehend aus 32 Nullen. •
void remove() throws NotesException
Neu seit Domino R6.5. Entfernt den DocLink aus dem RichText.
Neu seit Domino R6.5. Liefert bzw. setzt Stilinformationen für die Darstellung des verlinkten Textes, sofern der Doclink als Text und nicht als Symbol dargestellt wird. •
Neu seit Domino R6.5. Setzt bzw. liefert den Namen des Servers, auf dem sich das verlinkte Objekt befindet bzw. den leeren String, falls es sich um eine lokale Datenbank handelt (in der sich das Objekt befindet). •
Neu seit Domino R6.5. Liefert bzw. setzt den verlinkten Text, falls es sich um einen Text- und keine Symbollink handelt.
11.9
RichTextParagraphStyle
RichTextItems sind in Paragraphen aufgeteilt. Diese können über das Objekt RichTextParagraphStyle für die Ausgabe im NotesClient formatiert werden. RichTextParagraphStyle steuert Layoutparameter der Paragraphen, wie Zeilenabstände, Seitenränder, Umbruchsteuerung oder das Alignment und wird in Session über createRichTextParagraphStyle() erzeugt. Die wichtigsten Einsatzgebiete sind einerseits das direkte Setzen innerhalb eines RichTextItem durch appendParagraphStyle(RichTextParagraphStyle richtextparagraphstyle) und beim Erstellen von Seitenwechseln durch addPageBreak(RichTextParagraphStyle richtextparagraphstyle). Beim Erstellen von Tabellen über appendTable(int lines, int columns, Vector labels, int marginLeft, Vector columnStyles) können den Spalten eigene RichTextParagraphStyle-Objekte über den Vector columnStyles zugeordnet werden, die es dann insbesondere ermöglichen, die Spaltenbreite über setLeftMargin in RichTextParagraphStyle zu setzen. Für den linken Rand eines Absatzes wird zwischen der ersten und den übrigen Zeilen unterschieden, so dass am besten immer setLeftMargin und setFirstLineLeftMargin gleichzeitig gesetzt werden sollten, um Überraschungen zu vermeiden.
456
11 RichText
11.9.1
Methoden in RichTextParagraphStyle
•
int getAlignment() throws NotesException void setAlignment(int alignmentConstant) throws NotesException
Setzt bzw. liefert die Einstellung für das Alignment des Paragraphen. Der Rückgabewert bzw. der Parameter alignmentConstant kann einen der folgenden Werte annehmen: –
RichTextParagraphStyle.ALIGN_LEFT – Der nachfolgende Text wird
–
RichTextParagraphStyle.ALIGN_RIGHT –
links ausgerichtet. Der nachfolgende Text
wird rechts ausgerichtet. –
RichTextParagraphStyle.ALIGN_CENTER – Der nachfolgende Text
–
RichTextParagraphStyle.ALIGN_FULL – Der nachfolgende Text wird
–
RichTextParagraphStyle.ALIGN_NOWRAP – Die Ausrichtung wird zu-
wird zentriert. rechts und links ausgerichtet (Blocksatz). rückgesetzt. •
int getInterLineSpacing() throws NotesException void setInterLineSpacing(int spacing) throws NotesException
Setzt bzw. liefert die Einstellung für den Zeilenabstand der Zeilen dieses Paragraphen. Der Parameter spacing kann einen der Werte SPACING_SINGLE, SPACING_ONE_POINT_50 oder SPACING_DOUBLE in RichTextParagraphStyle für einzeiligen, 1,5-zeiligen oder zweizeiligen Zeilenabstand annehmen. •
int getSpacingAbove() throws NotesException void setSpacingAbove(int spacingAbove) throws NotesException
Setzt bzw. liefert die Einstellung für den Abstand vor dem Paragraphen. Der Parameter spacingAbove kann einen der Werte SPACING_SINGLE, SPACING_ONE_POINT_50 oder SPACING_DOUBLE in RichTextParagraphStyle für einzeiligen, 1,5-zeiligen oder zweizeiligen Abstand vor dem Paragraphen annehmen. •
int getSpacingBelow() throws NotesException void setSpacingBelow(int spacingBelow) throws NotesException
Setzt bzw. liefert die Einstellung für den Abstand nach dem Paragraphen. Der Parameter spacingBelow kann einen der Werte SPACING_SINGLE, SPACING_ONE_POINT_50 oder SPACING_DOUBLE in RichTextParagraphStyle für einzeiligen, 1,5-zeiligen oder zweizeiligen Abstand nach dem Paragraphen annehmen. •
int getLeftMargin() throws NotesException void setLeftMargin(int margin) throws NotesException
11.9.1 Methoden in RichTextParagraphStyle
457
Setzt bzw. liefert die Einstellung für den linken Rand des Paragraphen. Der Rückgabewert, bzw. der Parameter margin wird in so genannten „Twip“ berechnet, die durch die Konstanten RULER_ONE_INCH (=1440 Twip) oder RULER_ONE_CENTIMETER (=567 Twip) in RichTextParagraphStyle berechnet werden können. Default ist ein linker Rand von 1*RichTextParagraphStyle.RULER_ONE_INCH. Diese beiden Konstanten können mit beliebigen double größer Null multipliziert werden, müssen dann aber für die Verwendung als Parameter in einen (int) umgewandelt werden. Ein typischer Wert für margin sieht zum Beispiel so aus: (int) (3.21*RichTextParagraphStyle.RULER_ONE_CENTIMETER) (int) (0.65*RichTextParagraphStyle.RULER_ONE_INCH)
Wird ein RichTextParagraphStyle für die Formatierung von Spalten in Tabellen verwendet, beschreibt der linke Rand des Paragraphenstils dieser Spalte den Abstand des Textes vom linken Spaltenrand. •
int getRightMargin() throws NotesException void setRightMargin(int margin) throws NotesException
Setzt bzw. liefert die Einstellung für den rechten Rand des Paragraphen. Einstellung siehe setLeftMargin. Wird ein RichTextParagraphStyle für die Formatierung von Spalten in Tabellen verwendet, beschreibt der rechte Rand des Paragraphenstils dieser Spalte gleichzeitig die Breite der Spalte. •
int getFirstLineLeftMargin() throws NotesException void setFirstLineLeftMargin(int margin) throws NotesException
Setzt bzw. liefert die Einstellung für den linken Rand der ersten (!) Zeile des Paragraphen. Einstellung siehe setLeftMargin. •
int getPagination() throws NotesException void setPagination(int paginationConstant) throws NotesException
Setzt bzw. liefert die Einstellung für das Umbruchsverhalten von Paragraphen beim Seitenwechsel. Der Rückgabewert bzw. der Parameter paginationConstant kann die folgenden Werte annehmen: – – –
–
PAGINATE_DEFAULT – Zurücksetzen der Umbruchseinstellungen. PAGINATE_BEFORE – Ein Seitenumbruch erfolgt vor dem Paragraphen. PAGINATE_KEEP_WITH_NEXT Der Paragraph wird mit dem nachfolgenden
zusammengehalten, d.h. bei einem Seitenumbruch wird dieser und der nachfolgende Absatz immer auf derselben Seite erscheinen. PAGINATE_KEEP_TOGETHER – Innerhalb des Paragraphen erfolgt kein Seitenumbruch.
Diese Einstellung sollte vor der Anwendung von addPageBreak in RichTextItem gesetzt werden.
Liefert einen Vector von RichTextTab-Objekten, die die aktuellen Positionen für Tabulatoren in dem RichTextParagraphStyle repräsentieren. •
void setTab(int marginLeft, int type) throws NotesException Setzt eine neue Tabulatorposition an die Position marginLeft. Der Tabulator ist vom Typ type. Wird diese Eigenschaft des RichTextParagraphStyle gesetzt und dieser RichTextParagraphStyle auf ein RichTextItem angewendet, so wird die Verwendung von addTab() in RichTextItem jeweils einen
Versatz des Fließtextes zu dieser Position bewirken. Lediglich die Anwendung des RichTextParagraphStyle hat keine Auswirkung auf den Inhalt des RichTextItem. Gemessen wird immer vom linken Rand der Seite. Ist also eine Tabu-
latorposition links des linken Rands des Paragraphen, ist er wirkungslos. Werden mehrere Tabulatorpositionen an dieselbe Stelle gesetzt, wird die Position nur einmal markiert. Typischer Code könnte wie folgt aussehen. rtparagraphstyle = session.createRichTextParagraphStyle(); rtparagraphstyle.setRightMargin((int) (25.4 * RichTextParagraphStyle.RULER_ONE_CENTIMETER)); rtparagraphstyle.setTab(5 * RichTextParagraphStyle.RULER_ONE_CENTIMETER, RichTextParagraphStyle.TAB_LEFT); rtparagraphstyle.setTab(10 * RichTextParagraphStyle.RULER_ONE_CENTIMETER, RichTextParagraphStyle.TAB_LEFT); rti.addPageBreak(rtparagraphstyle); rti.appendText("Pos1"); rti.addTab(); rti.appendText("Pos2"); rti.addTab(); rti.appendText("Pos3");
Sofern der linke Rand den Standardabstand hat, wird dieser Code den Text „Pos1“ mit diesem Abstand, den Text „Pos2“ mit 5cm Abstand und den Text „Pos3“ mit 10cm Abstand vom linken Rand anzeigen. Beachten Sie, dass das Setzen der Tabs in RichTextParagraphStyle allein nicht genügt, sondern erst durch das Setzen des Tabs durch addTab eine Verschiebung des Textes stattfindet. •
void setTabs(int count, int startPos, int distance) throws NotesException void setTabs(int count, int startPos, int distance, int type) throws NotesException Setzt neue Tabulatorpositionen. Insgesamt werden count neue Tabulatoren gesetzt. Der erste an die Position startPos, alle nachfolgenden im Abstand von distance. Optional werden Tabulatoren vom Typ type erzeugt (siehe getType() in RichTextTab).
11.10 RichTextStyle •
459
void clearAllTabs() throws NotesException
Setzt alle Tabulatorpositionen zurück.
11.10
RichTextStyle
Schriften und Schriftstile im RichTextItem werden über einen RichTextStyle eingeleitet und geändert. Die einzelnen Eigenschaften (Schrifttyp oder bold usw.) werden einfach durch entsprechendes Hinzufügen eines RichTextStyle durch Anwenden von appendStyle in RichTextItem gesetzt. Typischer Code sieht wie folgt aus: rtstyle=session.createRichTextStyle(); rtstyle.setBold(RichTextStyle.YES); rti.appendStyle(rtstyle); rti.appendText("bold "); rtstyle=session.createRichTextStyle(); rtstyle.setItalic(RichTextStyle.YES); rti.appendStyle(rtstyle); rti.appendText("boldAndItalic "); rtstyle.setBold(RichTextStyle.NO); rti.appendStyle(rtstyle); rti.appendText("onlyItalic "); rtstyle.setItalic(RichTextStyle.NO); rti.appendStyle(rtstyle); rti.appendText("plain");
Beachten Sie, dass es unerheblich ist, ob beim neuen Setzen einer Eigenschaft (hier Italic ) der vorige (hiervon unabhängige Style – hier bold ) im RichTextStyle noch enthalten ist oder nicht . Anders gesprochen: Ob die Zeile ausgeführt wird oder nicht macht keinen Unterschied, die Eigenschaft bold bleibt so oder so erhalten, auch wenn Sie nicht erneut beim Setzen von Italic übermittelt wird.
11.10.1 •
Methoden in RichTextStyle
int getColor() throws NotesException void setColor(int notesColorConstant) throws NotesException
Liefert und setzt die Schriftfarbe als NotesFarbe, wobei hierfür eine der Konstanten COLOR_BLACK, COLOR_WHITE, COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_MAGENTA, COLOR_YELLOW, COLOR_CYAN, COLOR_DARK_RED, COLOR_DARK_GREEN, COLOR_DARK_BLUE, COLOR_DARK_MAGENTA, COLOR_DARK_YELLOW, COLOR_DARK_CYAN, COLOR_GRAY oder COLOR_LIGHT_GRAY in RichTextStyle (s. Kap. 8 – createColorObject) bzw. ein Wert zwischen 0 und 240 verwendet werden kann. RichTextStyle.STYLE_NO_CHANGE und RichTextStyle.MAYBE setzen bei Anwendung des Styles auf das RichTextItem die Farbe des Textes auf den zu-
EFFECTS_NONE – Zurücksetzen des Schrifteffekts. EFFECTS_SUPERSCRIPT – Hochgestellte Zeichen. EFFECTS_SUBSCRIPT – Tiefgestellte Zeichen. EFFECTS_SHADOW – Schattierte Zeichen. EFFECTS_EMBOSS – Verstärkte Zeichen. EFFECTS_EXTRUDE – Erweiterte Zeichen. STYLE_NO_CHANGE und MAYBE – Zurücksetzen auf den vorigen Effekt.
int getFont() throws NotesException void setFont(int fontConstant) throws NotesException Setzt bzw. liest die laufende Nummer des Fonts (s. Kap. 11.2 – getNotesFont). Als vordefinierte Konstanten stehen FONT_ROMAN, FONT_HELV und FONT_ COURIER in RichTextStyle zur Verfügung. RichTextStyle.STYLE_ NO_CHANGE und RichTextStyle.MAYBE setzen die Schrift bei Anwendung des RichTextStyle auf das RichTextItem auf die zuvor verwendete zurück. int getFontSize() throws NotesException void setFontSize(int sizeAsPoint) throws NotesException
Setzt bzw. liest die Größe der Schrift in Punkt. Zugelassen sind Werte von 1 bis 250. STYLE_NO_CHANGE und MAYBE setzen die Schriftgröße bei Anwendung des RichTextStyle auf das RichTextItem auf die zuvor verwendete zurück. •
int getBold() throws NotesException void setBold(int yesOrNo) throws NotesException
Setzt bzw. liest die Schrifteigenschaft „Fettdruck“. Zugelassen sind die Werte RichTextStyle.YES und RichTextStyle.NO. RichTextStyle.STYLE_NO_CHANGE und RichTextStyle.MAYBE setzen die Fettschrift bei Anwendung des RichTextStyle auf das RichTextItem auf den zuvor verwendeten Zustand zurück. •
int getItalic() throws NotesException void setItalic(int yesOrNo) throws NotesException
Setzt bzw. liest die Schrifteigenschaft „Kursivdruck“. Zugelassen sind die Werte RichTextStyle.YES und RichTextStyle.NO. RichTextStyle.STYLE_NO_CHANGE und RichTextStyle.MAYBE setzen den Kursivdruck bei Anwendung des RichTextStyle auf das RichTextItem auf den zuvor verwendeten Zustand zurück. •
int getStrikeThrough() throws NotesException void setStrikeThrough(int yesOrNo) throws NotesException
Setzt bzw. liest die Schrifteigenschaft „durchgestrichen“. Zugelassen sind die Werte RichTextStyle.YES und RichTextStyle.NO. RichTextStyle.STYLE_NO_CHANGE und RichTextStyle.MAYBE setzen den Schriftstil „durchgestrichen“ bei Anwendung des RichTextStyle auf das RichTextItem auf den zuvor verwendeten Zustand zurück. •
Setzt bzw. liest die Schrifteigenschaft „unterstrichen“. Zugelassen sind die Werte RichTextStyle.YES und RichTextStyle.NO. RichTextStyle.STYLE_NO_CHANGE und RichTextStyle.MAYBE setzen den Schriftstil „unterstrichen“ bei Anwendung des RichTextStyle auf das RichTextItem auf den zuvor verwendeten Zustand zurück. •
int getPassThruHTML() throws NotesException void setPassThruHTML(int yesOrNo) throws NotesException
Setzt bzw. liest die Schrifteigenschaft „Durchgangs-HTML“. Zugelassen sind die Werte RichTextStyle.YES und RichTextStyle.NO. RichTextStyle. STYLE_NO_CHANGE und RichTextStyle.MAYBE setzen den Schriftstil „Durchgangs-HTML“ bei Anwendung des RichTextStyle auf das RichTextItem auf den zuvor verwendeten Zustand zurück. Durchgangs-HTML ist eine Besonderheit im Domino-RichText. Dieser Schriftstil zeigt an, dass Text beim Rendern für den Webbrowser nicht HTML escaped wird, d.h. direkt als HTML an den Browser gegeben wird. •
boolean isDefault() throws NotesException
Neu seit Domino R6.5. Zeigt an, ob alle Schriftstileigenschaften dem Standard entsprechen. •
Session getParent() throws NotesException Liefert die Session, in der der RichTextStyle erzeugt wurde.
11.11
Zusammenfassung
RichText ist eine der herausragenden Besonderheiten von Domino im Vergleich zu anderen Datenbanksystemen; egal welcher Inhalt gespeichert werden soll, seien es Mediaelemente, gestalteter Text, Dateianhänge oder OLE-Objekte. Von Text bis Tabellen speichert RichText alles, was an Daten in einer Office-Umgebung denkbar ist. Während für Domino R6 RichText noch recht mühsam über das API beherrscht werden konnte, steht seit Domino R6.5 ein breites Framework zur Verfügung, mit dem alle benötigten Funktionen und Objekte im RichText verfügbar werden. Selbst die programmatische Erzeugung von RichText-Tabellen oder umgekehrt deren Export sind leicht zu handhaben, wie in ausführlichen Beispielen in diesem Kapitel aufgezeigt wurde – ein Unterfangen, das einen Domino-R5-Programmierer noch vor unlösbare Aufgaben gestellt hätte. In Kombination mit den neuen DB2-Funktionen in R7 lässt sich so das beste aus den verschiedenen Welten – RichText und RichMedia und relationale Struktur- und Enterprisedaten – in einem API zusammenfassen.
12
12 Weitere Objekte
In diesem Kapitel: Domino-Namen Datums- und Zeitobjekte Internationale Einstellungen Agent API
464
12 Weitere Objekte
12.1
Das Name-Objekt
Domino verwendet, wie bereits einführend in Kapitel 2.2.4 erläutert, für Namen ein kanonisches Namensformat, das auf X.400-Attributen basiert. Domino unterstützt nicht alle im X.400-Format definierten Komponenten eines Namens und kennt neben der kanonischen Form (CN=Vorname Nachname/OU=Organisationseinheit/O= Organisation) auch eine abgekürzte Form (Vorname Nachname/Organisationseinheit/Organisation). CN Benutzername (Common Name) OU Organisations(unter)einheit (Organizational Unit – auch als OU1 bis OU4) O Organisation (Organization) C Land (Country) G Nachname (Given Name) I Kürzel des zweiten Namens (Initial) S Vorname (Surname) Q Generationskennzeichen (z.B. Jr. – Generation Qualifier) P Private Management Domain Name (PRMD) A Administration Management Domain Name (ADMD) Domino unterstützt von diesem Standard die Bestandteile CN, OU, O und C, wobei statt OU1 bis OU4 bis zu viermal das Kennzeichen OU auftritt. Die Klasse Name dient dazu, Dominonamen und X.400 (auch ISO/IEC 10021 [ISO])-Namen ineinander umzuwandeln und die entsprechenden Bestandteile zu lesen. Sie ist in der Lage einfache (abgekürzte) und kanonische Namen ineinander umzuwandeln, sofern dies möglich ist. Im Gegensatz zur im X.400 üblichen Schreibweise, bei der die einzelnen Bestandteile durch Semikolon (oder wie für LDAP üblich durch Kommata) getrennt werden, verwendet diese Methode die für Domino gewohnte Trennung durch den Schrägstrich. Zusätzlich kennt die Klasse Name Internetnamen nach dem Standard RFC 821[RFC 821, SMTP] und 822 [RFC 822, ARPA Messages] und liefert zu E-Mail-Adressen nach diesem Standard die verschiedenen Bestandteile. Ein Name-Objekt wird in Session über createName erzeugt oder z.B. über getUserNameList() und getUserNameObject() als Objekt in Session bezogen. Alle Methoden dieser Klasse arbeiten gewissermaßen „blind“ und interpretieren lediglich den z.B. über den createName erzeugten Namen. Wenn also z.B. über createName Komponenten (z.B. Q für das Generationskennzeichen) eingegeben werden, so ist diese Klasse sehr wohl in der Lage diesen über getGeneration zu extrahieren. So würde z.B. getGeneration aus dem kanonischen Namen „CN=Vorname Nachname/Q=Jr./OU2=OrganisationseinheitZwei/O=ACompany/C=de“ korrekt die Generation als „Jr.“ lesen. Natürlich ist der Klasse Name auch die abgekürzte (abbreviated) Form von Domino bekannt, so wird aus „Vorname Nachname/OrganisationseinheitZwei/ACompany/de“ durch getOrgUnit1() korrekt die Organisationseinheit als „OrganisationseinheitZwei“ ausgelesen. Falls über createName ein Name-Objekt aus einem abgekürzten Namen erstellt wird (Meinname/Firma/DE), so wird der rechte Bestandteil nur dann als Landesbe-
12.1 Das Name-Objekt
465
standteil zurückgegeben, falls dieser aus nur zwei Zeichen besteht. Andernfalls wird der rechte Bestandteil als Organisation (O) zurückgegeben. Besteht der Name aus mehr als zwei Bestandteilen, so werden rechts entweder das Land (falls zwei Zeichen) oder die Organisation angenommen und dann die jeweils links davon befindlichen Bestandteile den Unterorganisationen zugeordnet. Die am weitesten rechts stehende Unterorganisationseinheit wird OU1 zugeordnet. Der linke Bestandteil des Namens wird dem Benutzernamen, dem Common Name (CN), zugeordnet. Meinname/Firma/DE -> CN=Meinname/O=Firma/C=DE Meinname/Firma/CDE -> CN=Meinname/OU=Firma/O=CDE Werden in einem abgekürzten Namen insgesamt mehr als sieben durch den Schrägstrich getrennte Bestandteile (CN, OU1..4, O und C) übergeben, so werden die sechs rechts liegenden Bestandteile als OU1..4, O und C interpretiert, der linke Bestandteil als Benutzername CN. Der Stringparameter für createName in Session sollte entweder einen abgekürzten Namen, einen kanonischen Namen oder einen Internetnamen enthalten, wobei optionale Bestandteile dieser Namen weggelassen werden können. Wurden ungültige Namen übergeben, so werden alle getter-Methoden den leeren String zurückgeben. Beispiele: a) Notes-Name (kanonisch – vollständig) CN=Vorname Nachname/OU=Verkauf/OU=Norddeutschland/O=Friesen AG/ C=DE b) Notes-Name (abbreviated – abgekürzt) Vorname Nachname/Verkauf/Norddeutschland/Friesen AG/DE c) X.400 Name G=Nachname; I=MiddleInitial; S=Vorname; Q=Jr.; CN=Vorname Nachname; OU2=Verkauf; OU1=Norddeutschland; O=Friesen AG; PRMD=Hauspost; ADMD=Post; C=de Dieser Name würde über die Klasse Name allerdings leicht verändert (Verwendung des Schrägstriches, OU statt OU1 und OU2 und Abkürzung der Kennzeichen PRMD und ADMD) dargestellt: G=Nachname/I=MiddleInitial/S=Vorname/Q=Jr./CN=Vorname Nachname/ OU=Verkauf/OU=Norddeutschland/O=Friesen AG/P=Hauspost/A=Post/C=de d) Internetname Vorname Nachname(Verkaufsleiter)
466
12 Weitere Objekte
Diese Methode ist nicht zu verwechseln mit dem Nachschlagen von Benutzerinformationen im Domino-Verzeichnis. Diese Methode weiß nur das über einen Namen, was über den createName eingegeben wurde. Wird ein Name z.B. über getUserNameObject geliefert, ist auch dies lediglich eine Objektrepräsentation eines kanonischen Domino-Namens – unabhängig von eventuell zusätzlich über diesen Benutzer bekannten Informationen im Adressbuch. Das Name-Objekt dient also lediglich der Umformung verschiedener Namensbestandteile und ist nicht etwa eine Kapselung eines Benutzerdokuments aus dem Domino-Verzeichnis (names.nsf).
12.1.1 •
Methoden in Name für RFC 821 und 822 Namen
String getAddr821() throws NotesException
Liefert die E-Mail-Adresse eines Internetnamens. Diese ist nur im Name-Objekt bekannt, wenn sie ausdrücklich als Internetname durch createName übergeben wurde. Das Name-Objekt schlägt keine Internetadressen von angemeldeten Benutzern nach. Ein Internetname hat die Form: [email protected] Phrase jeweils gefolgt von ein bis drei Kommentaren, die in Klammern () dargestellt werden, z.B.: DJBuch User(Java) (Domino) Die Methode gibt die E-Mail-Adresse des Internetnamens (hier djbuch@ djbuch.de) ohne Phrase oder Kommentare zurück. Ist die E-Mail Adresse ungültig, wird ein leerer String zurückgegeben. •
Liefert die optionalen Kommentare eines Internetnamens.
12.1.2 Methoden in Name für Notesnamen (X.400) 12.1.2 •
467
Methoden in Name für Notesnamen (X.400)
String getAbbreviated() throws NotesException
Liefert die abgekürzte Form des Namens, sofern dieser eindeutig ermittelbar ist. Für z.B. name = session.createName("CN=Mein Name/OU=Organisationseinheit") gibt es keine sinnvolle Kurzform („Mein Name/Organisationseinheit“ würde die Organisationseinheit zur Organisation machen). Allerdings sind dieser Methode Grenzen gesetzt. Unsinnige Namen, wie z.B. CN= Mein Name/OU=Orga/C=DE (fehlende Organisation bei Nennung des Landes) werden nicht korrekt aufgelöst (hier würde Mein Name/Orga/DE zurückgegeben). Wird über createName ein Name mit Bestandteilen erzeugt, die Domino nicht kennt, so liefert getAbbreviated nicht den abgekürzten, sondern den kanonischen Namen. Internetnamen (RFC 821, 822) oder nicht hierarchische Namen werden unverändert zurückgegeben. •
String getCountry() throws NotesException
Liefert den Landesbestandteil (C) des Namens oder den leeren String, falls dieser Bestandteil nicht im Namen vorhanden ist und für nicht hierarchische und RFC 821, 822 Namen. •
String getCanonical() throws NotesException
Liefert den Namen als kanonischen Namen, d.h. alle Bestandteile werden mit Ihrem Kennzeichen (CN= usw.) angezeigt. Internetnamen (RFC 821, 822) oder nicht hierarchische Namen werden unverändert zurückgegeben. •
String getCommon() throws NotesException
Liefert den Benutzernamen (CN) des Namens. Liefert den leeren String, falls dieser Bestandteil nicht im Namen vorhanden ist. Für Internetnamen wird, sofern vorhanden, der beschreibenden Namen (Phrase), ansonsten der local-Teil des Internetnamens (links vom @-Zeichen) zurück gegeben. Nicht hierarchische Namen werden unverändert zurückgegeben. •
String getGeneration() throws NotesException
Liefert das Generationskennzeichen (Q) oder den leeren String, falls dieser Bestandteil nicht im Namen vorhanden ist und für nicht hierarchische und RFC 821, 822 Namen. •
String getGiven() throws NotesException Liefert den Nachnamen (G) oder den leeren String, falls dieser Bestandteil
nicht im Namen vorhanden ist und für nicht hierarchische und RFC 821, 822 Namen. •
String getSurname() throws NotesException Liefert den Vornamen (S) oder den leeren String, falls dieser Bestandteil nicht
•
String getInitials() throws NotesException
im Namen vorhanden ist und für nicht hierarchische und RFC 821, 822 Namen. Liefert den (Anfangsbuchstaben des) zweiten Vornamen(s) oder den leeren String, falls dieser Bestandteil nicht im Namen vorhanden ist und für nicht hierarchische und RFC 821, 822 Namen.
468 •
12 Weitere Objekte String getKeyword() throws NotesException
Erzeugt ein Schlüsselwort, das die organisatorischen Bestandteile des Namens durch den Backslash getrennt anzeigt. Bestandteil sind, sofern vorhanden, das Land, die Organisation und alle organisatorischen Untereinheiten in der Reihenfolge C\O\OU1\OU2\OU3\OU4. •
String getLanguage() throws NotesException
Domino unterstützt neben dem allgemeinen Benutzernamen (CN) alternative Namen. Diesen kann ein kombinierter Sprach- und Ländercode zugeordnet werden, der ein Land und in der Regel eine Sprache referenziert. Für die deutsche Sprache z.B. gibt es die Ländercodes de-AT, de-CH, de-DE, deLI und de-LU, wobei einerseits jeweils die deutsche Sprache („de-“) und andererseits die jeweiligen Länder Österreich, Schweiz, Deutschland, Liechtenstein und Luxemburg bezeichnet werden. Die Sprach- und Ländercodes werden miteinander kombiniert, so stehen selbstverständlich z.B. für die Schweiz zusätzlich die Codes fr-CH und it-CH zur Verfügung. Ein Name in Verbidung mit einem Sprach- und Ländercode wird über createName (String name, String countryCode) erzeugt, wobei der Ländercode als String im zweiten Parameter countryCode übergeben wird. •
String getOrganization() throws NotesException
Liefert die Organisation (O) oder den leeren String, falls dieser Bestandteil nicht im Namen vorhanden ist und für nicht hierarchische und RFC 821, 822 Namen. •
Liefert die vier Organisationseinheiten (OU1.. OU4) oder den leeren String, falls dieser Bestandteil nicht im Namen vorhanden ist und für nicht hierarchische und RFC 821, 822 Namen. Wird ein Name-Objekt aus einem abgekürzten Namen erzeugt, so wird die am weitesten rechts stehende Organisationseinheit der OU1 zugeordnet. •
Session getParent() throws NotesException
Liefert die Domino-Session, in der das Name-Objekt erzeugt wurde. •
Zu Zeiten der Spezifizierung der X.400-Namen wurden auch elektronische Nachrichten noch in einem (staatlich) regulierten Postumfeld definiert. Daher benötigte jeder Name eine administrative Instanz (ADMD – ADministrativ Management Domain), der er zugeordnet war. Hausinterne Nachrichtensysteme konnten über private Managementsysteme verfügen und PRMD (PRivate Management Domain) zuordnen. •
boolean isHierarchical() throws NotesException
Zeigt an, ob ein Name hierarchisch oder flach aufgebaut ist.
12.2 Arbeiten mit Zeit-Objekten
12.2
469
Arbeiten mit Zeit-Objekten
Die Arbeit mit Zeit- und Datum-Objekten basiert in Domino auf den beiden Klassen DateTime und DateRange, wobei erstere eine Zeit und/oder ein Datum beschreibt und letztere einen Zeitraum definiert. Erzeugt werden diese Objekte durch createDateTime und createDateRange in Session. Da Domino sich schon immer als international eingesetztes Tool verstanden hat, berücksichtigt das DateTime-Objekt sämtliche Unwägbarkeiten, die einem Programmierer bei der Benutzung von Datum und Zeit begegnen können. So sind neben den üblichen Zeitzonen auch die unterschiedlichen Sommer- und Winterzeitumstellungen berücksichtigt, jeweils abhängig von den im Betriebssystem des Servers oder in Ausnahmefällen in dessen notes.ini-Datei spezifizierten lokalen Einstellungen. Bei der Arbeit mit Sommer- und Winterzeit, rechnet die Klasse DateTime bei Zeit-Kalkulationen normalerweise immer mit der internen Uhr, die nach GMT77 arbeitet. Die lokale Zeit wird bei Übergang zwischen Winter- und Sommerzeit und umgekehrt entsprechend eine Stunde vor oder zurückgestellt. Hierdurch wird erreicht, dass die getter-Methoden für lokale Zeit das Verhalten einer „echten“ Uhr während der Umstellung wiederspiegeln. Folglich wird z.B. am Morgen des Übergangs von Winter- auf Sommerzeit, wenn zu der Uhrzeit 1:00 Uhr zwei Stunden hinzugezählt werden, als Ergebnis 4:00 Uhr ausgegeben, da um zwei Uhr (in Deutschland) die Uhr um eine Stunde vorgestellt wird und somit das eigentliche Ergebnis (3:00 Uhr) gleich an die Sommerzeit angepasst wird. Gleiches gilt für Datumsanpassungen, die die Sommer-, Winterzeitgrenze überschreiten. Wird also zu einem DateTime-Objekt, das die Uhrzeit 3:00 Uhr und den Tag vor der Umstellung repräsentiert, ein Tag hinzugezählt, so wird der (lokalen) Uhrzeit eine Stunde hinzugezählt und als lokale Zeit 4:00 Uhr angegeben, da ja zwischenzeitlich die Uhr eine Stunde vorgestellt wurde und schließlich ein ganzer Tag (also 24 Stunden) vorwärtsgezählt werden sollte. Anders gesprochen wird so gerechnet, dass die Operationen tatsächlich verstrichener Zeit entsprechen. Dieses Verhalten wird als Default angenommen, da es den normalen Bedürfnissen entspricht (Wenn Sie um 1:00 Uhr Winterzeit am Umstellungsmorgen eine Reise von 2 Stunden antreten, wird danach Ihre Uhr tatsächlich 4:00 Uhr Sommerzeit anzeigen). Über einen optionalen Parameter preserveLocalTime kann für viele der Methoden erreicht werden, dass diese Anpassungen der lokalen Zeit nicht durchgeführt werden und nur rein rechnerisch die Uhrzeit um eine entsprechende Stundenzahl vorgestellt wird, wobei dann natürlich die intern geführte Uhrzeit für GMT korrigiert wird. Einige Beispiele finden Sie im Code zum Buch in der Klasse DateTimeDemo im package djbuch.kapitel_12. Die Arbeit mit DateTime wurde bereits in den Kapiteln 2.2.4 und 7.1ff. beschrieben, so dass die folgende Zusammenfassung zu Ihrer Übersicht dient und noch einige bisher nicht besprochene Besonderheiten erläutert.
77
Greenwich Mean Time, auch General Mean Time – Sonnenzeit am Nullmeridian.
Die verschiedenen adjust-Methoden passen die jeweiligen Parameter des Zeitbzw. Datumanteils des DateTime-Objektes an, wobei die Methoden adjustHour, adjustMinute und adjustSecond (für Stunde, Minute und Sekunden) keine Auswirkung haben, falls ein DateTime-Objekt keinen Zeitanteil hat (siehe setAnyTime), und die Methoden adjustDay, adjustMonth und adjustYear (für Tag, Monat und Jahr), falls das DateTime-Objekt keinen Datumsanteil (siehe setAnyDate) hat. Der Parameter amount kann positiv oder negativ sein und die jeweiligen Ergebnisse schreiten dementsprechend in der Zeit vor oder zurück. Der Parameter preserveLocalTime hat die oben beschriebene Funktion zur Erhaltung der lokalen Zeit (Datum) beim Überschreiten der Umstellung zwischen Sommer- und Winterzeit. Sämtliche Methoden arbeiten im Zusammenhang und veranlassen die Anpassungen der übrigen Parameter, sofern dies bei der Überschreitung von Minuten-, Stunden-, Tages-, Monats- oder Jahresgrenzen notwendig ist; so wird z.B. aus 31.12.2005 23:59:59 der 1.1.2006 00:00:00, wenn man dem ersten Datumsobjekt über adjustSecond (1) eine Sekunde hinzufügt. •
Konvertiert die Zeitzone des Zeitobjektes durch den Parameter newZone, der Werte von -12 bis 12 annehmen darf. Diese Klasse verändert nicht die im Objekt enthaltene GMT oder die lokalen Datums- und Zeitangaben, sondern führt nur intern diese neue Zeitzone.
12.2.1 Methoden in DateTime
471
Reflektiert wird das durch den Rückgabewert der Methode getZoneTime (liefert diejenige Zeit, die gerade in der Zone herrscht, die durch convertToZone gesetzt wurde), die einen String enthält, der als Text die Zeitzone anzeigt (z.B. ZE10 im Zeitobjekt 14.06.2005 20:00:00 ZE10). Wird für newZone ein negativer Wert angegeben, so wird eine Verschiebung nach Osten im Verhältnis zu GMT erreicht, ansonsten nach Westen. Die Zeitzonenkürzel werden östlich von GMT als CET (Central European Time) für eine Stunde Verschiebung und als ZE2 bis ZE12 (Zone East) für die weiteren östlichen Verschiebungen bezeichnet. Westlich von GMT werden die ersten drei Zonen als ZW1 bis ZW3 (Zone West) bezeichnet. Die übrigen Kürzel unterliegen international stark unterschiedlichen Gepflogenheiten, Domino nutzt folgende: AST (4 h westl. v. GMT) – Atlantic Standard Time EST (5 h westl. v. GMT) – Eastern Standard Time CST (6 h westl. v. GMT) – Central Standard Time MST (7 h westl. v. GMT) – Mountain Standard Time PST (8 h westl. v. GMT) – Pacific Standard Time YST (9 h westl. v. GMT) – Yukon Standard Time HST (10 h westl. v. GMT) – Hawaii Standard Time BST (11 h westl. v. GMT) – Bering Standard Time und ZW12 für die Zone 12 Stunden westl. von GMT. • • • •
void setAnyDate() throws NotesException Entfernt den Datumsanteil aus dem DateTime-Objekt. void setAnyTime() throws NotesException Entfernt den Zeitanteil aus dem DateTime-Objekt. void setNow() throws NotesException Setzt die Zeit des DateTime-Objekts auf die aktuelle Systemzeit. int timeDifference(DateTime early) throws NotesException Berechnet die Zeitdifferenz zwischen zwei DateTime-Objekten. Hierbei wird der „Abstand“ ausgehend von baseDateTime zur Zeit des aufrufenden Objektes zu-
rückgegeben. Beim Aufrufen von late.timeDifference (early) wird ein positiver Wert zurückgegeben, wenn das die Methode aufrufende DateTime-Objekt late eine spätere Zeit enthält als der Parameter early. Andernfalls wird ein negativer Wert zurückgegeben (oder 0). Die Berechnung kann vereinfacht als „late minus early“ betrachtet werden. •
double timeDifferenceDouble(DateTime datetime) throws NotesException Arbeitet wie timeDifference, jedoch wird das Ergebnis als double zurückge-
•
void setLocalDate(int year, int month, int day) throws NotesException void setLocalDate(int year, int month, int day, boolean preserveLocalTime) throws NotesException
geben.
472
12 Weitere Objekte Setzt das lokale Datum entsprechend den Parametern year, month und day auf das entsprechende Jahr, Monat und Tag, wobei bei Überschreiten der SommerWinterzeitgrenze wie oben beschrieben verfahren wird. Der Parameter preserveLocalTime arbeitet analog.
•
String getLocalTime() throws NotesException void setLocalTime(String s) throws NotesException void setLocalTime(int hour, int minute, int second, int hundredth) throws NotesException void setLocalTime(Date date) throws NotesException void setLocalTime(Calendar calendar) throws NotesException Die Signatur setLocalTime (Calendar) ist neu seit Domino R6. Stellt die durch das DateTime-Objekt repräsentierte Zeit neu ein bzw gibt diese zurück. Hierbei kann die Zeit entweder durch die Parameter hour, minute, second oder hundredth diskret in Stunden, Minuten, Sekunden und hundertstel Sekunden, über einen die Zeit beschreibenden String oder über ein Objekt der Klasse java.util.date oder java.util.Calendar gesetzt werden. Wer-
den für die Zeit Hundertstel angegeben, hat dies dennoch keine Auswirkung auf Berechnungen mit timeDifference oder timeDifferenceDouble. Wird ein Date- oder Calendar-Objekt übergeben, so werden sowohl Datum als auch Zeit neu gesetzt. Wird ein String für setLocalTime übergeben, so kann dieser entweder die Zeit und/oder das Datum als String enthalten (z.B. „17.07.2005 19:03 CEDT“) oder es können auch beschreibende Strings in der Form „Today“, „Tomorrow“, „Yesterday“, „Heute“, „Morgen“ oder „Gestern“ übergeben werden (siehe getToday, getYesterday und getTomorrow in International). Wird eine Uhrzeit als ausgeschriebener String übergeben, so kann die Zeitzone mit dem für convertToZone beschriebenen Zeitzonenkürzel übergeben werden. Die so übergebene Zeit einer anderen Zeitzone wird automatisch in die aktuelle des Systems überführt. Auch hierbei nimmt Domino Korrekturen entsprechend der Sommer- und Winterzeit vor. Wird z.B. der String „17.07.2005 19:03 CET“ übergeben, so konvertiert Domino die übergebene Winterzeit (CET) automatisch in 20:03 Sommerzeit (CEDT). •
String getGMTTime() throws NotesException
Liefert die GMT-Zeit, die durch das DateTime-Objekt repräsentiert wird. •
boolean isDST() throws NotesException Gibt true zurück, falls durch das DateTime-Objekt eine Zeit repräsentiert wird,
•
int getTimeZone() throws NotesException Gibt die Zeitzone der durch das DateTime-Objekt repräsentierten Zeit zurück. Diese wird als int Wert geliefert und stellt den Abstand in Stunden zu GMT dar. String getZoneTime() throws NotesException Wurde die Zeit durch convertToZone in eine andere Zeitzone verschoben, so gibt getZoneTime die in dieser Zeitzone gültige Zeit zurück, wobei der zurückgegebene String ein angehängtes Zeitzonenkürzel enthält. Siehe convertToZone.
die zur Zeit auf Sommerzeit gestellt ist.
•
12.2.2 Methoden in DateRange • • • •
String getDateOnly() throws NotesException Liefert das durch das DateTime-Objekt repräsentierte Datum als String. String getTimeOnly() throws NotesException Liefert die durch das DateTime-Objekt repräsentierte Zeit als String. Session getParent() throws NotesException Liefert die Session, in der das DateTime-Objekt erstellt wurde. Date toJavaDate() throws NotesException Wandelt das DateTime-Objekt in ein java.util.Date-Objekt um.
Setzt bzw. liest die Startzeit (das Startdatum) des Zeitraumes, der durch das DateRange-Objekt repräsentiert wird. Der DateRange ist nur vollständig, solange sowohl setStartDateTime als auch setEndDateTime gesetzt wurden. Startund Endzeitpunkt müssen nicht chronologisch sein, dürften aber spätestens bei der Anzeige durch ein Feld vom Datentyp Zeitraum in einer Maske Schwierigkeiten machen, da dieses Feldelement die Reihenfolge überprüft. •
Setzt bzw. liest die Endzeit (das Enddatum) des Zeitraumes, der durch das DateRange-Objekt repräsentiert wird. Der DateRange ist nur vollständig, solange sowohl setStartDateTime als auch setEndDateTime gesetzt wurden. Startund Endzeitpunkt müssen nicht chronologisch sein, dürften aber spätestens bei der Anzeige durch ein Feld vom Datentyp Zeitraum in einer Maske Schwierigkeiten machen, da dieses Feldelement die Reihenfolge überprüft. • •
Session getParent() throws NotesException Liefert die Domino-Session, in der das DateRange-Objekt erzeugt wurde. String getText() throws NotesException Gibt den DateRange als String zurück, sofern sowohl Start- als auch Endzeit-
punkt gesetzt wurden. •
void setText(String rangeAsText) throws NotesException Setzt gleichzeitig Start- und Endzeitpunkt des DateRange, wobei als Argument ein String bestehend aus zwei Zeitangaben, getrennt durch einen Whitespace
oder „ - “ übergeben wird. Zulässig sind z.B. „19:00 YST\t20:00 CET“ oder „Today 19:00 - Tomorrow 20:00“.
474
12 Weitere Objekte
12.3
Arbeiten mit internationalen Einstellungen
Domino-Server und alle Notes Clients verwenden seit Domino R6.x so weit möglich die lokalen Einstellungen des Betriebssystems, um lokalisierte Parameter für Zeit und Währung zu erhalten. Um diese auslesen zu können, kann über getInternational in Session ein Objekt der Klasse International bezogen werden, das alle von Domino gelesenen und verwendeten Einstellungen zur Verfügung stellt. So kann z.B. über getTimeZone in International die aktuelle Zeitzone ermittelt werden und, falls notwendig, in DateTime zum Einsatz gebracht werden, wobei an dieser Stelle angemerkt sei, dass bereits DateTime versucht beim neuen Erzeugen von DateTime-Objekten lokale Einstellungen zu respektieren. Die Klasse International stellt sämtliche Parameter nur zur Information zur Verfügung und erlaubt es nicht, diese Einstellungen zu ändern.
12.3.1 •
Methoden in International
String getAMString() throws NotesException
Liefert ein Kürzel, das bei einer 12-Stunden-Uhr den Vormittag kennzeichnet. •
String getPMString() throws NotesException
Liefert ein Kürzel, das bei einer 12-Stunden-Uhr den Nachmittag kennzeichnet. •
int getCurrencyDigits() throws NotesException
Liefert die Anzahl von Nachkommastellen, die für Währungen verwendet werden. •
String getCurrencySymbol() throws NotesException
Liefert das Symbol für die Währung. •
String getDateSep() throws NotesException
Liefert das Trennzeichen für Datumsangaben. •
String getDecimalSep() throws NotesException
Liefert das Dezimaltrennzeichen. •
String getThousandsSep() throws NotesException
Liefert das Tausender-Trennzeichen. •
String getTimeSep() throws NotesException
Liefert das Trennzeichen für Zeitangaben. • •
int getTimeZone() throws NotesException Liefert die Zeitzone in Stunden. Siehe auch convertToZone in DateTime String getToday() throws NotesException Liefert den String, der in der lokalen Sprache verwendet wird, um den heuti-
gen Tag zu kennzeichnen (z.B. „Heute“ oder „Today“). Dieser String kann z.B. in •
•
createDateTime (String) in Session verwendet werden. String getTomorrow() throws NotesException Liefert den String, der in der lokalen Sprache verwendet wird, um den nächsten Tag zu kennzeichnen (z.B. „Morgen“ oder „Tomorrow“). Dieser String kann z.B. in createDateTime (String) in Session verwendet werden. String getYesterday() throws NotesException
12.4 Methoden in Agent
475
Liefert den String, der in der lokalen Sprache verwendet wird, um den vorigen Tag zu kennzeichnen (z.B. „Gestern“ oder „Yesterday“). Dieser String kann z.B. in createDateTime (String) in Session verwendet werden. •
boolean isCurrencySuffix() throws NotesException Zeigt an, ob das Währungssymbol nach (Rückgabewert true) oder vor der Zahl
•
boolean isCurrencySpace() throws NotesException
einer Währung ausgegeben wird. Zeigt an, ob zwischen Zahl und Symbol in Währungen ein Leerzeichen verwendet wird. •
boolean isCurrencyZero() throws NotesException
Zeigt an, ob Währungsangaben, die zwischen Null und Eins liegen, mit oder ohne eine führende Null angezeigt werden. •
boolean isDateMDY() throws NotesException Liefert true, falls Datumsangaben in der Reihenfolge Monat-Tag-Jahr ausgege-
ben werden. •
boolean isDateDMY() throws NotesException Liefert true, falls Datumsangaben in der Reihenfolge Tag-Monat-Jahr ausgege-
ben werden. •
boolean isDateYMD() throws NotesException Liefert true, falls Datumsangaben in der Reihenfolge Jahr-Monat-Tag ausgege-
ben werden. • •
boolean isDST() throws NotesException Liefert true, falls die aktuelle Zeit Sommerzeit ist. boolean isTime24Hour() throws NotesException
Zeigt an, ob die Zeit als 24- oder als 12-Stundenuhr verwendet wird. •
Session getParent() throws NotesException Liefert die Session, in der das International-Objekt bezogen wurde.
12.4
Methoden in Agent
Die Verwendung von Agenten in Domino, die über den Domino Designer implementiert werden, finden Sie in Kapitel 4.10ff. Der Vollständigkeit halber und als praktische Übersicht finden Sie hier eine Kurzübersicht über alle Methoden dieser Klasse. •
void run() throws NotesException void run(String noteID) throws NotesException Führt den durch das Agent-Objekt repräsentierten Agenten aus. Bei einer lokalen Session wird er auf dem aufrufenden Computer, bei Remote-Sessions (DII-
OP) auf dem IIOP-Server ausgeführt. Der optionale Parameter noteID ermöglicht die Übergabe einer Note ID eines Dokuments (siehe getNoteID() in Document), das als Parameterübergabecontainer genutzt werden und dann vom gestarteten Agenten über db.getDocumentByID (agent.getParameterDocID()) geöffnet werden kann, wobei die Variable db ein Database- und agent ein Agent-Objekt ist. (s. Kap. 4.15).
476 •
12 Weitere Objekte int runOnServer() throws NotesException int runOnServer(String noteID) throws NotesException Startet den durch das Agent-Objekt repräsentierten Agenten auf dem Server, auf
dem sich die diesen Agenten enthaltende Datenbank befindet bzw. auf dem lokalen Computer, wenn diese Datenbank lokal liegt. Für den Parameter noteID, siehe run(). Rückgabewert ist 0, falls der Agent erfolgreich gestartet werden konnte. •
void remove() throws NotesException
Löscht den Agenten (sein Gestaltungsdokument) endgültig aus der Datenbank. •
void save() throws NotesException Speichert die durch setServerName oder setEnabled gemachten Änderun-
gen. Durch das Speichern wird als Eigentümer der Benutzer der aktuellen Session eingetragen, wobei diese Änderung des Eigentümers erst mit Aufbau einer neuen Session reflektiert wird. •
String getName() throws NotesException
Gibt den Namen des Agenten zurück. Dieser wird als String bestehend aus Klarname und Aliasname zurückgegeben, wobei der (die) Aliasname(n) durch das Pipesymbol „|“ getrennt wird. •
String getOwner() throws NotesException
Liefert den Eigentümer (den letzten Signierer bzw. den letzten, der den Agenten gespeichert hat) zurück. •
String getCommonOwner() throws NotesException
Gibt den Benutzernamen (Common Name) des Eigentümers (getOwner) des Agenten zurück. •
DateTime getLastRun() throws NotesException
Liefert Datum und Uhrzeit, zu denen dieser Agent (auf dem Server, auf dem sich die Datenbank befindet) ausgeführt wurde. •
Setzt, bzw. liest den Servernamen, auf dem dieser Agent (bei periodischer Ausführung) gestartet werden soll. Ist dieser Name der leere String (default), so wird der Agent (bei periodischer Ausführung) in Replik auf jedem Server gestartet, andernfalls nur auf dem spezifizierten. Diese Eigenschaft wird repliziert, d.h. es lässt sich bei replizierten Datenbanken entweder für einen oder für alle Server die periodische Ausführung aktivieren. Soll der Agent auf mehreren, aber nicht allen Servern ausgeführt werden, empfiehlt es sich, den Code des Agenten so zu überarbeiten, dass er nur noch aus einem Starter-Aufruf besteht und der übrige Code in Bibliotheken gepflegt wird. Dann ist es unkritisch mehrere Dubletten des Agenten anzulegen, ohne den Pflegeaufwand unnötig zu erhöhen. Diese Änderung muss durch save() in Agent gespeichert werden.
12.4 Methoden in Agent •
477
String getQuery() throws NotesException
Agenten können entweder für kein spezifisches Dokument, für alle Dokumente oder für speziell ausgewählte Dokumente (siehe getTarget) gestartet werden. Diese Selektion von Dokumenten steht in AgentContext über getUnprocessedDocuments zur Verfügung. getQuery gibt die SELECT-Formel des Agenten zurück, falls für diesen eine solche spezifiziert wurde (s. Kap. 7.1.4 und 3.4ff.). •
String getComment() throws NotesException
Im Domino Designer kann ein Kommentar zur Erläuterung für einen Agenten vergeben werden, der durch getComment ausgelesen werden kann. •
Database getParent() throws NotesException
Liefert die Datenbank, in der der Agent definiert ist. •
boolean isPublic() throws NotesException
Agenten können entweder als privat oder öffentlich definiert werden. Private Agenten stehen nur dem Eigentümer zur Verfügung. Privaten Agenten stehen nicht alle Targets und Trigger zur Verfügung. (s. Kap. 3.4ff.). Über isPublic kann festgestellt werden, ob ein Agent privat oder öffentlich ist. •
int getTrigger() throws NotesException
Agenten können auf verschiedene Weise automatisiert oder manuell gestartet werden. Diese Startarten werden über die so genannten Trigger festgelegt (s. Kap. 3.4.1) Die Trigger werden durch die Konstanten TRIGGER_NONE, TRIGGER_SCHEDU-
•
LED, TRIGGER_AFTER_MAIL_DELIVERY, TRIGGER_DOC_PASTED, TRIGGER_MANUAL, TRIGGER_DOC_UPDATE und TRIGGER_BEFORE_MAIL_DELIVERY in Agent repräsentiert. int getTarget() throws NotesException
Agenten können entweder für kein spezifisches Dokument, für alle Dokumente oder für speziell ausgewählte Dokumente gestartet werden. (s. Kap. 3.4.2). Die Targets werden durch die Konstanten TARGET_NONE, TAR-
•
GET_ALL_DOCS, TARGET_NEW_DOCS, TARGET_NEW_OR_MODIFIED_DOCS, TARGET_SELECTED_DOCS, TARGET_ALL_DOCS_IN_VIEW, TARGET_UNREAD_DOCS_IN_VIEW oder TARGET_RUN_ONCE in Agent repräsentiert. boolean isNotesAgent() throws NotesException boolean isWebAgent() throws NotesException
Für Agenten kann über das Kontextmenü im Domino Designer festgelegt werden, ob diese für den Notes Client und/oder einen Browser sichtbar und verwendbar sind. isNotesAgent liefert true, falls eine Ausführung im Notes Client erlaubt ist. isWebAgent liefert true, falls eine Ausführung über einen Browser erlaubt ist. •
String getURL() throws NotesException String getNotesURL() throws NotesException String getHttpURL() throws NotesException getNotesURL und getHTTPURL sind neu seit Domino R6.5. Gibt die Notes-
bzw. die WebURL des Agenten zurück, über die er via Notes Client bzw. Browser gestartet werden kann.
478
12 Weitere Objekte Diese Methoden arbeiten analog zu den gleichnamigen Methoden in Document (s. Kap. 7.1.4.2).
•
•
String getParameterDocID() throws NotesException Wird ein Agent über run oder runOnServer gestartet, kann optional eine Note ID eines Document übergeben werden, die durch getParameterDocID gelesen werden kann. Siehe run(). boolean isActivatable() throws NotesException
Neu seit Domino R6.5. Periodische Agenten können in den Sicherheitseinstellungen im ContextMenu die Eigenschaft „Allow user activation“ erhalten. Diese Eigenschaft ist neu seit Domino R6 und erlaubt es Benutzern mit Editor-Zugriff auf die Datenbank, die periodische Ausführung von Agenten zu starten oder zu beenden, ohne die Signatur (den Eigentümer) des Agenten zu verändern und ohne die hierfür normalerweise benötigten Rechte als Designer in der ACL erhalten zu müssen. •
String getOnBehalfOf() throws NotesException
Neu seit Domino-R6.5-Agenten können seit Domino R6 unter einer anderen Benutzerkennung als der durch die Signatur festgelegten gestartet werden. Hierfür müssen neben der Eigenschaft „run on behalf of“ in den Sicherheitseinstellungen des Agenten im Domino Designer spezielle Rechte im Serverdokument im Domino-Verzeichnis (names.nsf) vergeben werden (s. Kap. 4.16). •
Neu seit Domino R6.5. Seit Domino R6 können Dokumente und Designelemente (temporär) für den Zugriff durch Dritte gesperrt werden (locking). Ausführliche Erläuterungen zum Locking und Design-Locking finden Sie in Kapitel 7.7.
12.5
Zusammenfassung
Auch für viele Standardaufgaben hält Domino die notwendigen API-Methoden und Objekte bereit, so dass die Arbeit mit Datum, Zeit oder internationalisierten Einstellungen keine Schwierigkeiten macht. Der Agent als API-Klasse wurde in diesem Kapitel nochmals aufgegriffen, um ein Nachschlagewerk für die dort verfügbaren Methoden zu erhalten. Beim Einsatz von Agenten sind die Erläuterungen aus Kapitel 4.10ff wichtig.
13
13 Objekt- und Speichertechniken
In diesem Kapitel: lotus.domino.Document – Funktionen erweitern Domino-Objekte erweitern und implementieren Domino-Document cachen Domino-Objekte im Multithreading Domino und DB2
480
13 Objekt- und Speichertechniken
Das Objektmodell der Domino-Java-Objekte ist umfangreich und ausgereift. Dennoch ist jede Domino-Anwendung anders und früher oder später werden Anforderungen zu erfüllen sein, die nicht per se durch dieses Modell abgebildet werden. Folgerichtig werden also entweder die neu zu entwickelnden Klassen DominoObjekte – vermutlich allen voran das Document – als Property aufnehmen oder es wird die Anforderung erwachsen, das Datenkonzept Dominos, bestehend aus Session, Database, View und Document zu erweitern. Dieses Kapitel wird zwei mögliche Ansätze aufzeigen, um diese Erweiterungen zu ermöglichen. Alle Domino-Java-Objekte sind als Interface konzipiert, eigentlich die perfekte Basis für die Implementierung eigener Domino-Objekte. Da aber die Objekterzeugung außerhalb des eigenen Einflussbereiches stattfindet – Domino liefert über die NotesFactory entweder eine „out of the box“ erzeugte lotus.domino.cso.Session oder eine lotus.domino.local.Session, die verwendet werden muss (!) –, können die Interfaces de facto nicht durch eigene Klassen abgeleitet werden. Im folgenden Kapitel wird nicht nur eine Alternative hierzu aufgezeigt, sondern eine Möglichkeit besprochen, die es doch möglich machen wird, die Interfaces zu implementieren. Die dadurch entstehenden Chancen und Risiken werden diskutiert. Hieraus ergibt sich gewissermaßen als Nebenprodukt eine Technik, die es erlaubt, Domino-Document-Objekte einem schnellen Cache zuzuführen und sessionübergreifend zu nutzen. In diesem Zusammenhang wird erläutert, welche Besonderheiten zu beachten sind, wenn Domino-Anwendungen multithreaded aufgebaut werden. Domino-Datenbanken können ab Version 7 eine DB2-Datenbank als Datenbasis erhalten. Die hierfür notwendigen Techniken werden ebenso untersucht wie die Auswirkungen auf die Java-Objekte.
13.1
Erweitern der Objekt-Funktionalitäten
Es ist ein guter Ansatz für Domino-Java-Anwendungen, die die Domino-Objekte intensiv einsetzen, keine parallele Objekt- und Datenstruktur aufzubauen und womöglich ständig zwischen beiden Strukturen abzubilden, sondern die Domino-Objekte möglichst nahtlos zu integrieren. Oft wird daher direkt insbesondere auf dem Document gearbeitet und die Felder (Items) dieser Objekte werden direkt mit den Methoden replaceItemValue und getItemValue gesetzt und gelesen. Da diese immer über einen Namen, der als String übergeben werden muss, referenziert werden, wird hier eine nicht zu vernachlässigende Quelle für Fehler geschaffen, die durch Schreibfehler in Itemnamen verursacht werden. Die erste Bestrebung muss also sein, sämtliche in Domino durch Strings abgebildeten Bezeichner – hier sollten auch Ansichten- und Maskenaliase und, sofern möglich und sinnvoll, auch Datenbank- und Servernamen einbezogen werden – in Konstanten oder Konfigurationen zentral abzubilden.
13.1.1 Erweiterung der Document-Funktionalitäten
481
Best Practice Items sollten in Dokumenten nie über ihren Namen, sondern zumindest über Konstanten angesprochen werden. So werden Tippfehler vermieden und es ist sichergestellt, dass alle Bestandteile einer Anwendung Gleiches immer mit gleichem Namen bezeichnen – auch in unterschiedlichen Dokumententypen. Häufig genutzte Items sollten über getter- und setter-Methoden abgebildet werden. So können implizite Plausibilitätschecks durchgeführt werden. Die implizite Verwendung von save() in derartigen setter-Methoden sollte aus Performancegesichtspunkten vermieden werden.
Die Zuweisung document.replaceItemValue("F_Titel", "Willkommen"); document.save();
wird dann zu private static final String ITEM_TITLE = "F_Titel"; ... document.replaceItemValue(ITEM_TITLE, "Willkommen"); document.save();
Aber auch derartige Konstanten sind kompliziert zu pflegen und vor allem werden sie nicht nahtlos vom pre-typing eines IDE unterstützt. Man würde also eine Lösung folgender Art erwarten: document.setTitle ("Willkommen"); document.save();
Optimal wäre also eine Lösung, die einerseits die Methoden des Document, wie doc.save() oder die getItemValue und replaceItemValue-Methoden erhält und andererseits Erweiterungen der Form doc.setTitle(String) erlaubt – eben eine klassische Erweiterung.
13.1.1
Erweiterung der Document-Funktionalitäten
Der einfachste Ansatz ist der Aufbau einer neuen Klasse, die das Document als Instanzvariable enthält. Dann kann das Document über einen setter und getter zugänglich gemacht und die gewünschten zusätzlichen Methoden (getTitle) nach Bedarf in der Klasse angelegt werden. Dies hat einige Nebenwirkungen, die anhand des
482
13 Objekt- und Speichertechniken
package djbuch.kapitel_13; import lotus.domino.*; public class CMSDocumentSimple { private static final String FIELD_ACTIVE = "F_isactive"; private static final String FIELD_TITEL = "F_titel"; private Document doc = null; public CMSDocumentSimple (Document initialDoc) { doc = initialDoc; } public Document getDoc() { return doc; } public void setDoc(Document initialDoc) { doc = initialDoc; } public boolean isActive () throws NotesException { if (getDoc().hasItem(FIELD_ACTIVE)) { return getDoc().getItemValueInteger(FIELD_ACTIVE) == 1; } else { return false; } } public void setActive (boolean flag) throws NotesException { getDoc() .replaceItemValue (FIELD_ACTIVE,new Integer (flag?1:0)); } public String getTitle () throws NotesException { return doc.getItemValueString(FIELD_TITEL); } public Item setTitle (String t) throws NotesException { return doc.replaceItemValue(FIELD_TITEL, t); } }
Listing 13-1 Einfaches Document mit erweiterter Funktionalität
Beispiels in Listing 13-1 und Listing 13-2 aufgezeigt werden. Das Beispiel ist eine rudimentäre Implementation eines Dokuments, wie es in einem Content Management System Anwendung finden könnte. Das CMSDocumentSimple selbst (Listing 13-1) ist sehr einfach gehalten, neben dem Domino-Document als Instanzvariable enthält es setter- und getter-Methoden für den Titel des Dokuments und für die Eigenschaft, die anzeigt, ob das Dokument im Content Management aktiv oder inaktiv ist – sicher eine der wichtigen Funktionen in einem CMS. Durch die Kapselung in der Klasse können nun die Itemnamen in Konstanten abgebildet, müssen aber nur innerhalb dieser Klasse codiert werden und somit extern nicht bekannt sein. Das zugehörige Document wird entweder im Konstruktor oder in setDoc festgelegt und kann über getDoc() referenziert werden, wobei hier keinerlei Besonderheiten bemerkenswert sind. Gleiches gilt für getTitle und setTitle , die lediglich die getItemValue- und setItemValue-Methoden kapseln und hierbei die Itemnamen fest verdrahten, so dass hier keine Schreibfehler mehr auftreten können.
13.1.1 Erweiterung der Document-Funktionalitäten
483
Ein diskreter boolean-Wert kann in Items nicht hinterlegt werden, daher bildet die Methode setStatus diesen auf die Integer-Werte 1 und 0 ab . Dies macht eine kleine Erweiterung der Methode isActive (hier wurde die auch in Domino übliche Notation „is...“ statt „get...“ gewählt) nötig. Damit NullPointerexeptions vermieden werden, prüft diese Methode zunächst , ob es das benötigte Item überhaupt gibt. Falls nicht, wird der Status als inaktiv angenommen. Dieser Methode kommt übrigens das Verhalten (s. Kap. 7.1.1ff.) der Methode getItemValueInteger zugute, die für Nicht-Integerwerte eines Items immer 0 zurückgibt, so dass nur korrekt auf 1 gesetzte Items ein „aktives“ Dokument zur Folge haben. Die Klasse ist so konzipiert, dass jede Änderung erst nach dem Aufruf von getDoc().save() dauerhaft gespeichert bleibt. Von einem impliziten Speichern durch save() in jedem setter ist aus Performancegesichtspunkten dringend abzuraten. In der Klasse CMSLoopSimple kommt die neue Klasse CMSDocumentSimple zum Einsatz. Die Aufgabe des Beispiels aus Listing 13-2 ist es, alle aktiven CMS Dokumente zu deaktivieren und umgekehrt. package djbuch.kapitel_13; import lotus.domino.*; import djbuch.kapitel_06.*; public class CMSLoopSimple implements Runnable { //...main ... public void run () { Session session = null; Database db = null; Document doc=null, nextDoc=null; View view=null; DocumentCollection dc=null; try { session=NotesFactory.createSession (HOST, USER, PASSWORD); db = session .getDatabase(session.getServerName(),"djbuch/djbuch.nsf"); String key = "CMS Document Simple " + System.currentTimeMillis(); CMSDocumentSimple cmsDoc = new CMSDocumentSimple ( DemoTools.createDoc(db,"CMS Simple",key,null)); //...your code goes here... GC.recycle(cmsDoc.getDoc()); cmsDoc=null; view = db.getView("V_lookup_k6"); dc=view.getAllDocumentsByKey("CMS Document Simple ",false); doc=dc.getFirstDocument(); while (doc!=null) { nextDoc = dc.getNextDocument(doc); cmsDoc=new CMSDocumentSimple(doc); cmsDoc.setActive(cmsDoc.isActive()?false:true); System.out.println (cmsDoc.getTitle() + " ist " + (cmsDoc.isActive()?"aktiv":"inaktiv")); cmsDoc.getDoc().save(true,false); GC.recycle(doc); //Dies recycled einen Teil von cmsDoc! doc=nextDoc; } } catch (NotesException e) { //...printStackTrace und recycle ... } }
Listing 13-2 Einfache Verarbeitung des erweiterten Document
484
13 Objekt- und Speichertechniken Als Datenbasis wird bei jedem Start ein neues Dokument anhand der Methode
DemoTools.createDoc erzeugt. Soll ein Domino-Document in ein CMSDocumentSimple überführt werden, kann dies einfach mit dem Konstruktor dieser Klasse erfolgen . Wird dieses neue CMSDocumentSimple nach Gebrauch nicht mehr benötigt, muss natürlich das enthaltende Domino-Document wieder recycelt werden . Zu bedenken ist hierbei, dass durch den Aufruf von cmsDoc.getDoc().recycle(), der in GC.recycle (Document) ausgeführt wird, der „Document-Anteil“ des CMSDocumentSimple ungültig wird. Um eine spätere versehentliche Nutzung zu vermeiden, sollte daher unbedingt das cmsDoc auf null gesetzt werden.
Die eigentliche Schleife über die CMS-Dokumente erfolgt über gewöhnliche Domino-Document-Objekte . Hierin ist ein Nachteil dieses bisherigen einfachen Ansatzes zu sehen. Das CMSDocumentSimple kann nicht direkt und gleichzeitig als Domino-Objekt und mit seinen Erweiterungen verwendet werden. Um den Status wie gewünscht zu verändern, muss zunächst ein neues CMSDocumentSimpleObjekt erzeugt und dann dessen setActive-Methode verwendet werden. Um das darunterliegende Domino-Document speichern zu können, wird anhand von getDoc() die Methode save() aufgerufen. Erneut kann zum Zeitpunkt des Recyclings die etwas ungünstige Verschränkung der Schleifenvariablen doc und nextDoc und dem CMSDocumentSimple festgestellt werden. Die konsequente Schlussfolgerung hieraus ist natürlich, die Methoden von Document auch in CMSDocumentSimple zur Verfügung zu stellen, um so zu erreichen, dass direkt mit dem CMSDocumentSimple – auch in der Schleife – gearbeitet werden kann. Aufmerksame Leser werden sich fragen, warum nicht spätestens an diesem Punkt die Möglichkeit aufgegriffen wird, das lotus.domino.Document-Interface zu implementieren. Aus Java-Sicht ist dies nicht nur möglich, sondern absolut folgerichtig. In der Praxis würde dieser Weg aber zu ClassCastExceptions führen, denn eigentlich arbeiten Sie nie mit lotus.domino-Objekten, sondern – wegen der eingangs erwähnten Erzeugung der Objekte außerhalb Ihrer Reichweite – immer entweder mit einem lotus.domino.cso- oder einem lotus.domino.local-Objekt. Nehmen wir also einmal an, wir hätten eine DocumentCollection (aus lotus.domino.cso) meineCSOCollection und auf Basis einer eigene Implementierung von lotus.domino.Document ein implementationDocument der Klasse public class DJDocumentImplementation implements lotus.domino. Document. Der Java-Compiler wäre zufrieden. Ein Aufruf von meineCSOCollection (implementationDocument) führt zur Laufzeit jedoch zu einer ClassCastException. Warum? Die Ursache ist schnell gefunden. Die Implementierung von getNextDocument ist konsequent als getNextDocument (lotus.domino. Document parameterDoc) deklariert, muss jedoch intern einen Cast (lotus. domino.cso.Document) parameterDoc durchführen, um mit dem Document arbeiten zu können. Ein Cast von DJDocumentImplementation nach (lotus. domino.cso.Document) ist jedoch nicht möglich.
Für einige Sonderfälle ist eine Implementierung von Domino-Objekten möglich und sinnvoll, was später noch diskutiert werden wird.
13.1.2 Pseudoimplementierung von lotus.domino.Document 13.1.2
485
Pseudoimplementierung von lotus.domino.Document
Wenn nun eine Implementierung nicht möglich (und auch nicht ratsam) ist, so soll nach einer Alternative gesucht werden. Die einfache Forderung lautet: Die neu zu schaffende Klasse soll sich (möglichst) wie ein lotus.domino.Document verhalten und erweiterbar sein. Um dieses Ziel zu erreichen wird die neue Klasse DJDocument78 erstellt. In einem ersten Schritt erhält sie alle Methoden, die auch lotus.domino.Document zur Verfügung stellt. Über eine Instanzvariable wird wie in CMSDocumentSimple ein Domino-Document vorgehalten. Die Methoden werden so implementiert, dass sie lediglich die Parameter aufgreifen und direkt an dieses Document weiterleiten. Die Klasse erhält einen Konstruktor public DJDocument (Document initialDoc){ setDoc (initialDoc); }
und z.B. die Methode appendItemValue (String, Object) sieht dann wie folgt aus: public Item appendItemValue(String name, Object val) throws NotesException { return doc.appendItemValue(name, val); }
Auch diese Umsetzung findet sich im Beispielcode und ist in Listing 13-3 abgebildet. Die Variable doc wird hierbei als Instanzvariable private Document doc = null; definiert und über den Konstruktor mit einem gültigen Domino-Document belegt. Die Methode setDoc ist öffentlich verfügbar und sorgt zusätzlich noch dafür, dass durch ein neuerliches Setzen des internen Document doc nicht etwa eine Referenz verloren ginge, bevor das interne Document recycelt werden konnte. Gleichzeitig stellt diese Methode sicher, dass isRecycledOrRemoved=false gesetzt wird, falls ein Nicht-Null-Wert als Parameter übergeben wurde. Hier zeigt sich bereits eine erste Schwierigkeit. Aus einem bestimmten Grund, der später noch erläutert wird, soll es möglich sein, dass für die Instanzvariable doc auch der null-Wert zulässig ist. Damit keine Operationen auf der Instanzvariablen doc mit dem hierdurch entstehenden null-Wert ausgeführt werden, müssen die Methoden eine entsprechende ergibt sich dann Überprüfung erhalten. Die Methode appendItemValue wie folgt:
78
Das Kürzel „DJ“ steht übrigens für Domino-Java.
486
13 Objekt- und Speichertechniken
... public class DJDocument { private Document doc = null; private boolean isRecycledOrRemoved = true; private static final int EXCEPTION_NO = NotesError.NOTES_ERR_DELETED; private static final String EXCEPTION_MSG = "Document has been removed or recycled."; public DJDocument (Document initialDoc){ setDoc (initialDoc); } public Document getDoc() { return doc; } public void setDoc(Document initialDoc) { if (doc!=null && !isRecycledOrRemoved) { try { doc.recycle(); } catch (NotesException e) { e.printStackTrace(); } } if (initialDoc != null) {isRecycledOrRemoved=false;} doc = initialDoc; } public void makeResponse(Document arg0) throws NotesException { if (doc==null || isRecycledOrRemoved) throw new NotesException (EXCEPTION_NO,EXCEPTION_MSG); doc.makeResponse(arg0); } public void makeResponse(DJDocument arg0) throws NotesException { if (doc==null || isRecycledOrRemoved) throw new NotesException (EXCEPTION_NO,EXCEPTION_MSG); doc.makeResponse(arg0.getDoc()); } public void copyAllItems(Document arg0, boolean arg1) throws NotesException { if (doc==null || isRecycledOrRemoved) throw new NotesException (EXCEPTION_NO,EXCEPTION_MSG); doc.copyAllItems(arg0,arg1); } public void copyAllItems(DJDocument arg0, boolean arg1) throws NotesException { if (doc==null || isRecycledOrRemoved) throw new NotesException (EXCEPTION_NO,EXCEPTION_MSG); doc.copyAllItems(arg0.getDoc(),arg1); } public boolean remove(boolean arg0) throws NotesException { if (doc==null || isRecycledOrRemoved) throw new NotesException (EXCEPTION_NO,EXCEPTION_MSG); boolean result = doc.remove(arg0); isRecycledOrRemoved=true; return result; } public boolean removePermanently(boolean arg0) throws NotesException { if (doc==null || isRecycledOrRemoved) throw new NotesException (EXCEPTION_NO,EXCEPTION_MSG); boolean result = doc.removePermanently(arg0); isRecycledOrRemoved=true; return result; } public void recycle() throws NotesException { if (doc==null) throw new NotesException (EXCEPTION_NO,EXCEPTION_MSG);
13.1.2 Pseudoimplementierung von lotus.domino.Document
487
doc.recycle(); doc=null; isRecycledOrRemoved=true; } public void recycle(Vector arg0) throws NotesException { if (doc==null) throw new NotesException (EXCEPTION_NO,EXCEPTION_MSG); Vector v = new Vector(); boolean myself=false; for (int i = 0, size = arg0.size(); i < size; i++) { Object o = arg0.elementAt(i); if (o instanceof DJDocument) { Document thisDoc = ((DJDocument)o).getDoc(); if (thisDoc != null) { v.add(thisDoc); if (thisDoc.equals (this.getDoc())) myself=true; } } else { v.add(o); if (o.equals (this.getDoc())) myself=true; } } doc.recycle(v); if (myself) { doc=null; isRecycledOrRemoved=true; } } public Item appendItemValue(String arg0, Object arg1) throws NotesException { if (doc==null) throw new NotesException (EXCEPTION_NO,EXCEPTION_MSG); return doc.appendItemValue(arg0, arg1); } ...
Listing 13-3 Pseudoimplementierung von Document
public Item appendItemValue(String arg0, Object arg1) throws NotesException { if (doc==null || isRecycledOrRemoved) throw new NotesException (EXCEPTION_NO,EXCEPTION_MSG); return doc.appendItemValue(arg0, arg1); }
Neben der Überprüfung auf null wird klassenintern noch ein boolean-Parameter isRecycledOrRemoved vorgehalten, der sicherstellt, dass das interne Document weder gelöscht noch recycelt wurde. Nun müssen noch einige weitere Methoden näher beleuchtet werden. Die Methode recycle () hat zur Folge, dass die Instanzvariable doc ungültig wird. Daher wird diese zusätzlich zum recycle auf null gesetzt – was aus eben diesem Grund auch zulässig sein muss –, so dass alle übrigen Methoden dies erkennen und darauf adäquat reagieren können . Parallel wird der Parameter isRecycle-
488
13 Objekt- und Speichertechniken
dOrRemoved gesetzt, der erkennen lässt, ob ein DJDocument nur gelöscht (remove() setzt nur den boolean isRecycledOrRemoved) oder auch recycelt (recycle setzt doc=null und isRecycledOrRemoved=true) wurde. Für die Methode recycle (Vector) wird noch zusätzlicher Code benötigt, der es ermöglicht, Objekte der Klasse DJDocument im Vector zuzulassen und
zu erkennen und dafür sorgt, dass das jeweils dahinterliegende Document statt des DJDocument dem recycle zugeführt wird . Außerdem wird überprüft, ob das zum DJDocument gehörende Document im Vector vorliegt . Falls dies der Fall ist, wird ebenfalls die Instanzvariable doc auf null gesetzt. Äquivalent wird für die Methoden remove und removePermanently vorgegangen, wobei hier doc noch nicht auf null gesetzt werden darf, da entsprechend den Gepflogenheiten für das Domino-Document nachfolgend noch ein recycle erwartet wird. Lediglich isRecycledOrRemoved==true zeigt an, dass das Document nicht mehr verwendet werden kann. Dass die oben verwendete Konstante EXCEPTION_MSG den Wert „Document has been removed or recycled“ erhält, versteht sich nun von selbst, die IntegerKonstante EXCEPTION_NO wurde mit dem Wert NotesError. NOTES_ERR_DELETED belegt, den Domino selbst für diesen Zweck verwendet. Zwei Methoden – copyAllItems und makeResponse – erlauben als Parameter ein Document. Aus Bequemlichkeitsgründen wurde der Klasse daher jeweils eine Dublette , die statt dem Document ein DJDocument erlaubt, hinzugefügt, wobei diese Methoden dann per getDoc() das eigentliche Document an die Originale dieser Methoden durchschleifen. Mit der Klasse DJDocument steht nun eine Basis zur Verfügung, die alle Fähigkeiten eines Document hat und gleichzeitig erweiterbar ist. Als Erweiterung von DJDocument wird nun die Klasse CMSDocument (Listing 13-4) definiert, die der Klasse CMSDocumentSimple sehr ähnlich ist. Aufgrund der in DJDocument zur Verfügung stehenden Methoden können nun die Operationen, die sich auf das Domino-Document beziehen, das bisher per getDoc() referenziert werden musste, direkt auf das Objekt selbst bezogen werden. Die setter und getter wie z.B. isActive können per this.getItemValue.. usw. die Instanzvariable doc aus DJDocument verändern. Neben der Vereinfachung des Codes ist der Gewinn an Übersichtlichkeit nicht zu unterschätzen. Zusätzlich wurde eine kleine Veränderung eingeführt. Statt des public-Konstruktors hat die Klasse eine Factory Methode erhalten, die statische Methode getInstance . Hierdurch wird erreicht, dass bei der Übergabe eines nullParameters, statt eines neuen Objektes, das anhand des private Konstruktors nach wie vor erzeugt werden kann, null zurückgegeben wird. In der späteren Verwendung soll diese Klasse sich verhalten wie ein DominoDocument. Wenn also ein Document den Wert null hat, soll auch das CMSDocument-Objekt null sein. Dies wird durch die Konstruktion mit getInstance erreicht. In der Verarbeitung (Beispiel s. Listing 13-5) wird diese Klasse ähnlich transparent handhabbar, wie dies bereits für die Methode isActive zu sehen war. Ein neues CMSDocument erhält man durch die Anwendung der statischen Methode getInstance . Angenommen die Methode createDoc lieferte einen
13.1.2 Pseudoimplementierung von lotus.domino.Document
489
//...import... public class CMSDocument extends DJDocument { //...Instanzvariablen... public static CMSDocument getInstance (Document initialDoc) { if (initialDoc==null) { return null; } else { return new CMSDocument (initialDoc); } } public boolean isActive () throws NotesException { if (this.hasItem(FIELD_ACTIVE)) { return this.getItemValueInteger(FIELD_ACTIVE) == 1; } else { return false; } } //...setActive / getTitle / setTitle private CMSDocument (Document doc) { super (doc); } }
Listing 13-4 CMSDocument als Erweiterung der Pseudoimplementierung
Listing 13-5 Komfortable und transparente Verarbeitung durch die Pseudoimplementierung
490
13 Objekt- und Speichertechniken
null-Wert zurück, würde nun auch das neue CMSDocument cmsDoc den Wert null erhalten. So kann sicher zwischen null und einem „leeren“ CMSDocument
(mit fehlendem Domino-Document) unterschieden werden. Dies ist ein Fortschritt zum vorigen Beispiel der Klasse CMSDocumentSimple, in dem ein CMSDocumentSimple-Objekt ohne Domino-Document erstellt worden wäre. Auch das Recycling konnte nun so angepasst werden, als ob es sich um ein „original“ Domino-Document handeln würde. Als Schleifenvariable wird nun kein Document mehr benötigt, es kann direkt das mittels getInstance(dc.getFirstDocument()) ermittelte CMSDocument verwendet werden, indem es wie ein Document in der while-Schleife auf null überprüft wird, wobei auch hier die Eigenschaft von getInstance zum Tragen kommt, null zurückzugeben, falls ein null-Parameter bei der Instanzierung verwendet wurde. Die für Document bereits bekannte Mimik, das jeweils nächste Dokument jeweils in einer Variablen zwischenzuspeichern, bleibt hier erhalten, allerdings muss sowohl der Parameter für getNextDocument anhand von getDoc() in ein lotus.domino.Document, als auch der Rückgabewert per getInstance wieder in ein CMSDocument zurückgewandelt werden . Alle übrigen Operationen wie save , recycle oder die Zuweisung des nächsten Schleifenwertes werden so durchgeführt, wie es von einem „normalen“ Document zu erwarten wäre, und gleichzeitig kennt das CMSDocument die ihm eigenen Methoden , z.B. zur Statusänderung. Mit der DJDocument-Klasse steht Ihnen nun ein Werkzeug zur Verfügung, auf dessen Basis neue Klassen erstellt werden können, die sowohl eigene neue Funktionen implementieren und gleichzeitig die Methoden des Document mitbringen können.
13.1.3
Implementieren von Domino-Java-Objekten
Wie Sie sehen konnten, sind die Möglichkeiten, die Interfaces abzuleiten, für die Domino-Java-Objekte eingeschränkt. Für bestimmte Anwendungszwecke und insbesondere für Testszenarien bietet sich diese Vorgehensweise jedoch an. Im Folgenden soll ein Szenario skizziert werden, anhand dessen erläutert wird, wie durch Erstellen einer Klasse, die das Interface lotus.domino.AgentContext implementiert, ein Domino-Java-Agent, der im Domino Designer erstellt wurde, in ein Servlet umgewandelt werden kann, ohne dass der ursprüngliche Code modifiziert werden muss. Agenten, die als Domino-Java-Agenten angelegt wurden, basieren auf Klassen, die AgentBase erweitern (s. Kap. 4.10). Sie werden über das GUI des Domino Designers angelegt. Wurde ein solcher Agent gut aufgebaut, so enthält der eigentliche Agent möglichst wenig Code und im Wesentlichen nur ein Starter-Objekt, das die gewünschte Verarbeitung anstößt. In der Datenbank djbuch.nsf finden Sie einen derartigen Agenten mit Alias „AG_queryOpenNews“.
13.1.3 Implementieren von Domino-Java-Objekten
491
Abb. 13-1 Die Maske FO_RSSNews
Dieser Agent ist als so genannter WebQueryOpen-Agent konzipiert und gehört somit zu einer Maske, die wiederum diesen Agenten aufruft. Die zugehörige Maske heißt „FO_RSSNews“ und ruft diesen Agenten als WebQueryOpen-Agent auf (s. Abb. 13-1). Wird nun die Maske im Browser geöffnet, so startet dieser Agent, führt seine Arbeiten aus, wird beendet und die Maske wird endgültig geöffnet und angezeigt. Die Aufgabe des Agenten ist es, als Demo-Beispiel einen RSS News Feed [Heise, News] zu laden, mittels eines XSL Stylesheets in HTML umzuwandeln und in das Item „Body“ des Kontextdokuments zu schreiben. Die Maske wiederum zeigt dieses Kontextdokument an. Hierfür wurde die Maske lediglich mit einem Feld „Body“ ausgestattet. Zusätzlich hat sie zwei Eigenschaften erhalten, die dafür sorgen, dass die Maske einerseits nicht dazu verwendet werden kann, Dokumente neu zu erstellen (es soll lediglich angezeigt werden – erreicht wird das durch das versteckte Feld „SaveOptions“ mit dem Wert "0") und eventuelle Inhalte immer als so genanntes DurchgangsHTML anzeigt (HTML-Tags werden nicht escaped, sondern direkt an den Browser durchgeschleift – erreicht wird dies durch die Maskeneigenschaft „ContentType: HTML“). Damit Sie das Beispiel einfach starten können, wurde noch eine Seite in die Datenbank eingebaut, die einen vorbereiteten Link enthält. Um das Beispiel zu starten, gehen Sie wie folgt vor: Öffnen Sie die Datenbank djbuch.nsf im Domino Designer und klicken Sie in der linken Navigation auf den Eintrag „Pages“. Markieren Sie nun
492
13 Objekt- und Speichertechniken
im rechten Fenster die Seite „P_rssNews“ und wählen Sie den Preview-Button oder wählen Sie im Menü den Befehl „Design“->„Preview in WebBrowser“ und wählen Sie Ihren bevorzugten Browser aus. Nun öffnet sich eine Seite im Browser, die den gewünschten Link enthält und die Seite mit der URL „http://www.ihrServer.de/djbuch/djbuch.nsf/FO_RSSNews?ReadForm“ öffnet. Falls die geöffnete Datenbank lokal lag, wird „www.ihrServer.de“ durch „localhost“ ersetzt und im Hintergrund der HTTP-Hilfstask des Notes Clients gestartet. Wenn Sie einen Domino-Server installiert und die Datenbank über den Server geöffnet haben, so wird die Seite über den HTTP-Task des Servers geöffnet. Als Ergebnis sehen Sie eine einfache Darstellung der aktuellsten Nachrichten des geladenen RSS Feeds (s. Abb. 13-2).
Abb. 13-2 Der WebQueryOpen Agent und dessen Anzeige im Browser über die Maske FO_RSSNews
Die Idee ist nun, den Agenten, der diese Seite erzeugt, in ein Servlet umzuwandeln. Angenommen, über diesen Agenten ist nur der direkt im Agenten sichtbare Code (Listing 13-6) bekannt und der eigentliche Code steht dem Agenten (wie in diesem Beispiel) in einer Bibliothek (Jar-Datei) zur Verfügung. Möglicherweise haben Sie wegen strenger Versionspflege oder weil es sich um externen Code handelt, keinen Zugriff auf diesen Code außer als Jar-Datei. Die Jar-Datei können Sie laden, indem Sie den Agenten im Domino Designer öffnen und die Jar-Datei dort über den Button „export“ ins Dateisystem speichern. Der Agent selbst ist einfach aufgebaut. Er erzeugt einen AgentContext und mit diesem eine neue Instanz eines RSSNews-Objekts. Jetzt wird die Methode process gestartet. Nun ist nichts über diese Methode bekannt, aber der Agent soll schließlich
13.1.3 Implementieren von Domino-Java-Objekten
493
import lotus.domino.*; import djbuch.rssnews.RSSNews; import djbuch.rssnews.RSSNewsR7; public class RSSNewsQueryOpenAgent extends AgentBase { public void NotesMain() { try { Session session = getSession(); AgentContext agentContext = session.getAgentContext(); //RSSNews rss = new RSSNews (agentContext); RSSNewsR7 rss = new RSSNewsR7 (agentContext); rss.process(); rss.attach(rss.getCode()); } catch(Exception e) { e.printStackTrace(); } } }
Listing 13-6 Code des WebQueryOpen-Agenten
einen RSS Feed laden und in HTML umwandeln, also sei zunächst davon ausgegangen, dass genau dies geschieht. In einem zweiten Schritt wird der offenbar hierbei erzeugte HTML-Code angehängt. Damit eine Maske derartige Inhalte anzeigen kann, kann dies nur das ContextDocument sein und anhand der Maske läßt sich davon ausgehen, dass das Item „Body“ mit dem Code gefüllt wird. Für die Umwandlung in ein Servlet muss nun, anstatt den Code in ein Item zu schreiben, dieser direkt an den Browser übergeben werden. Die Methode getCode() sieht sehr vielversprechend aus, sie wird sicherlich den erzeugten Code enthalten. Damit aber ein RSSNews-Objekt erzeugt werden kann, wird ein AgentContext-Objekt benötigt. Dieses steht aber nur im Kontext eines Domino-Java-Agenten zur Verfügung. Es ist nicht möglich ein solches Objekt aus einem Servlet heraus zu instanzieren. Die Erfahrung zeigt, dass vom AgentContext-Objekt in der Regel insbesondere die aktuelle Datenbank (über getCurrentDatabase()) und das aktuelle Dokument (das ContextDocument über getDocumentContext()) benötigt werden. Die Idee ist nun, einen künstlichen AgentContext als Implementation des Interface lotus.domino.AgentContext aufzubauen, dem diese beiden Objekte einfach über den Konstruktor mitgegeben werden. Listing 13-7 zeigt eine derartige Umsetzung. Dieser AgentContext ist schon etwas weiter ausgebaut, er sieht bereits zwei Konstruktoren vor und ermöglicht, in den Aufrufparametern die Felder „aktueller Benutzer“, „aktuelle Datenbank“, „aktuelles Dokument“ und „aktueller Agent“ zu übergeben, die einfach in den Instanzvariablen gespeichert werden, und entsprechend dem Interface später über die Methoden getEffectiveUserName(), getCurrentDatabase() , getDocumentContext() und getCurrentAgent() wieder zur Verfügung zu stellen. Bei dieser Vorgehensweise ist es sehr wichtig, dass alle übrigen (noch nicht
Listing 13-7 AgentContext als Eigenimplementierung
implementierten) Methoden eine Exception werfen, damit erkannt werden kann, ob weitere Methoden benötigt werden und durch entsprechenden Code ausgebaut werden müssen. Im Beispielcode zum Buch befindet sich zusätzlich die Klasse DJAgent die mit denselben Mitteln ein künstliches Objekt, diesmal einen Agent, erstellt. Er wird benötigt, wenn dem DJAgentContext im zweiten Konstruktor ein Agent über den AgentContext zur Verfügung gestellt werden soll. Den kompletten Code finden Sie im Package djbuch.kapitel_13. Letztendlich gilt für alle über diese Technik implementierten Objekte: Anstatt ein Objekt über die klassischen Wege von Domino zu beziehen (z.B. einen Agent durch die Programmierung im Domino Designer, das Erweitern von AgentBase und den Aufruf von getCurrentAgent in AgentContext oder einen AgentContext durch das Starten eines Agenten und den Aufruf von getAgentContext in Session)
13.1.3 Implementieren von Domino-Java-Objekten
495
Best Practice Der abgeleitete AgentContext kann verwendet werden, um Agenten außerhalb des Domino Designers zu starten, zu entwickeln und zu testen. Die unterschiedlichen Sicherheitskontexte müssen berücksichtigt werden. Webagenten können durch den abgeleiteten AgentContext einfach in Servlets umgewandelt werden.
wird ein benötigtes Objekt „künstlich zusammengestellt“, indem alle für den aktuellen Anwendungszweck benötigten Instanzvariablen über einen neuen Konstruktor übermittelt werden. Geeignet ist diese Vorgehensweise insbesondere für Debuggingzwecke und während der Entwicklung. So kann auf einfache Weise ein Agentloader zusammengebaut werden, der direkt aus dem IDE gestartet werden kann und den Start über den Domino-Agent simuliert. Mit dem DJAgentContext steht nun eine sehr einfache Lösung zur Verfügung, mit der der Code, der bisher nur im Context des Domino-Java-Agent lauffähig war, unabhängig ausgeführt und insbesondere getestet werden kann. Diese Technik ermöglicht Ihnen, Ihre Domino-Java-Agenten komplett im IDE zu entwickeln und zu testen, ohne bei jeder Änderung den Code in den Domino Designer importieren zu müssen und ohne gleichzeitig auf den AgentContext verzichten zu müssen. Um dies zu erreichen, muss lediglich ein neuer DJAgentContext mit der gewünschten Datenbank und einem (beliebigen, neuen) Document aus dieser Datenbank erzeugt werden und kann der Anwendung übergeben werden. Die Verwendung dieser Technik soll an einem sehr nützlichen Sonderfall demonstriert werden: Die Umwandlung eines WebQueryOpen-Agenten in ein Servlet. Hierfür muss die beschriebene Technik lediglich auf das Servlet angewendet werden. In Listing 13-8 wird zunächst eine Datenbank benötigt, in der sich der Agent abspielen soll. Im Beispiel wurde der Bezug der Datenbank hart codiert, natürlich ist auch eine dynamische Bestimmung (z.B. über einen URL-Parameter, über den die Replik ID der Datenbank übermittelt wird; besser sind hier allerdings eigene intern gepflegte Aliasnamen, um keine Sicherheitslücken aufzureissen) denkbar. Außerdem wird ein ContextDocument benötigt, das hier einfach nur neu erzeugt wird . Beachten Sie, dass dieses Dokument während der gesamten Laufzeit (wie auch ein „echtes“ ContextDocument) nicht gespeichert wird, sondern lediglich als Objekt im Hauptspeicher verbleibt.
496
13 Objekt- und Speichertechniken
//...import... public class RSSNewsServlet extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException { Session session = null; Database db = null; Document doc = null; PrintWriter out = res.getWriter(); try { NotesThread.sinitThread(); session = NotesFactory.createSession(); db = session.getDatabase(session.getServerName(), "djbuch/djbuch.nsf"); doc = db.createDocument(); DJAgentContext agentContext = new DJAgentContext(req.getRemoteUser(), db, doc); //RSSNews rss = new RSSNews(agentContext); RSSNewsR7 rss = new RSSNewsR7(agentContext); rss.process(); res.setContentType("text/html"); out.print(rss.getCode()); out.flush(); out.close(); } catch (NotesException e) { //...printStackTrace, recycle und stermThread... } }
Listing 13-8 In Servlet umgewandelter Agent Nun kann wie auch im Agenten das RSSNews-Objekt instanziert werden79. Der hierfür benötigte AgentContext wird als DJAgentContext aus der Datenbank und dem neuen Dokument sozusagen „zusammengebaut“. Zusätzlich wird noch der angemeldete Benutzer (req.getRemoteUser ) mit in den DJAgentContext eingebaut. Würde die Anwendung per getEffectiveUserName() ermitteln, welches der aktuelle Benutzer ist, würde auch dieser Wert an dieser Stelle manipuliert werden können. Es fehlt nur noch der Aufruf von process() in RSSNews und der generierte Code (rss.getCode() ) kann an den Browser übermittelt werden, nachdem, wie im Servlet üblich, der ContentType eingestellt wurde. Das Ergebnis (s. Abb. 13-3) ist identisch mit der Ausgabe des WebQueryOpenAgenten. Lediglich die für den Aufruf verwendete URL unterscheidet sich. Wenn Sie das Beispiel selbst ausprobieren, werden Sie feststellen, dass der Aufbau dieser Seite erheblich schneller vonstatten geht, als über den WebQueryOpen-Agen-
79
Das RSSNews-Objekt existiert in zwei Versionen – eine für Domino R6.x und älter und eine für Domino R7. Dies hat seine Ursache in der XSL Verarbeitung, die im RSSNews-Objekt benötigt wird. Während Domino R6 noch darauf angewiesen war, die Klasssen und Methoden aus dem Package com.lotus.xsl, das dem Domino-Server in Form der Jar-Datei LotusXSL.jar beiliegt, zu verwenden, kann mit Domino R7 auf die neuen Packages aus javax.xml zurückgegriffen werden, die die XSL-Verarbeitung deutlich vereinfachen. Beide Versionen liegen dem SourceCode im Package djbuch.rssnews bei.
13.1.3 Implementieren von Domino-Java-Objekten
497
Abb. 13-3 Ausgabe des RSSNews Servlets
ten. Ursache hierfür ist der Überhang, der durch das Laden der Maske und des Agenten entsteht. Schließlich startet Domino für jeden Aufruf des Agenten eine eigene ClassLoader-Instanz. Um das Beispiel einfach nachzuvollziehen, befindet sich im Beispielcodeverzeichnis 01 Examples/lib die Jar-Datei rssnewsservlet.jar. Diese legen Sie ins Verzeichnis domino/servlet innerhalb des Dataverzeichnisses, fügen im Serverdokument im Abschnitt „Internet Protocolls“-> „Domino Web Engine“ im Feld „Java Servlets / ClassPath“ den Eintrag „domino\servlet\rssnewsservlet.jar“ hinzu und ergänzen die Datei servlets.properties um die Zeilen servlet.rssnewsservlet.code= djbuch.kapitel_13.RSSNewsServlet.class servlets.startup=rssnewsservlet
Nach einem Neustart des HTTP-Tasks können Sie das Servlet unter der URL http://www.IhrServer.de/servlet/rssnewsservlet erreichen. Fazit Der künstlich angelegte AgentContext bietet die Möglichkeit, Agenten auch außerhalb des Domino Designers und außerhalb des Domino-Agenten-Kontextes auszuführen. Dies kommt insbesondere bei der Umwandlung von (Web-) Agenten in Servlets und bei Tests zum Tragen. Ein Aspekt wurde bisher noch nicht beleuchtet. Die Session im Servlet wurde per NotesFactory.createSession(); geöffnet. Folglich ist der Server der Besitzer dieser Session. Dies kann ein Vor-, aber auch ein Nachteil sein. Wenn für den Agenten
498
13 Objekt- und Speichertechniken
die Eigenschaft „Run as Web User“ aktiviert war, muss hier nachgearbeitet werden. Nähere Informationen finden Sie in den Kapiteln 4 und 5, insbesondere in Kapitel 5.2.3. Schließlich sei noch darauf hingewiesen, dass Sie natürlich Ihre WebAgenten von Anfang an so konzipieren können, dass Sie ohne den AgentContext auskommen, bzw. dass diese von Beginn an als Servlet geplant werden. Dies gilt natürlich nur, wenn der notwendige Zugang zum Code besteht bzw. dieser neu entwickelt wird.
13.2
Cashing von Domino-Objekten
In Kapitel 13.1.1f. wurde gezeigt, wie die Funktionalitäten eines Document angepasst und durch „Nachbau“ des Interfaces das Document auch für Erweiterungen zur Verfügung gestellt werden kann. Das so entstandene DJDocument ist weiterhin eng mit dem lotus.domino. Document verwoben und steht daher ebenfalls in enger Verbindung mit der Domino-Session. Ein Domino-Objekt und somit auch das DJDocument steht immer nur in und während einer Domino-Session zur Verfügung. Vor allem bei Web-AnPerformance beim Lesen von Daten aus wendungen entsteht daher die Fralotus.domino.Document gestellung, wie ein Document zwischen zwei Web-Zugriffen vorgeSollen große Mengen von Dokumenten und / halten werden kann, am besten als oder viele Items aus Dokumenten gelesen werObjekt persistent im Hauptspeicher. Letzteres ist vor allem wichtig den, müssen die Verarbeitungszeiten der Mefür Objekte, die sich selten ändern, thoden getItemValue und verwandter Methoaber sehr häufig benötigt werden den im Auge behalten werden. oder deren Erzeugung zeit- und/ oder ressourcenintensiv ist. Mit eiUnter ungünstigen Bedingungen kann das Lesen nem lotus.domino.Document der Daten zum Flaschenhals einer Anwendung wäre dies eine nicht triviale Aufgawerden. be, da die Domino-Session über mehrere Webseiten und somit über mehrere autarke Programmaufrufe z.B. eines Servlets aufrecht erhalten werden müsste (s. Kap. 17.4.1ff). Alle bisher aufgezeigten Techniken bauen aber die Domino-Session für jeden Aufruf einer Seite (eines Servlets oder eines WebAgenten) neu auf. Natürlich stehen Domino-Dokumente immer über die Datenbank zur Verfügung und es ist denkbar, z.B. die Universal ID des Dokuments persistent zu speichern und das Dokument anhand dieser bei Bedarf wieder per getDocumentByUNID wieder zu laden. Hierfür ist aber in jedem Fall mindestens der Aufbau der Domino-Session, das Öffnen der Datenbank und letzlich das Laden des Dokumentes erforderlich. Zusätzlich kommt noch ein weiterer Punkt hinzu, der nicht unberücksichtigt bleiben darf. Das Auslesen von Items aus einem Document kann eine zeitintensive
13.2.1 Das DJCacheDocument
499
Angelegenheit sein. Der Zeitbedarf für das Auslesen über die Methoden getItemValue und verwandte Methoden ist nicht linear abhängig von der Anzahl der Items in einem Document und der Menge an Daten innerhalb der Items. Beim Profiling einer produktiven Domino-Java-Anwendungen wurden unter ungünstigen Bedingungen Situationen gemessen, in denen das reine Lesen (!) von Daten über getItemValueString 50% der gesamten Verarbeitungszeit einer Anwendung einnahm, in der Daten aus einer Domino-Datenbank gelesen und über eine Bubblesearch sortiert werden sollten. Zum Vergleich: Die Zeiten wurden für rund 1000 Dokumente ermittelt, wobei die Bubblesearch lediglich 2% der Verarbeitungszeit in Anspruch nahm. Zur Lösung dieser Fragestellung verspricht die Datenhaltung in Arraylistund HashMap-Objekten große Vorteile.
13.2.1
Das DJCacheDocument
Da Domino-Java-Objekte nur zusammen mit einer gültigen Domino-Session existieren können, wird für das Vorhaben, ein Dokument im Java-Hauptspeicher zu cachen, ein Objekt benötigt, das ohne diese Session auskommt. Prinzipiell würde es genügen, über getItemValue einfach alle Vectoren des Document auszulesen und zum Beispiel in einer HashMap zu speichern. Da dies nicht sehr komfortabel wäre, wurde das DJCacheDocument entwickelt. Dieses soll folgenden Bedingungen genügen: 1 Ein DJCacheDocument kann direkt aus einem lotus.domino.Document erzeugt werden. 2 Es bietet mindestens die Standardmethoden getItemValue, replaceItemValue und appendItemValue und verwandte Methoden. 3 Diese Methoden verhalten sich äquivalent zu den Methoden von lotus.domino.Document80. 4 Aus einem DJCacheDocument kann auf das Original-Document geschlossen werden, aus dem es erzeugt wurde. 5 Es soll als POJO angelegt sein und somit über einen CacheMechanismus einfach cachebar sein. Bis auf Punkt 3 sind diese Bedingungen leicht zu erfüllen und als Klasse zu implementieren. In Tabelle 7-3 und Tabelle 7-4 wurde das Verhalten der getItemValueMethoden beschrieben. Die Schwierigkeit liegt nun darin, die Rückgabewerte so abzustimmen, dass die Methoden dieses zum Teil ungewöhnliche Verhalten abbilden. Weitere Punkte sind folglich zu berücksichtigen: 6 Die Rückgabewerte der Methoden müssen den Vorgaben aus Tabelle 7-3 und Tabelle 7-4 entsprechen. 80
Dies ist nicht trivial, da diese Methoden zum Teil recht unkonventionell arbeiten. Insbesondere bei Zugriff auf nicht erwartete Datentypen werden in vielen Fällen unerwartete Werte zurückgegeben (s. Kap. 7.1 ff).
500 7
8
13 Objekt- und Speichertechniken Es muss zugelassen sein, dass mehrere Items gleichen Namens verwendet werden, wobei die getItemValue-Methoden immer nur das erste Item berücksichtigen. Jedes Item kann entweder diskrete Werte aufnehmen oder eine Liste aus Werten, die als Vector behandelt werden.
Zusätzlich ergeben sich noch folgende Randbedingungen: 9
10
11
12
13
Dokumente können DateTime-Objekte aufnehmen. Da diese eine Session benötigen, muss das DJCacheDocument diese auf Date-Objekte mappen. Die Rückgabe dieser Werte erfordert eine Session, damit eine Rückumwandlung in DateTime erfolgen kann. Die Klasse erhält zusätzlich die Methode Vector getAllItemNames(), die die Namen aller Items zurückgibt. Diese Methode dient als Ersatz für die Methode Vector getItems(). Zusätzlich erhält die Klasse die Methoden getParentDatabaseFileName und getParentDatabaseFilePath, die den bequemen Bezug der aus dem original Document bezogenen Informationen zur ParentDatabase liefern. Im Gegensatz zum Vorbild benötigt die Klasse als POJO einen Konstruktor, der als public DJCacheDocument(Document doc) throws NotesException implementiert wird und als Parameter das lotus.domino.Document erhält, aus dem es erzeugt werden soll. Die Verwaltung der Item-Namen übernimmt die innere Klasse FieldNames, die insbesondere das Verhalten bei der doppelten Verwendung von Item-Namen abbildet.
Im Listing 13-9 befindet sich der Code der Klasse, die diese Bedingungen erfüllen wird. Sie bildet die wichtigsten Methoden in Document ab, insbesondere natürlich getItemValue, replaceItemValue und appendItemValue. Sie besitzt eine innere Klasse FieldNames , die die Verwaltung der Item-Namen übernimmt. Jedes DJCacheDocument hat eine FieldNames-Instanzvariable fn , in der die Item-Namen gespeichert werden. Sie wird wie folgt verwendet: •
String addName (String fieldName)
Fügt einem vorhandenen Namen einen neuen (intern in FieldNames verwalteten) Namenseintrag hinzu bzw. legt einen neuen an. Wird in der Regel parallel zum Ablegen eines Objektes in der HashMap elements in DJCacheDocument verwendet (zu jedem Value-Eintrag wird parallel ein Namenseintrag verwaltet). Aus Sicht des DJCacheDocuments kann es mehrere Items mit dem gleichen Namen geben. Intern werden diesen Items über FieldNames eindeutige Namen zugeordnet.
13.2.1 Das DJCacheDocument
501
package djbuch.kapitel_13; import java.util.*; import lotus.domino.*; public class DJCacheDocument { private static final int DJCACHEDOMINOEXCEPTIONID = 999; private static final boolean SIMULATE_R7 = true; private static final boolean IS_IIOP = false; private Map elements = Collections .synchronizedMap(new HashMap ()); private FieldNames fn = new FieldNames(); private String universalID = null; private String parentDocumentUNID = null; private String parentDatabaseFilePath = null; private String parentDatabaseFileName = null; private String parentDatabaseReplicaID = null; public DJCacheDocument(Document doc) throws NotesException { if (doc != null) { init (doc); } } private void init (Document doc) throws NotesException { universalID = doc.getUniversalID(); parentDocumentUNID = doc.getParentDocumentUNID(); parentDatabaseFilePath = doc.getParentDatabase().getFilePath(); parentDatabaseFileName = doc.getParentDatabase().getFileName(); parentDatabaseReplicaID = doc.getParentDatabase().getReplicaID(); Vector v = doc.getItems(); Item item = null; for (int i = v.size() - 1; i >= 0; i--) { item = (Item) v.elementAt(i); if (item != null && (item.getType() == Item.TEXT || item.getType() == Item.AUTHORS || item.getType() == Item.DATETIMES || item.getType() == Item.NAMES || item.getType() == Item.NUMBERS)) { appendItemValue(item.getName(), item.getValues()); } } } public void appendItemValue(String fieldName, Object value) { if (!ok (fieldName)) { return; } if (value == null) { value = ""; } Vector v = new Vector(); boolean isDateTime = false; if (value instanceof Vector) { v = (Vector)value; if (isInteger(v)) { v = toDouble(v); } else if (isDateTime(v)) { isDateTime=true; v = toJavaDate(v); } } else if (value instanceof Integer) { v.addElement(new Double(((Integer)value).doubleValue())); } else if (value instanceof DateTime) { isDateTime=true; try { v.addElement (((DateTime) value).toJavaDate()); } catch (NotesException e) { v = new Vector(); v.addElement ("");
502
13 Objekt- und Speichertechniken } } else { v.addElement(value); } elements.put(fn.addName(fieldName), v); if (isDateTime) { fn.setType(fieldName, DateTime.class.getName()); }
} public void appendItemValue(String fieldName) { appendItemValue(fieldName, ""); } public void appendItemValue(String fieldName, int value) { appendItemValue(fieldName, new Double(value)); } public void appendItemValue(String fieldName, double value) { appendItemValue(fieldName, new Double(value)); } public Vector getItemValue(String fieldName) { if (!hasItem (fieldName)) { return new Vector(); } Object result = elements.get(fn.makeName(fieldName)); if (result != null) { //Werte werden intern nur als Vector gespeichert return (Vector) result; } else { return new Vector(); } } public String getItemValueString(String fieldName) { if (!hasItem (fieldName)) { return IS_IIOP?null:SIMULATE_R7?"":null; } Vector retVal = getItemValue (fieldName); try { return (String) retVal.firstElement(); } catch (Exception otherException) { return SIMULATE_R7?"":null; // sonst null/"". } } public int getItemValueInteger(String fieldName) { if (!hasItem (fieldName)) { return 0; } double val = getItemValueDouble (fieldName); if (!IS_IIOP) { int mult = val < 0?-1:1; return mult * Math .round(new Double (Math.abs (val)).floatValue()); } else { return Math.round(new Float(val).floatValue()); } } public double getItemValueDouble(String fieldName) { if (!hasItem (fieldName)) { return 0.0; } Vector retVal = getItemValue (fieldName); try { return ((Double) retVal.firstElement()).doubleValue(); } catch (Exception e) { return 0.0; } }
13.2.1 Das DJCacheDocument public Vector getItemValueDateTimeArray (String fieldName, Session session)throws NotesException { String type = fn.getType(fieldName); if (DateTime.class.getName().equals(type)) { Vector source = getItemValue(fieldName); Vector result = new Vector(); for (int i = 0; i<source.size();i++){ result.add (session .createDateTime((Date)source.elementAt(i))); } return result; } else { if (!IS_IIOP) { throw new NotesException ( NotesError.NOTES_ERR_NOT_A_DATE_ITEM, "Item value is not a date type"); } else { return new Vector(); } } } public Vector getAllItemNames () { return fn.getAllNames(); } public String getParentDocumentUNID() { return parentDocumentUNID; } public String getUniversalID() { return universalID; } public void replaceItemValue(String fieldName, Object value) { removeItem (fieldName); appendItemValue (fieldName, value); } public void removeItem (String fieldName) { if (!fn.hasName(fieldName)) { return; } Vector v = fn.getNames(fieldName); for (int i = 0; i < v.size(); i++) { elements.remove ((String) v.elementAt(i)); } fn.removeName(fieldName); } public boolean hasItem(String fieldName) { return fn.hasName(fieldName); } public void recycle() { } public String toString() { String tmp = elements.toString().replace(',', '\n'); return " " + tmp.substring(1, tmp.length() - 1); } public String getParentDatabaseFileName() { return parentDatabaseFileName; } public String getParentDatabaseFilePath() { return parentDatabaseFilePath; } public Database getParentDatabase(Session session) throws NotesException { if (parentDatabaseReplicaID==null || parentDatabaseReplicaID.equals("")) { throw new NotesException(DJCACHEDOMINOEXCEPTIONID, "Das DJCacheDocument hat keine Parent Datenbank.");
503
504
13 Objekt- und Speichertechniken } return session.getDbDirectory("") .openDatabaseByReplicaID(parentDatabaseReplicaID);
} private static boolean ok (String fieldName) { return fieldName != null && fieldName.length() > 0; } public Document getDocument (Session session) throws NotesException { if (universalID == null) { throw new NotesException(DJCACHEDOMINOEXCEPTIONID, "Das DJCacheDocument hat keine Universal ID."); } return getParentDatabase(session).getDocumentByUNID(universalID); } public Document createDocument (Database db) throws NotesException { Document result = db.createDocument(); Vector v = this.getAllItemNames(); for (int i=0, s=v.size(); i<s; i++) { String name=(String)v.elementAt(i); if (this.getType(name) != null && this.getType(name).equals(DateTime.class.getName())) { result.replaceItemValue (name, this.getItemValueDateTimeArray(name,db.getParent())); } else { result.replaceItemValue(name,this.getItemValue(name)); } } return result; } ... protected class FieldNames { ... } }
Listing 13-9 Das DJCacheDocument
•
String makeName (String fieldName) Nicht zu verwechseln mit addName. Gibt lediglich die korrekte Schreibweise des
intern zu verwendenden Namenseintrags zurück. Hinter einem Namen können sich viele (intern in FieldNames verwaltete) Namenseinträge verbergen. Dieser Mechanismus bildet das Verhalten der Document-Items ab, von denen bekanntlich mehrere mit gleichem Namen vorliegen dürfen, die aber in den Java-Objekten unter eindeutigen Namen verwaltet werden müssen. •
void setType (String fieldName, String className)
Optional kann einem Item-Namen ein Typ (Klassenname) zugeordnet werden. Dies wird insbesondere für DateTime-Items benötigt, damit eine Rückwärtskonvertierung von Date in DateTime stattfinden kann. •
String getType (String fieldName)
Gibt den Typ zu einem Item-Namen zurück oder null, falls keine Typzuordnung stattgefunden hat.
13.2.1 Das DJCacheDocument
505
Verhalten des DJCacheDocument Das DJCacheDocument bildet soweit möglich das Verhalten eines Document in einer lokalen Session ab. Einschränkungen Nicht alle Funktionen sind berücksichtigt. So sind z.B. Autoren- oder Leserfelder nicht implementiert. Ursprünglich verschlüsselte Items werden unverschlüselt gespeichert.
•
Vector getAllNames ()
Liefert alle Namen der vorliegenden Items in DJCacheDocument zurück. Namen doppelter Items werden nur einmal zurückgegeben, d.h. die Liste im Rückgabe-Vector ist eindeutig. • • •
boolean hasName (String fieldName) Liefert true, falls es einen Eintrag für den Item-Namen fieldName gibt. void removeName (String fieldName)
Löscht alle zu einem Item-Namen vorliegenden Einträge. Die übrigen Methoden werden für die interne Verarbeitung in FieldNames benötigt.
Während in der Instanzvariable fn die Item-Namen verwaltet werden, liegen die eigentlichen Werte (Items) des DJCacheDocument in der Instanzvariable elements . Um die Mimik des lotus.domino.Document, alle Items optional als Vector zu betrachten und zurückzugeben, möglichst einfach abzubilden, werden auch im DJCacheDocument alle Items als Vector in der HashMap elements gespeichert. Ein neues DJCacheDocument wird aus einem vorhandenen Document über den Konstruktor von DJCacheDocument erzeugt . In der gezeigten Implementierung wird auf eine Initialisierung verzichtet, falls versucht werden sollte, ein neues DJCacheDocument aus einem null-Wert zu erzeugen. Dieses Verhalten entspricht durchaus einem leeren Document-Rumpf. Für sämtliche getItemValue-Methoden wird immer null zurückgegeben. Durch replaceItemValue oder appendItemValue können neue Items hinzugefügt werden. Allerdings bleiben die Referenzen auf ein Original-Document leer. Im Normalfall werden zum initialisierenden Document verschiedene Parameter in Instanzvariablen zwischengespeichert , so dass es später möglich ist, z.B. die Datenbank, in der sich das Document befand, zu laden. Außerdem werden aus diesen Instanzvariablen die Methoden getUniversalID, getParentDocumentUNID (falls es sich um ein Antwortdokument handelte) und getParentDatabase bedient, wobei diese Methode im Gegensatz zu Document eine Session als Parameter benötigt. Anhand dieser Session und der intern gespeicherten Replik ID der ParentDatabase, kann dann die ursprünglich enthaltende Database neu geladen
506
13 Objekt- und Speichertechniken
Verhalten des DJCacheDocument Integer-Werte und Vectoren aus Integer-Werten werden als Double bzw. als Vectoren aus Double gespeichert. DateTime-Items müssen durch java.util.Date abgebildet werden, um nicht von einer Session abhängig zu sein. Alle Werte werden als Vector im DJCacheDocument gespeichert. Für DateTime-Objekte wird der Typ mit gespeichert um beim Auslesen zu wissen, dass in DateTime umgewandelt werden muss.
werden. So bleibt der Bezug zum erzeugenden Document erhalten. Die Methoden getUniversalID und getParentDocumentUNID funktionieren sogar völlig
transparent. Aus Bequemlichkeits- und Performancegründen werden zusätzlich der FileName und FilePath der übergeordneten Database mit gespeichert und über die Methoden getParentDatabaseFileName und getParentDatabaseFilePath zur Verfügung gestellt. Werden diese Werte benötigt – und dies ist für die Rückreferenzierung durchaus des öfteren der Fall –, erspart dies die (teure) Instanzierung der Database, die ja im Gegensatz zum Document für DJCacheDocument tatsächlich neu instanziert und geladen werden muss. Bei der Instanzierung des DJCacheDocument werden in der Methode init neben diesen Instanzvariablen nun die eigentlichen Items des Document in das DJCacheDocument übertragen. Hierfür wird eine Schleife über den Vector getItems in Document abgearbeitet. Diese Schleife verarbeitet die Items rückwärts . Hierdurch wird erreicht, dass später die Items in der gleichen Reihenfolge wie im Document zurückgegeben werden, falls mehrere Items gleichen Namens vorliegen. Zur Zeit verarbeitet das DJCacheDocument-Items vom Typ Text (normale Strings und Listen von Strings), Autorenfelder, DateTime-Items, Namensfelder und Items vom Typ Numbers (Integer, Double oder Listen davon) . Soll diese Liste erweitert werden, müsste die Methode appendItemValue erweitert werden. Aufgrund der Struktur der Klasse würde dies aber genügen, da alle übrigen betroffenen Methoden sich auf die Methode appendItemValue beziehen. Die Speicherung der Werte der Items des Document im DJCacheDocument wird bereits mit der Methode appendItemValue in DJCacheDocument abgebildet. Dies erleichtert spätere Erweiterungen. Diese Methode ist der Kern der Klasse und behandelt die meisten der „Eigenarten“ des Document. Hier werden die meisten notwendigen Typenkonvertierungen vorgenommen und dafür gesorgt, dass alle Werte intern als Vector verwaltet werden. Über die im Folgenden beschriebenen mehr oder weniger ungewöhnlichen Vorgehensweisen, Konvertierungen, Rückgabewerte, etc. lässt sich vortrefflich streiten.
13.2.1 Das DJCacheDocument
507
Im Einzelfall kann dies eine gute Gelegenheit sein, Optimierungen vorzunehmen. Grundlage aller Entscheidungen für die eine oder andere Vorgehensweise war immer, eine möglichst exakte Mimik des Verhaltens eines lotus.domino.Document zu erreichen. Der große Vorteil hiervon ist, dass Sie ein Document sehr einfach durch ein DJCacheDocument austauschen können, ohne befürchten zu müssen, dass Ihre Anwendung „zerbricht“. Beachten Sie, dass die vorliegende Implementierung das Verhalten einer lokalen Session abbildet. Soll mit DIIOP-Sessions gearbeitet werden, müssen die Unterschiede, die in Tabelle 7-3 und Tabelle 7-4 in Kapitel 7.1.2 beschrieben sind, nachimplementiert werden. Ebenso ist zu beachten, dass sämtliche Sicherheitsfunktionen eines Document ignoriert werden. Dies liegt durchaus in der Absicht dieser Klasse. Sie soll als einfacher und insbesondere persistenter und schneller Container eines Dokuments fungieren. Werden Verschlüsselung, Leserfelder oder Ähnliches benötigt, ist immer das ursprüngliche Document vorzuziehen. Hierbei ließe sich bei der vorgestellten Vorgehensweise kein Vorteil erzielen. Werden Sicherheitsfunktionen nicht benötigt, so lassen sich mit dem DJCacheDocument erhebliche Geschwindigkeitsvorteile erzielen, wie weiter unten (s. Kap. 13.2.2) aufgezeigt wird. Wird versucht, mit ungültigen Item-Namen ein Item zu erzeugen, so wird dies ignoriert . Wird versucht, einen null-Wert in ein Item zu schreiben , so wird ein leerer String ins Item geschrieben. Dies entspricht dem Verhalten von Document. Bei der Implementierung von DJCacheDocument wurde festgelegt, dass eventuell vorkommende Konvertierungen soweit möglich immer bereits beim Schreiben in der Methode appendItemValue erledigt werden. Daher werden Vectoren aus Integerwerten oder diskrete Integerwerte bereits in dieser Methode in Double umgewandelt. Bei Vectoren wird die Hilfsmethode isInteger benötigt, die true zurückgibt, wenn mindestens ein Element des Vectors vom Typ Integer ist. In diesem Fall wird mittels der Methode toDouble versucht, die Elemente des Vectors in Double umzuwandeln. Insgesamt wurde versucht die Klasse DJCacheDocument robust aufzubauen und tolerant mit Eingabe- oder Konvertierungsfehlern umzugehen. Daher werden Konvertierungsfehler einfach ignoriert, indem solche Elemente nicht geschrieben werden. Dies wird auch in der Methode so gehandhabt. Enthält ein Vector Integer und gleichzeitig Werte anderer Datentypen, so wird bei der Benutzung von appendItemValue ein Vector in das DJCacheDocument geschrieben, das nur die in Double umgewandelten Elemente enthält. Andere werden aus dem Vector entfernt. Sollen lotus.domino.DateTime-Elemente in das DJCacheDocument geschrieben werden, müssen diese in java.util.Date umgewandelt werden, da an den Typ DateTime die Domino-Session gekoppelt ist. Diese Umwandlung wird sowohl für Vectoren aus DateTime-Elementen als auch für diskrete Werte durchgeführt. Hierbei wird ebenso tolerant und robust wie für Integer-Werte verfahren.
508
13 Objekt- und Speichertechniken
. Dieses Intern speichert das DJCacheDocument sämtliche Werte als Vector Verfahren wird gewählt, da das lotus.domino.Document Werte sowohl diskret (z.B. durch getItemValueString) als auch als Vector (getItemValue) zurückgeben kann und so die Rückgabe vereinfacht wird. Sämtliche andere Objekte, die der Methode appendItemValue als Parameter übergeben werden, werden in einem Vector „verpackt“ , wenn es sich nicht ohnehin um einen solchen gehandelt hat. Nun muss ein Objekt nur noch in der HashMap gespeichert werden . Hierbei wird durch addName in FieldNames ein neuer Name generiert. Um eventuelle Doppelungen etc. kümmert sich bereits die Klasse FieldNames, so dass der Problematik von doppelten Items hier keine Aufmerksamkeit mehr geschenkt werden muss. Die Klasse FieldNames und insbesondere die Methode addName ist so gebaut, dass appendItemValue vorhandene Items nicht überschreibt, sondern parallel intern unter eindeutigen Namen gespeichert werden. Ähnlich wie im Vorbild Document ist es allerdings nicht (ohne Weiteres) möglich, solche Doppelungen wieder auszulesen81. , der es später erFür DateTime-Items wird intern noch ein Type gespeichert möglicht, zu erkennen, dass die interne Darstellung als Date wieder zurückverwandelt werden muss (darf). Da alle appendItemValue- und replaceItemValue-Methoden auf appendItemValue (String, Object) zurückgeführt werden können, ist die Mimik des Schreibens hiermit beschrieben. Als Wrapper steht die Methode appendItemValue (String) zur Verfügung, die lediglich ein leeres Item (leerer String) unter dem übergebenen Namen erzeugt und zwei neu eingeführte Varianten, die so im Document nicht zur Verfügung stehen, es aber erleichtern, int- und double-Werte zu schreiben, ohne dies vorher in ein Object umwandeln zu müssen . Auch die Methode replaceItemValue wird auf appendItemValue zurückgeführt. Hier wird lediglich zuvor das alte Item mittels removeItem gelöscht, wobei letztere Methode nicht nur das ursprüngliche Objekt aus der HashMap löscht, sondern auch den Namen in der Klassenvariablen fn durch den Aufruf von fn.removeName(fieldName) aufräumt. Die getter-Methoden bilden die Eigenarten des Document ab, insbesondere, wenn es um die Rückgabewerte bei dem Versuch geht, leere Items oder Items unerwarteten Datentyps zu lesen. Hierbei wurde Tabelle 7-3 zugrunde gelegt. Der einfachste Fall ist die Methode getItemValue (String fieldName). Sie gibt in jedem Fall einen Vector zurück. Im Fehlerfall oder gibt es das Item nicht , wird ein leerer Vector zurückgegeben. Hier sei nochmals darauf hingewiesen, dass hier makeName anstelle von addName verwendet werden muss, da kein Element hinzugefügt wird, sondern lediglich der gültige (interne) Name des Items benötigt wird, um das Element aus der HashMap auslesen zu können. Die Methode getItemValueString muss null bzw. "" zurückgeben, wenn es das Item nicht gibt oder wenn das Item nicht in einen String umgewandelt werden kann . Die Unterscheidung, ob für das original lotus.domino.Document in diesen Fällen null oder "" zurückgegeben wird, hängt (leider) davon ab, ob die Session lokal oder über DIIOP aufgebaut wird. Dazu hat sich das Verhalten noch seit 81
Dies ist im Document durch das Item-Modell – unter Zuhilfenahme von item.remove (s. Kap. 7.1.2) – möglich. Das DJCacheDocument sieht dies (noch) nicht vor.
13.2.1 Das DJCacheDocument
509
Domino R6.x geändert. Um dem Anspruch zu genügen, dass das DJCacheDocument auch diese Ausnahmefälle ebenso wie Domino handhabt, wurden hier entsprechende Entscheider eingebaut. Im Normalfall gibt die Methode getItemValueString das erste Element des gespeicherten Objekts als String zurück . Hier wird bewusst der Cast (String) verwendet und auf den Einsatz von toString() verzichtet. Nur so wird eine Äquivalenz zum Document erreicht. Ähnliches gilt für getItemValueInteger und getItemValueDouble, die 0 bzw. 0.0 im Fehlerfall zurückgeben müssen, wobei getItemValueInteger auf getItemValueDouble zurückgeführt werden kann . Leider rundet Domino (für lokale Verbindungen) anders als Java. Dies wird hier ebenfalls berücksichtigt. Schließlich bleibt noch getItemValueDateTimeArray, die für das DJCacheDocument anders als für das Document den zusätzlichen Parameter Session benötigt, damit der interne Date-Wert in einen DateTime-Wert zurückgewandelt werden kann . Dank des intern gespeicherten Type kann festgestellt werden, ob das Item auch wirklich ursprünglich ein DateTime (und nicht etwa ein orginäres Date)-Objekt war und es wird eine Exception geworfen, wenn dies nicht der Fall ist. Anders als im Document gibt getItemValue das interne Date-Objekt anstelle eines DateTime-Objektes zurück, da getItemValue keine Session zur Verfügung hat. Zur bequemeren Verwendung wurden die Methoden createDocument und getDocument eingeführt, die wiederum aus dem DJCacheDocument ein neues lotus.domino.Document erzeugen bzw. das Original laden, aus dem das DJCacheDocument erzeugt wurde, und somit eine Rückwärtskonvertierung ermöglichen. Insgesamt stehen folgende öffentliche Methoden zur Verfügung: •
DJCacheDocument(Document originalDoc) throws NotesException Im Gegensatz zu einem lotus.domino.Document hat DJCacheDocument einen Konstruktor. Er dient dazu, aus einem lotus.domino.Document ein DJCacheDocument zu erzeugen. Hierbei werden alle Items vom Typ Item.TEXT,
Item.AUTHORS, Item.DATETIMES, Item.NAMES, Item.NUMBERS übernommen. Autorenfelder werden als Textfelder gespeichert. Das erzeugte DJCacheDocument ist unabhängig vom lotus.domino.Document. Das DJCacheDocument darf auch mit einem null-Parameter erzeugt werden . Dann liefern zwar einige Methoden (getParentDatabase oder getParentDatabaseFileName) Exceptions oder null, aber die Methoden replaceItemValue usw. können wie gewohnt verwendet werden. Mit originalDoc==null wird ein neues leeres DJCacheDocument instanziert, das unabhängig von einem lotus.domino.Document ist. •
•
void appendItemValue(String fieldName) Fügt ein Item mit Namen fieldName und Wert "" (leerer String) DJCacheDocument hinzu. void appendItemValue(String fieldName, double value)
dem
Fügt dem Item einen Vector mit einem Element, das ein Double-Objekt mit Wert value enthält, hinzu.
510 •
13 Objekt- und Speichertechniken void appendItemValue(String fieldName, int value)
Fügt dem Item einen Vector mit einem Element, das ein Integer-Objekt mit Wert value enthält, hinzu. •
•
• •
void appendItemValue(String fieldName, Object value) Ist value ein Vector, wird das Objekt direkt hinzugefügt. Alle übrigen Objekte werden zuvor als Element in einen Vector gelegt. Integer und Vectoren aus Integer werden in Double- und DateTime-Objekte bzw. Vectoren aus DateTime in Date umgewandelt. Document createDocument(Database db) throws NotesException
Erzeugt ein neues lotus.domino.Document mit den Items des DJCacheDocument in der Datenbank db. Rückgabewert ist das neue Document. Es wird nur als Objekt zurückgegeben und ist nach dem Erzeugen nicht automatisch gespeichert (ein save() muss ausdrücklich nach dem createDocument ausgeführt werden). Vector getAllItemNames() Liefert alle Namen aller Items als Vector aus String . Document getDocument(Session session) throws NotesException
Versucht anhand der als Parameter übergebenen Session und der intern gespeicherten Universal ID das Original-lotus.domino.Document, aus dem das DJCacheDocument ursprünglich erzeugt worden war, wieder zu laden. Hierfür wird die ebenfalls intern anhand des Original-Dokument gespeicherte Replik ID der Datenbank, aus der das Original stammte, verwendet. • •
• • • •
Vector getItemValue(String fieldName) Liefert den Wert des Items fieldName als Vector . Siehe auch Tabelle 7-3. Vector getItemValueDateTimeArray(String fieldName, Session session) throws NotesException Liefert das DateTime-Objekt des Items mit Namen fieldName als Vector, sofern unter diesem Namen ein DateTime-Objekt im Original-Document gespeichert war. Ansonsten eine NotesException. Siehe auch Tabelle 7-3). double getItemValueDouble(String fieldName) Liefert den Wert des Items fieldName als double . Siehe auch Tabelle 7-3. int getItemValueInteger(String fieldName) Liefert den Wert des Items fieldName als int . Siehe auch Tabelle 7-3. String getItemValueString(String fieldName) Liefert den Wert des Items fieldName als String . Siehe auch Tabelle 7-3. Database getParentDatabase(Session session) throws NotesException Liefert die Datenbank, in der das Original lotus.domino.Document gespeichert war, als Database-Objekt . Daher wird als Parameter eine Session benötigt. Eine NotesException wird geworfen, wenn diese Datenbank nicht geladen werden kann oder falls das DJCacheDocument aus einem null-Objekt
erzeugt wurde. •
String getParentDatabaseFileName()
13.2.1 Das DJCacheDocument
511
Liefert den (intern zwischengespeicherten) Dateinamen der Datenbank, in der das ursprüngliche lotus.domino.Document gespeichert war ohne diese Datenbank zu laden oder null, falls das DJCacheDocument aus einem nullObjekt erzeugt wurde. •
String getParentDatabaseFilePath()
Liefert den (intern zwischengespeicherten) Dateipfad der Datenbank, in der das ursprüngliche lotus.domino.Document gespeichert war oder null, falls das DJCacheDocument aus einem null-Objekt erzeugt wurde. •
String getParentDocumentUNID()
Liefert die (intern zwischengespeicherte) Universal ID eines optionalen Hauptdokuments des ursprünglichen lotus.domino.Document, aus dem das DJCacheDocument erzeugt worden war oder null, falls das DJCacheDocument aus einem null-Objekt erzeugt wurde oder falls es kein Hauptdokument gab – wenn also das Original-Document kein Antwortdokument war. • •
String getType(String fieldName) Liefert den Type eines Items. Wird nur für DateTime-Items unterstützt. String getUniversalID()
Liefert die (intern zwischengespeicherte) Universal ID des Originallotus.domino.Document, aus dem das DJCacheDocument erzeugt wurde oder null, falls das DJCacheDocument aus einem null-Objekt erzeugt wurde. •
boolean hasItem(String fieldName) Liefert true, falls das DJCacheDocument ein Item mit Namen fieldName hat
•
void recycle()
. Hat keine Wirkung. Wurde nur der Vollständigkeit halber implementiert • •
•
.
removeItem(String fieldName) Entfernt das Item mit Namen fieldName . void replaceItemValue(String fieldName, Object value) Ersetzt das Item mit Namen fieldName mit dem Wert von value. Arbeitet ansonsten wie appendItemValue . String toString() Liefert eine sehr einfach gehaltene String-Repräsentation des DJCacheDocument .
Dem Code zum Buch wurde noch ein JUnit-Test in der Klasse DJCacheDocumentTest hinzugefügt. Dieser basiert auf einer lokalen Session. Das Verhalten wird im direkten Vergleich mit einem lotus.domino.Document geprüft, so dass sichergestellt ist, dass das Verhalten wirklich eins zu eins umgesetzt wurde. Der Code bietet die Möglichkeit, den Verbindungsaufbau auf eine DIIOP-Session umzustellen. Hier lässt sich dann gut erkennen, dass die Implementierung auch die Besonderheiten unterstützt, soll es in einer DIIOP-Umgebung eingesetzt werden,. Gleichzeitig bietet der JUnit-Test einen guten Ansatz, das DJCacheDocument Ihren Bedürfnissen anzupassen.
neue DJCacheDocument erstellen: 41844ms neue Document erstellen: 22515ms x 514 DJCacheDocument.getItemValue Operationen: 235ms x 514 Document.getIemValue Operationen: 31125ms x 514 DJCacheDocument.replaceIemValue Operationen: 3937ms x 514 Document.replaceIemValue Operationen: 35078ms
Tabelle 13-1 Performance Test – DJCacheDocument und Document im Vergleich
13.2.2
Performancevergleich
Um einschätzen zu können, wie die Klasse sich in der Laufzeit verhält, wurde dem JUnit-Test ein Abschnitt hinzugefügt, der die Geschwindigkeit für die Erstellung des DJCacheDocument und das Ein- und Auslesen von Daten untersucht und mit dem Document vergleicht. Das Ergebnis eines Testlaufs wird in Tabelle 13-1 dargestellt, wobei die Zahlen für drei Kombinationen aus Anzahl und Größe der Items ermittelt wurden. Alle Ergebnisse wurden für die Durchführung von 1000 Wiederholungen ermittelt. Hierbei ist festzustellen: • • •
Das Lesen von Items im DJCacheDocument ist erheblich schneller (bis zu Faktor 100) als für das Document. Das Schreiben von Items im DJCacheDocument ist erheblich schneller (bis zu Faktor 10) als für das Document. Das Erstellen des DJCacheDocument ist unverhältnismäßig teuer. Es kann bis zu doppelt so lange dauern wie die Erstellung eines Document. Dies ist im Wesentlichen dem Auslesen aus dem Document zuzuschreiben.
Fazit: Ist das DJCacheDocument erst einmal erstellt, so bietet es einen erheblichen Geschwindigkeitsvorteil. Damit ist es prädestiniert für den ursprünglich geplanten Einsatzzweck: Das Cachen von teuer zu erzeugenden und häufig benötigten Konfigurationsdokumenten.
13.2.3 Verwendung des DJCacheDocument
513
Neben den Vorteilen des CacheDocument (Performance, Cachebarkeit) darf allerdings nicht vergessen werden, dass für dieses Objekt keinerlei der für Domino sonst üblichen Sicherheitsfunktionen implementiert sind.
13.2.3
Verwendung des DJCacheDocument
Das so implementierte DJCacheDocument erfüllt die oben definierten Eigenschaften. Durch die strenge Nachahmung der Mimik von Document lässt sich nun jedes lotus.domino.Document einfach über den Konstruktor new DJCacheDocument (Document) in ein solches umwandeln. Nun muss ab diesem Punkt im Code nur noch das neue DJCacheDocument weiterverwendet werden. In den meisten Fällen muss nur wenig oder kein Code angepasst werden, solange dieses DJCacheDocument nicht selbst wieder zum Parameter eines Domino-Java-Objekts wird. Nun soll natürlich auch an einem einfachen Beispiel die Verwendung des DJCacheDocument in einem Cache demonstriert werden. Zu diesem Zweck finden Sie in Listing 13-10 eine Implementierung unter Zuhilfenahme des ShiftOne Object Cache [ShiftOne]. Dieser Cache kennt bereits viele verschiedene Cache-Mechanismen und bietet neben einer leichten Konfiguration zusätzliche Wrapper, die ihn wirklich universell einsetzbar machen. ... public class DJCacheDemo implements Runnable { ... public void run() { Session session = null; Database db = null; Document doc = null; try { session = NotesFactory.createSession(HOST, USER, PASSWORD); db = session.getDatabase(session .getServerName(), "djbuch/djbuch.nsf"); Cache cache = DJCache.getCache(); doc = makeADoc(db); DJCacheDocument djDoc = new DJCacheDocument(doc); cache.addObject("key", djDoc); doc = GC.recycle(doc); djDoc = null; djDoc = (DJCacheDocument) cache.getObject("key"); doc = djDoc.createDocument(db); doc.save (true,false); System.out.println ("\n\nDJCacheDocument: "); DemoTools.dumpDoc (djDoc); System.out.println ("\n\nlotus.domino.Document: "); DemoTools.dumpDoc (doc); } catch (Exception e) { ... } finally { GC.recycle(db); GC.recycle(session); } } }
Listing 13-10 Die Verwendung des DJCacheDocument in einem Cache
514
13 Objekt- und Speichertechniken Im Code zum Buch im Package djbuch.kapitel_13 befindet sich die Klasse
DJCache, die das Setup des Caches erledigt und einen internen Singleton besitzt, in dem ein Cache geführt wird. Dieser kann über getCache () bezogen werden. Hilf-
reich ist, dass der ShiftOne Cache bereits eine Synchronized-Implementierung z.B. eines LRU Caches (least recently used) besitzt, die notwendig wird, wenn z.B. ein gemeinsamer Cache für Konfigurationsdokumente beim Einsatz von Servlets verwendet werden soll (also in Multithreaded-Umgebungen). Für den Einsatz des DJCacheDocument wird in Listing 13-10 zunächst ein Cache bezogen und mittels einer hier nicht aufgeführten Hilfsmethode ein lotus.domino.Document erzeugt. Die hierfür verwendete Methode makeADoc legt lediglich ein Dokument in der Datenbank an und versieht es mit einigen Items. Aus diesem Document kann ein DJCacheDocument erzeugt und dieses wiederum in den Cache gelegt werden. Um zu verdeutlichen, dass das DJCacheDocument wirklich im Cache bleibt, wird sowohl das Original-lotus.domino.Document recycelt als auch die Referenz des DJCacheDocument auf null gesetzt. Wie erwartet kann das DJCacheDocument wieder aus dem Cache geladen und sogar anhand der Methode createDocument wieder in ein lotus.domino. Document umgewandelt werden. Der Vergleich zeigt, dass diese beiden Dokumente gleich sind – bis auf die Einschränkung, dass aus dem Item $updatedby im DJCacheDocument ein Text-Item wurde. Ein kleiner Hinweis noch am Rande: Profildokumente sollen in Domino genau diese Aufgabe erfüllen. Es sind gecachte Dokumente, die performant referenziert werden können. Einzige Einschränkung ist hier, dass weiterhin eine Session benötigt wird, um ein Profildokument zu laden. Das DJCacheDocument wurde aber genau für den Fall entwickelt, dass keine (kontinuierliche) Session zur Verfügung steht.
13.3
Domino-Objekte in Multithreading-Umgebungen
Sollen Domino-Java-Objekte in Multithreaded-Umgebungen verwendet werden, so sind einige Besonderheiten zu berücksichtigen. Grundsätzlich können zwar Domino-Java-Objekte über Threads geteilt werden, hierbei sind aber besondere Regeln zu beachten. In Kapitel 5.2 wurde bereits ein Sonderfall vorgestellt: die Nutzung von Domino-Java-Objekten in Servlets. Dieser ist jedoch unkritisch (wie auch weiter unten noch aufgezeigt wird), da für jede Instanz des Servlets ein eigener Thread vorgesehen ist und dieser jeweils über die statischen Methoden in NotesThread in einen eigenen (Pseudo-) NotesThread konvertiert werden. Zu den Besonderheiten in Multithreaded-Umgebungen heißt es im Domino Designer-Handbuch82:
82
[Lotus, DesignerR65] – help65_designer.nsf – Kapitel NotesThread.class – Multithreading Issues. Nummerierung hinzugefügt durch den Autor.
13.3.1 Gemeinsame oder geteilte Session
515
„You should avoid multithreading unless you have good reason to use it, such as proceeding while file input/output and Web requests are processing. Observe the following guidelines: 1
2 3
4 5 6
Within a session, Domino Objects are shared, synchronized, and recycled across threads. Using a different session on each thread loses these capabilities; you must explicitly manage synchronization and recycle on a per-thread basis. Do not use DbDirectory across threads. Accessing an existing document on multiple threads is permissible, but accessing it on just one thread simplifies memory management. Restricting accesses to one thread allows you to recycle without checking the other threads. Creating documents across threads is always safe and these objects can be recycled without reference to the other threads. Profile documents are cached on a per-thread basis. In the event of an update contention, the last thread updating wins. Take care not to delete a document needed for navigation by a view or collection on another thread. When child objects are used on threads other than the parent, keep the parent thread alive until all child threads terminate. This is particularly important when using Domino Objects in AWT event handlers.“
An dieser Stelle soll nochmals darauf hingewiesen werden, dass die statischen Methoden sinitThread und stermThread immer nur einmal innerhalb eines Threads eingesetzt werden dürfen. Daher ist es gute Praxis, in der Regel NotesThreads durch Implementierung von Runnable (oder durch Erweiterung von NotesThread) zu erzeugen.
13.3.1
Gemeinsame oder geteilte Session
„Innerhalb einer Session werden Domino-Objekte geteilt, synchronisiert und recycelt. Wird für jeden Thread eine eigene Session aufgebaut, geht diese Eigenschaft verloren; Synchronisation und Recycling müssen dann für jeden Thread separat und manuell durchgeführt werden.“ 83 In dem ersten Punkt spricht das Handbuch zwar die Möglichkeit an, per NotesThread nt = new NotesThread ((Runnable) myClass); nt.start(); – wobei myClass Runnable implementiert – einen neuen NotesThread zu instanzieren, aber in diesem NotesThread keine eigene Notes-Session über die NotesFactory zu beziehen, sondern diese über jeden neuen Thread zu teilen. Dies erfordert, dass die Session außerhalb von myClass erzeugt und im Konstruktor von myClass mit übergeben wird, um dort zur Verfügung zu stehen. Die positive Folge ist, dass Objekte nur in einem der Threads erzeugt und recycelt werden müssen. Dies kann aber gleichzeitig ein Nachteil sein. Wenn Sie z.B. ein und dieselbe Datenbank in mehreren Threads über getDatabase in Session beziehen, so 83
Übersetzung durch den Autor.
516
13 Objekt- und Speichertechniken
handelt es sich aus Domino-Sicht um ein und dasselbe Objekt. Folglich dürfen Sie dieses Database-Objekt lediglich in einem der Threads recyceln. Hierauf wird weiter unten noch eingegangen. •
•
•
Domino-Java-Objekte können nicht immer miteinander interagieren, wenn sie nicht mit derselben Session erzeugt wurden. So ist es zum Beispiel durchaus möglich, ein Document, das über die eine Session bezogen wurde, mittels eines Database-Objekts, das aus einer anderen Session stammt, in diese Datenbank zu kopieren. Für DocumentCollections trifft dies nicht zu. DocumentCollections können nur Dokumente aufnehmen, die auch in derselben Session bezogen wurden, andernfalls wird dies mit der NotesException „Document is from a different database“ quittiert. Vermeiden Sie daher diese Mischung aus geteilten Objekten und nicht geteilter Session. Datenbanken, die über getDatabase in Session bezogen werden, verweisen immer auf ein und dasselbe Database-Java-Objekt, wenn sie durch dieselbe Session erzeugt wurden (equals in Database liefert true). Werden zwei Database-Objekte aus verschiedenen Sessions erzeugt, so werden auch jeweils verschiedene Java-Objekte zurückgegeben. Analog gilt dies für Ansichten, die über getView in Database bezogen werden.
13.3.2
DbDirectory darf nicht über Threads geteilt werden
„Benutzen Sie DbDirectory nicht über mehrere Threads“. Diese Regel fordert lediglich, dass Sie ein DbDirectory-Objekt nicht an andere Threads übergeben dürfen.
13.3.3
Dokumente und Multithreading
„Im Prinzip ist es erlaubt, auf Dokumente threadübergreifend zuzugreifen. Wird nur aus einem Thread zugegriffen, vereinfacht dies die Speicherverwaltung. Wird der Zugriff auf einen Thread beschränkt, so kann ein solches Dokument recycelt werden, ohne die übrigen Threads zu berücksichtigen. Das Erstellen neuer Dokumente ist immer sicher. Das Recycling solcher Dokumente kann ohne Rücksicht auf andere Threads durchgeführt werden.“ Die Hilfe ist an dieser Stelle nicht sehr ausführlich. Gemeint ist Folgendes: Wenn ein Dokument in einem Java-Objekt referenziert wird und dieses Java-Objekt über mehrere NotesThreads geteilt wird – indem es z.B. im Konstruktor oder über eine Methode an einen anderen Thread übergeben wird –, so ist dies zwar erlaubt, aber es ist mit nachteiligem Einfluss auf das Speichermanagement zu rechnen (mögliche Performanceeinbußen).
13.3.3 Dokumente und Multithreading
517
Best Practice Müssen Domino-Java-Objekte (insbesondere Document) von Threads gemeinsam genutzt werden, sollte auch die Session, auf der diese Objekte basieren, gemeinsam genutzt werden, d.h. die Session wird bereits bei der Erzeugung eines Threads als Parameter übergeben. Dies ist nicht zu verwechseln mit der gemeinsamen Nutzung eines Domino-Dokuments, das in der NSF-Datenbank gespeichert ist. Diese Dokumente können von beliebigen Threads jeweils autark mit eigener Domino-Session in ein eigenes Java-Objekt geladen und dem gesamten Lifecycle – Laden, Verändern, Löschen oder Speichern und Recycling – zugeführt werden. Wird die Session gemeinsam genutzt, dürfen Objekte, die von den Threads gemeinsam genutzt werden, nur von einem Thread dem Recycling zugeführt werden. Werden dieselben Datenbanken oder Ansichten von mehreren Threads bei gemeinsamer Session benötigt, so sollten diese Objekte möglichst nur an einer Stelle erzeugt und wieder aufgeräumt werden, um Fehler zu vermeiden, da Domino für getView und getDatabase immer ein und dasselbe Java-Objekt zurückgibt (bei gemeinsamer Session). Bei der gemeinsamen Nutzung von Domino-Objekten und insbesondere der Session sollte sichergestellt werden, dass der Thread, der diese Objekte erzeugt, bis zuletzt bestehen bleibt. Die Erweiterung von NotesThread oder die Implementierung von Runnable ist der Verwendung von sinitThread und stermThread zur Erzeugung von NotesThreads vorzuziehen.
Hinzugefügt sei hier, dass eine derartige Übergabe nur zu empfehlen ist, wenn sich die Threads die Session teilen. Es ist nicht (immer) zulässig, in zwei getrennten NotesThreads über die NotesFactory eine jeweils eigene Session aufzubauen und dann Domino-Java-Objekte zwischen den Threads auszutauschen (siehe Punkt 1). Oder anders formuliert: •
•
Domino-Java-Document-Objekte können miteinander interagieren, wenn sie mit derselben Session erzeugt wurden, auch wenn dies über mehrere Threads geschieht. Mehrere Sessions in mehreren Threads können völlig unabhängig voneinander ein und dasselbe Dokument einer Datenbank laden. Dies geschieht jedoch völlig unabhängig voneinander, so dass beide dem Recycling zugeführt werden müssen. Hierbei müssen Speicherkonflikte beachtet werden.
Gleichzeitig ist es aber sehr wohl zulässig, z.B. ein und dasselbe (in der Datenbank gespeicherte) Dokument in einer Domino-Datenbank z.B. per getDocumentByUNID in zwei getrennten Threads mit zwei getrennten Sessions zu laden. Die hieraus entstehenden individuellen Java-Objekte – einschließlich der dahinter liegenden C++ Re-
518
13 Objekt- und Speichertechniken
präsentation – sind unabhängig voneinander und müssen auch jeweils separat dem Recycling zugeführt werden. Der einzige Punkt, der für dieses Szenario zu berücksichtigen ist, ist die Synchronisation. Wird diese unterlassen, wird es bei der Verwendung von save() zu Speicherkonflikten kommen. Getrennte Sessions in getrennten Threads sind nichts anderes als unabhängige Client-Zugriffe, wie sie z.B. durch (mehrere) Notes Clients entstehen.
13.3.4
Profildokument und Multithreading
„Profildokumente werden je Thread gecacht. Falls mehrere Threads dasselbe Profildokument ändern, setzt sich die (zeitlich) letzte Änderung durch.“ Anders als „normale“ Dokumente kennen Profildokumente keine Speicher- oder Replizierkonflikte. Dementsprechend ist es konsequent, dass sich hier – wie für diesen Fall üblich – die letzte Änderung durchsetzt.
13.3.5
Dokumente löschen im Multithreading
„Beim Löschen von Dokumenten muss berücksichtigt werden, dass keine solchen Dokumente gelöscht werden, die von Ansichten oder Collections in anderen Threads für die Navigation benötigt werden.“ Hierbei handelt es sich um ein grundsätzliches (und für Multithreading typisches) Problem – unabhängig von Domino. Stellen Sie sicher, dass Sie keine Objekte löschen, die noch von anderen Threads benötigt werden. Dies macht insbesondere Probleme, wenn der eine Thread versucht, eine Ansicht oder DocumentCollection zu durchschreiten (mit z.B. getNextDocument()), und ein anderer Thread eines der in der Collection enthaltenen Dokumente löscht. Dies wird zu Fehlern führen, da die Collection gewissermaßen „unterbrochen“ wurde.
13.3.6
Parent- und Child-Objekte im Multithreading
„Wenn Objekte, die von einem übergeordneten Objekt abhängen, in einem anderen Thread als dem des übergeordneten Objekts verwendet werden, so muss sichergestellt werden, dass der Thread des übergeordneten Objekts so lange aktiv bleibt, wie Threads, die abhängige Objekte verwenden. Dies ist insbesondere wichtig, wenn Domino-Objekte in AWT Event-Handlern eingesetzt werden.“ Ein einfaches Beispiel für eine derartige Situation ist ein RichTextItem, das aus einem Document bezogen wurde. Wird das RichTextItem in einem anderen Thread als das Document verwendet, so muss der Thread, in dem das Document bezogen wird, mindestens solange aktiv sein wie der Thread, der das RichTextItem verwendet.
13.3.7 Multithreading mit gemeinsamer Session
519
... public class DJStressRunnerOneSession { ... //diverse Konstanten private static final boolean[] info = new boolean [STRESS_COUNT]; private static DJStressTestExternalSession [] st = new DJStressTestExternalSession [STRESS_COUNT]; private static NotesThread [] nt = new NotesThread [STRESS_COUNT]; public static void main(String[] args){ Session session = null; try { if (HOST == null) {NotesThread.sinitThread();} session=NotesFactory.createSession (HOST, USER, PASSWORD); long L = System.currentTimeMillis(); for (int i=0; i < STRESS_COUNT; i++) { st [i] = new DJStressTestExternalSession (session,"Thread " + i); nt [i] = new NotesThread( st [i]); nt [i].start(); info[i] = false; } int allDone=0; for (int dCount = 0; dCount < MAX_DELAY_COUNT && allDone!=STRESS_COUNT; dCount++) { int tmpDone = allDone; for (int i = 0; i < STRESS_COUNT; i++) { if (st [i].isDone() && !info [i]) { allDone++; info [i] = true; } } if (tmpDone != allDone) { System.out.println (allDone + " Threads done. (" + (System.currentTimeMillis() - L) + ")"); } Thread.sleep(DELAY); } } catch (Exception e) { e.printStackTrace(); } finally { ... //Überfallige Threads stoppen, recycle, stermThread } } }
Die Handhabung von Multithreaded-Anwendungen soll anhand einer kleinen Applikation erläutert werden. Das Beispiel ist die Basis eines Stresstest-Moduls, mit dem Anwendungen (oder z.B. ein Domino-Server) unter Last gebracht und getestet werden können. Die Anwendung besteht aus zwei Klassen: Einer Basisklasse, die dazu dient eine beliebe Anzahl von Tests in einem jeweils eigenen Thread zu starten (Lis-
Listing 13-12 Domino-Multithreaded-Stresstest – Test
ting 13-11) und dem eigentlichen Test (Listing 13-12). Der Test entstammt einem realen Anwendungsfall. Mit ihm wurde das Verhalten der in Kapitel 13.1.2 vorgestellten CMSDocument-Klasse getestet. Das Startermodul enthält zunächst drei Arrays, die jeweils neben einem Flag für die Fertigstellung die Tests basierend auf der Klasse DJStressTestExternalSession und die hieraus aufgebauten NotesThreads enthalten .
13.3.7 Multithreading mit gemeinsamer Session
521
Auch wenn die Session über sämtliche Instanzen des Tests geteilt und eingangs wie gewohnt über die NotesFactory erzeugt wird, müssen die eigentlichen Tests Runnable implementieren und dann über new NotesThread (Runnable) in einen NotesThread konvertiert werden. Hierfür wird innerhalb einer Schleife, die sämtliche (STRESS_COUNT) Tests startet, je ein neues Test-Objekt mit new DJStressTestExternalSession erzeugt. Beachten Sie, dass nur eine einzige Session an sämtliche DJStressTestExternalSession-Objekte übergeben wird. Nun können die Tests jeweils gestartet werden. Zusätzlich wird festgehalten, dass die Tests noch nicht fertiggestellt sind. Nachdem die Tests gestartet wurden, muss eigentlich nur darauf gewartet werden, dass die Threads ihre Fertigstellung (s.u.) zurückmelden. In einer Schleife, die durch eine maximale Wartezeit (oder alternativ die Fertigstellung aller Threads) begrenzt ist , wird für alle gestarteten Threads in einer weiteren Schleife der Status der Threads überprüft . Durch einen kleinen Delay wird sichergestellt, dass die Überprüfung während der Wartezeit nicht zu viele Ressourcen blockiert. Listing 13-12 zeigt den eigentlichen Test. Er sucht in der Datenbank djbuch.nsf nach Dokumenten mit einem bestimmten Titel und schaltet das Flag isActive für diese Dokumente. Hierbei kommt die Klasse CMSDocument zum Einsatz. Die Klasse DJStressTestExternalSession implementiert Runnable, um als Thread fungieren zu können. Seine Domino-Session bezieht sie über den Konstruktor . Das Database-Objekt db und die Ansicht view werden anhand dieser externen Session bezogen. Für das Verständnis des Multithreadings für Domino-Java-Objekte ist wichtig zu verstehen, dass für alle (!) Threads der Aufruf von getDatabase und von getView immer ein und dasselbe Java-Objekt liefert. Folglich wäre es fatal, diese Objekte in der Klasse DJStressTestExternalSession zu recyceln, da sonst für alle übrigen Threads diese Objekte nicht mehr verfügbar wären. Dies wird durch die auskommentierten Recycle-Aufrufe angedeutet . Für Document gilt dies nicht. Hier kann die Schleife wie gewohnt mit getNextDocument durchschritten und das Recycling durchgeführt werden. Die Verwendung von getInstance in CMSDocument wird in Kapitel 13.1.2 näher erläutert. Auch die DocumentCollection ist individuell für den Thread und wird dem Recycling zugeführt . Wie oben bereits angedeutet, konnte weder die Datenbank noch der View, die in DJStressTestExternalSession erzeugt werden, dort auch wieder recycelt werden . Die in den beiden Listings (Listing 13-11 und Listing 13-12) gezeigte Anwendung enthält in diesem Zustand daher noch ein Memory Leak. Weder das Database- noch das View-Objekt werden nach Ablauf sämtlicher Threads recycelt. Dies lässt sich beheben, indem innerhalb der Tests ganz auf den Bezug von Database und View verzichtet wird und diese ähnlich wie die Session außerhalb an einer (und nur einer) zentralen Stelle erzeugt und wieder freigegeben werden. Wenn Sie die Anwendung starten, werden Sie feststellen, dass der Test selbst bei hoher Last (z.B. 1000 Threads) noch sehr schnell durchläuft, obwohl immerhin jeder Test ein Dokument erzeugt, die gesamte Datenbank durchsuchen und alle Trefferdokumente speichern muss.
522
13 Objekt- und Speichertechniken
Die Bündelung von vielen Anfragen in einer Session über mehrere Threads funktioniert sehr gut. Diese Technik kann auch angewendet werden, wenn serielle Operationen parallelisiert werden sollen. Dabei muss beachtet werden, dass eine Session nicht durch zu viele parallele Operationen überladen wird. Fazit: Die Verteilung von mehreren Anfragen über mehrere Threads bei gemeinsamer Nutzung bietet gute Möglichkeiten, Ressourcen zu schonen – so wird z.B. bei DIIOP-Verbindungen nicht für jeden Thread ein neuer Socket benötigt, da sich alle einen Socket der Session teilen. Gleichzeitig birgt dies die Gefahr, durch Recyclingfehler Memory Leaks zu erzeugen.
13.3.8
Multithreading ohne gemeinsame Session
Um das Verhalten bei gemeinsamer und ohne gemeinsame Session vergleichen zu können, wurden dem Code zum Buch die beiden Klassen DJStressRunner und DJStressTest hinzugefügt. Sie erreichen durch eine kleine Abwandlung der Klassen DJStressRunnerOneSession und DJStressTestExternalSession, dass für jeden einzelnen Test eine neue Session aufgebaut wird. Anstatt in DJStressRunner eine gemeinsame Session zu erzeugen und an den Test zu übergeben, wird einfach die Erzeugung der Session in die Testklasse DJStressTest verlagert. Führt man diese Anwendung aus, so stellt man fest, dass die Performance hierunter erheblich leidet. Schließlich muss für jeden Thread eine neue Session initialisiert werden. Wird DIIOP genutzt, so kann man sehr gut über netstat -a (DOS-Konsole) beobachten, wie für jede Session ein TCP Socket eröffnet werden muss. Aus der Sicht eines Lasttests ist diese Herangehensweise allerdings in der Regel die sinnvollere, insbesondere wenn der Test via DIIOP auf eine entfernte Maschine angesetzt wird. In einer produktiven Umgebung werden ja viele (einzelne) Clients sich mit jeweils einer eigenen Session mit dem Server verbinden wollen. Die Unterschiede im Code beschränken sich ansonsten neben dem Verschieben des createSession-Aufrufes in den Test auf das unterschiedliche Recycling. Hat der Thread eine eigene Session, muss er sich auch selbst um das Recycling kümmern. Die in Listing 13-12 auskommentierten Recycle-Aufrufe für Database und View müssen hier wieder aktiviert werden.
13.4 Domino und DB2
13.4
523
Domino und DB2
Die Datenstruktur von Domino basiert auf dem bereits vielseitig diskutierten Modell loser Schemata, das im Wesentlichen durch das Document und seine Items repräsentiert wird. Offenkundig handelt es sich hierbei nicht um einen relationalen Datenbankansatz, auch wenn sich solche Strukturen in Domino abbilden lassen (s. Kap. 7.3.1). Vergleicht man Domino mit relationalen Datenbanken, steht die nachrichtenund objektzentrierte Welt verteilter Systeme von Domino der strukturierten Welt strikter Schemata und transaktionszentrierter SQL-Datenbanken gegenüber. Schon sehr früh wurde nach Konzepten gesucht, relationale (SQL-basierte) Datenbanksysteme mit Domino zu verbinden, um die Vorteile der beiden Welten miteinander zu kombinieren. Aus diesen Überlegungen entstanden verschiedene Konnektoren, wie DECS84 oder LEI85. Für Domino Version 7 hat nun Lotus den bisher radikalsten und dadurch auch nahtlosesten Ansatz gewählt. Domino-Datenbanken können anstatt in einem NSF File in einer DB2-RDBMS-Datenbank gespeichert werden (NSFDB2).
13.4.1
Einführung
Die Speicherung von Domino-Daten in einer relationalen Datenbank wie DB2 hat weitreichende Folgen und eröffnet bisher ungeahnte Möglichkeiten. Gleichzeitig bedeutet dies an vielen Stellen Veränderungen, die bei der Anwendungsentwicklung und der Administration berücksichtigt werden müssen. Neben der Einrichtung der Verbindungen zwischen Domino und DB2, der Verwaltung der DB2-basierten Domino-Datenbanken im Domino Administrator, müssen sich nun Domino Administratoren und teilweise auch Anwendungsentwickler mit der Administration und den Berechtigungskonzepten von DB2 auseinandersetzen. Vor allem aus Sicht von Java öffnet der Schritt in Richtung RDBMS Perspektiven, an die bisher nicht zu denken war. Die hieraus resultierenden Veränderungen sind aber nicht nur technischer Art, sondern vor allem in den Zielsetzungen und informellen Absichten von Anwendungen zu suchen. Allerdings sollte nicht vergessen werden, dass die DB2-Speicherung von Domino-Daten in einer relationalen Datenbank keineswegs ein Paradigmenwechsel ist, sondern lediglich neue Möglichkeiten eröffnen will. Um die Verarbeitung und Verwendung dieser neuen Technik verstehen zu können, sollen zunächst noch einmal die verschiedenen Perspektiven von Domino und relationalen Datenbanken untersucht werden. Domino basiert auf losen Schemata (dem Document) und zeichnet sich aus durch: 84 85
Domino Enterprise Connection Services – Dieser Ansatz ermöglicht die Definition von Datenverbindungen zu externen Datenquellen, die live abgefragt werden können. Lotus Enterprice Integrator – LEI erweitert die Funktionen von DECS dahingehend, dass Daten physisch zwischen Domino und externen Datenquellen synchronisiert werden können. Zusätzlich stehen Schnittstellen zu Lotus Script zur Verfügung.
524
• • • • •
• • • •
13 Objekt- und Speichertechniken
Zentrierung auf Kommunikation, Teamworkanwendungen, Workflow und Strategie. Unterstützung von mobilen Nutzern und Offline-Arbeitern ohne aktive Verbindung zu einem Server. Flexible Datenstrukturen auf Basis loser Schemata und entsprechend hochflexible Datentypen. Hieraus resultierende Konzentration auf Workflow, Kollaboration, Data Mining und Knowledge Management. Weitreichende Unterstützung von eingebetteten Objekten, Multimediaelementen, RichText, Dokument-Verlinkungen und Binärdaten, die alle in der einheitlichen Datenstruktur des Document gespeichert werden können. Berechtigung und Verschlüsselung bis auf Basis von Dokument und Feld. Ausgereifte Volltextindizierung. Dezentrale Verarbeitung in inhomogenen, verteilten Umgebungen. Buttom-up-Entwicklung.
Während sich also Domino auf die Verarbeitung unstrukturierter Daten beliebigen Datentyps (RichText, Multimedia, Binärdaten) für Knowledgemanagement, Teamarbeit und Workflow konzentriert, bedienen RDBMS-Datenbanken große Mengen strukturierter, streng typisierter Daten, die im Regelfall in einer nicht redundanten Normalform gespeichert werden: • • • • • • • •
Konzentration auf die effektive, operationale Verarbeitung großer Datenmengen. Transaktionszentriert. Verschiedene Commit-Strategien, wie Atomic Commit und Two Phase Commit. Strikte Einhaltung von Schemata und klar vorstrukturierten Datentypen. Redundanzfrei. Zentrale Datenhaltung für 2- oder 3-Trier-Anwendungen. Systemzentrierte Top-Down-Entwicklung. Konzentration auf ständig mit dem Server verbundene Online-Arbeitsplätze.
Die Schwerpunkte beider Systeme sind zum Teil so unterschiedlich und gegensätzlich, dass die Koexistenz beider Systeme seine Berechtigung findet. Lotus' Ansatz, die Domino-Daten in der RDBMS-Umgebung DB2 vorzuhalten und über verschiedene Techniken einerseits Domino-Daten in DB2 durch die so genannten DAV (DB2 Access View) und andererseits SQL-Daten in Domino (Query View) zur Verfügung zu stellen, ist ambitioniert und wird Vorteile für beide Systeme haben. DB2-Datenbankanwendungen werden insbesondere von der Möglichkeit, Daten offline und in verteilten Umgebungen einzusetzen (Replikation), profitieren. So ist zum Beispiel denkbar, dass für eine Anwendung Roadwarrior im Außendienst über eine Domino-Anwendung Daten erfassen, die später in einer RDBMS-Anwendung benötigt werden. Leicht werden diese Daten über Replikation und NSFDB2 in die DB2-Datenbank gelangen. Gleichzeitig profitiert DB2 von der Eigenschaft Dominos,
13.4.2 Struktur
525
Daten per Datensatz mit Benutzerrechten ausstatten zu können. Domino-Daten unterliegen auch in ihrer DB2-Repräsentation dem kompletten Berechtigungssystem von Domino. Domino-Datenbanken werden insbesondere von der Möglichkeit von RDBMSSystemen profitieren, Daten relational miteinander zu verknüpfen. Dies hebt für Domino die Grenze zwischen Domino-Datenbanken auf, da durch das NSFDB2-Format und die Verwendung einfacher SQL-Joins Daten verschiedener NSF-Files verknüpft angezeigt werden könnnen. In der Kombination von DAV und QueryView eröffnet sich die Möglichkeit, in Domino-Anwendungen Domino-Daten in einem QueryView über SQL zu selektieren. Der Vorteil liegt hierbei in der Möglichkeit, den SQL Select dynamisch, z.B. über Benutzereingaben, zu generieren.
13.4.2
Struktur
Auch wenn der Schritt von Lotus, die Speicherung von Domino-Daten in DB2 zu ermöglichen, im Hintergrund sicherlich enorme Veränderungen nach sich gezogen hat, ist aus Sicht von Domino an der Oberfläche der größte Teil dieser Veränderungen nahezu transparent. Letztendlich lässt sich NSFDB2 im Wesentlichen durch drei Neuerungen beschreiben: Das NSFDB2-Dateiformat NSFDB2 ist der Name für das neue Datenformat, in dem Domino-Daten in DB2 gespeichert werden. Wird eine Domino-Datenbank in DB2 gespeichert, hat dies aus Sicht einer Domino-Anwendung (zunächst) keine Änderung zur Folge. Das neue Speicherformat ist transparent. Auch ist mit der reinen Änderung der Datenhaltung noch keine unmittelbare Erweiterung von Funktionalitäten verbunden. Allerdings können NSFDB2-Datenbanken nur über einen Domino-Server erreicht werden. Lokal (Abb. 13-4) kommt nur das (alte) NSF-Format zum Einsatz. Verschiedene Repliken einer Datenbank können sowohl als NSFDB2 als auch als NSF in gemischten Umgebungen vorliegen, wobei zwischen den verschiedenen Formaten wie gewohnt repliziert werden kann. Das Format kann zwischen den beiden Möglichkeiten gewechselt werden.
Abb. 13-4 Lokaler Zugriff auf eine NSFDB2-Datenbank ist nicht möglich
526
13 Objekt- und Speichertechniken Domino-Daten im NSFDB2-Format sind auch aus Sicht von DB2 zunächst nicht erreichbar, denn die eigentliche Datenhaltung erfolgt in eigens hierfür entwickelten (verteilten) Tabellen mit Datenhaltungs- und Verwaltungsaufgaben. Um Domino-Daten über DB2 abzufragen, müssen DB2 Access Views (DAV) über den Domino Designer eingerichtet werden.
DAV – DB2 Access View Sollen Domino-Daten für DB2 verfügbar sein, muss ein DB2 Acccess View (kurz „DAV“) eingerichtet werden. Ein DAV wird über den Domino Designer eingerichtet, ist aber aus der Domino-Perspektive eher als ein Konfigurationsdokument als ein Designelement einzustufen. DAVs wiederum sind – auch wenn sie über den Domino Designer eingerichtet werden – nur für die Benutzung in DB2 bestimmt und können aus Domino nicht direkt referenziert werden (siehe jedoch „QueryView“). Wird ein DAV eingerichtet, so wird im Domino Designer festgelegt, welche Dokumente und Items dieser Dokumente in DB2 sichtbar sein sollen. Zur Zeit ist eine Spezifikation der Dokumente nur auf Basis ihrer Defaultmaske (also über das Item mit Namen Form) möglich. Items können beliebig angegeben werden. Dies können sowohl Items sein, die als Feld in der zugehörigen Maske definiert sind, als auch Items, die auf anderen Wegen in das Dokument gelangt sind. Liegen Items, die im DAV definiert wurden, in einem Dokument nicht vor, bleiben die korrespondierenden Werte in der DB2-Tabelle leer. Einzige Einschränkung hierbei ist die Liste der möglichen Datentypen, die für die Verwendung in DB2 freigegeben werden können. Dies sind Text, Textlist, Number, Numberlist, Date/ Time und Date/Time List or Range, wobei z.B. Namensfelder zumindest als Text (VARCHAR) in DB2 zur Verfügung gestellt werden können. RichText kann (zur Zeit?) nicht für den Zugriff über DB2 freigegeben werden. Ist ein DAV eingerichtet, muss über entsprechende Funktionsbuttons die Datenbank in DB2 upgedatet werden. Dieser Prozess ist vordefiniert und muss lediglich ausgelöst werden. Nach Erstellen eines DAV werden die Dominodaten redundant in einer DB2-Tabelle (<>_T) gespeichert und über eine DB2-Ansicht (<>) bereitgestellt. Die Tabelle selbst bleibt für den Benutzer, der den DAV im Designer eingerichtet hat, gesperrt, da nur in der zugehörigen Ansicht die Domino-ACL-Sicherheit für DB2 durchgesetzt wird und daher immer auch nur diese Ansicht in Anwendungen verwendet werden soll. Ein DAV ermöglicht also die zeilenbezogene ACL-Sicherheit in einer DB2-Datenbank! Eine derart über ein DAV erzeugte Ansicht ist wie jede andere SQL-Tabelle in DB2 verwendbar und kann auch in der Kombination mit anderen Tabellen oder Ansichten eingesetzt werden.
13.4.2 Struktur
527
Die in einer DAV-Ansicht gezeigten Daten unterliegen der Domino-Sicherheit. Hierfür werden im Domino-Adressbuch (names.nsf) Mappings zwischen den DB2- und den Domino-Benutzern hinterlegt. Ein DB2-Benutzer darf in einer DAV-Ansicht nur die Daten lesen oder bearbeiten, die der zugeordnete DominoBenutzer lesen oder bearbeiten darf. Hierin liegt ein großer Vorteil, aber auch eine gewisse Schwierigkeit, da administrativ dafür gesorgt werden muss, dass die Mappings für alle beteiligten Benutzer richtig eingerichtet sind. Damit DB2 auf DAV-Ansichten aus Domino zugreifen kann, muss auf dem Domino-Server zusätzlich der so genannte DB2 Access Server installiert sein. Dies ist ein modifizierter Domino-Server, der für die Kommunikation zwischen beiden Systemen und insbesondere die Durchsetzung von Domino-Rechten in Richtung DB2 zuständig ist. Query View Ein Query View ermöglicht nun den umgekehrten Weg. Ein Query View ist eine normale Domino-Ansicht, deren SELECT-Formel86 nicht wie gewohnt über eine @Formel bestimmt wird, sondern über einen SQL-Ausdruck. Da grundsätzlich für den Domino-Server festgelegt ist, welche DB2-Datenbank die Domino-Daten hält, können in dieser SQL-Formel Datenbanken und Ansichten einfach über das Schema und den Tabellen- oder Ansichtenname eindeutig angesprochen werden. Im Umkehrschluss bedeutet dies allerdings, dass (zunächst) nur Daten angesprochen werden können, die sich in der dem Domino-Server zugeordneten DB2Datenbank befinden. Über die Einrichtung von DRDA-Datasource-Verbindungen lässt sich diese Hürde jedoch überbrücken. Aus Sicherheitsgründen sind in der SQL-SELECT-Formel nur Anweisungen zulässig, die ein Resultset zurückgeben. Die durch das SQL Statement selektierten Spalten der DB2-Tabellen oder -Ansichten können in den Spaltenformeln der Domino-Ansichten wie Items über den Namen referenziert werden. Handelt es sich bei den SQL-Daten um originäre SQL-Daten (nicht um einen DAV View), so werden die Daten nur in der Domino Query View angezeigt. Ein Doppelklick bewirkt kein Öffnen eines Domino-Dokuments, da eine derartige Zuordnung ja auch nicht vorliegt. Bei derartigen „originären“ DB2-Daten wird auch von „federated data“ gesprochen. Sollen in einer Domino-Datenbank im NSFDB2-Format nur federated data angesprochen werden, ist es nicht notwendig den DB2 Access Server zu installieren. Interessant wird es, wenn über einen Query View Domino-Daten referenziert werden, die zuvor als DAV dargestellt wurden. Im Query View können nämlich nun Domino-Daten über eine SQL-Formel ausgewählt und natürlich über einen SQL-JOIN verbunden werden. Dies gilt natürlich auch für DAV-Ansichten in DB2, die verschiedenen (!) Domino-Datenbanken entstammen. So wird es ermöglicht, datenbankübergreifend Domino-Daten miteinander zu verbinden und in einem Query View anzuzeigen. 86
Siehe auch Kapitel 3.3.3
528
13 Objekt- und Speichertechniken DAV-Ansichten enthalten eine Spalte #NOTEID, in der die NoteID der zugehörigen Dokumente gespeichert wird. Über diese wird es möglich, dass Dokumente eines DAV nicht nur im Query View angezeigt, sondern auch durch Doppelklick geöffnet werden können. Allerdings ist dabei darauf zu achten, dass nur solche Dokumente geöffnet werden können, die zu der Domino-Datenbank gehören, in der sich der Query View befindet. Zusätzlich ist erforderlich, dass im SQL Statement die Spalte #NOTEID ausdrücklich geladen wird. Ein weiterer interessanter Aspekt ergibt sich dadurch, dass die SQL-Anweisung, die für einen Query View definiert wird, nicht statisch angegeben werden muss, sondern als Ergebnis einer @Formel berechnet wird. Folglich kann die SQL-Anweisung dynamisch zur Laufzeit berechnet werden und kann daher zum Beispiel abhängig von Benutzereingaben definiert werden.
Was NSFDB2 leistet: • • • • •
• • •
Schaffung eines zentralen Datastores durch Speicherung von Domino-Daten in DB2. Überbrückung der Schranken zwischen Domino und RDBMS-Daten. Query Views stellen DB2-Daten in Domino zur Verfügung. DB2 Access Views stellen Domino-Daten in DB2 zur Verfügung. Dynamische Views – Live Selection von DB2- oder Domino-Daten auch via Benutzerinteraktion durch Verwendung von @Prompt bei der Berechnung der SQL SELECT-Anweisung. Datenbankübergreifende Verknüpfung von Domino-Daten. Verknüpfung von Unternehmensdaten aus DB2 mit Domino-Daten. Zeilenbasierte (Datensatzbasierte) Domino-ACL-Sicherheit in DB2 (für DAV Views).
Einschränkungen und Besonderheiten • • •
Query Views können (noch?) nicht im Browser angezeigt werden. Query Views werden nicht automatisch refreshed, sondern müssen ausdrücklich neu geladen werden (F9 im Notes Client oder view.refresh()). Jedem Domino-Benutzer, der einen Query View anzeigen soll, muss ein DB2-Benutzer zugeordnet sein. Dies ist insbesondere eine wichtige Voraussetzung für Notes Client-Anwendungen. Java-Anwendungen und -Agenten werden in der Regel ohnehin unter einem (einzelnen) Funktionsuser betrieben und sind im Normalfall hiervon nicht betroffen.
13.4.3 Sicherheit, Benutzer und Gruppen •
• •
529
Werden in Query Views federated data angezeigt und diese Daten mit Daten aus Dokumenten der aktuellen Datenbank verknüpft, können diese Dokumente der aktuellen Datenbank geöffnet werden. Dokumente entfernter Datenbanken können über einen Query View (in Kombination mit einem DAV) angezeigt, nicht aber geöffnet werden. DB2 Access Views können keine RichText-Inhalte verarbeiten. DB2-basierte Datenbanken können wie normale Datenbanken repliziert werden. Es kann auch zwischen den beiden Formaten NSFDB2 und NSF repliziert werden. Wird eine NSFDB2-basierte Datenbank mit einer NSF-Datenbank repliziert, bleiben die Designelemente, die das DB2-Format voraussetzen, unsichtbar bzw. nicht verwendbar. Lokale Repliken haben immer das klassische NSF-Format.
NSFDB2 ist eine neue Funktion für Domino R7. Daher soll in den beiden folgenden Kapiteln auf die beiden administrativen Themen Sicherheit und Setup der DB2-Anbindung eingegangen werden, auch wenn dies nur indirekt für die Java-Entwicklung unter Domino erforderlich ist.
13.4.3
Sicherheit, Benutzer und Gruppen
Für das Verständnis der Zusammenarbeit von Domino und DB2 ist das grundlegende Verständnis der Berechtigungen und der involvierten Benutzer und Gruppen bei der Zusammenarbeit beider Systeme von großer Hilfe. Da dieses Wissen insbesondere beim Setup erforderlich ist, ist dieses Kapitel einführend dem Setup vorangestellt. Zunächst ist zu beachten, dass einige beschriebene Aktionen erst ausgeführt werden können, wenn der Domino-Server für die Benutzung mit DB2 aktiviert ist. Die Erläuterung bereits an dieser Stelle wird das spätere Verständnis erleichtern. •
• •
•
Jedem Domino-Benutzer, der auf DB2-Daten zugreifen soll, muss ein DB2-Benutzer zugeordnet werden. Diese Zuordnung wird im Domino Administrator vorgenommen, indem im Menü „People“ -> „People“ -> „Set DB2 Username“ der zugehörige Benutzer eingetragen wird. Es können hier nur Benutzer eingetragen werden, die im Betriebssystem als Benutzer zur Verfügung stehen, da diese (und nur diese) gleichzeitig als Benutzer für DB2 verwendet werden (können) – s. Abb. 13-5. Die Zuordnung muss ein-eindeutig sein, d.h. es kann für jeden Domino-Benutzer nur genau einen zugeordneten DB2-Benutzer geben und jeder DB2-Benutzer darf nur genau einmal als zugeordnet eingetragen werden. DB2-Benutzer sind Betriebssystem-Benutzer und werden als solche angelegt. DB2 Access Views unterliegen den ACL-Berechtigungen von Domino. DB2 erkennt die Rechte, indem anhand des Domino-DB2-Benutzermappings der dem aktuellen DB2-Nutzer zugeordnete Domino-Benutzer ausgewählt wird. Query Views unterliegen den in DB2 eingerichteten Rechten des gemappten DB2-Benutzers.
530
13 Objekt- und Speichertechniken
Abb. 13-5 Mapping zwischen Domino und DB2-Benutzern
•
•
Sämtliche Rechte auf DB2-Strukturen müssen über die DB2-Tools eingerichtet werden – am besten über Gruppen in DB2. Dies gilt nicht für DB2 Access Views, deren Rechte beim Anlegen im Domino Designer korrekt gesetzt werden und da dort ohnehin die Domino-ACL gilt. Jeder lokale Administrator (unter Windows) ist gleichzeitig Datenbankadministrator auf einer DB2-Instanz auf derselben Maschine. Diese Berechtigung wird implizit gewährt und muss nicht ausdrücklich eingerichtet werden. Wird also auf einer Entwicklungsmaschine mit administrativen Accounts gearbeitet, ist dafür Sorge zu tragen, dass die DB2-Anwendung auch für „normale“ Benutzer geeignet ist, indem die entsprechenden Berechtigungen in der DB2-Steuerzentrale eingerichtet werden und die Anwendung mit entsprechenden Benutzeraccounts getestet wird.
Aus Sicht von Domino werden verschiedene Domino-Benutzer und deren zugehörige User IDs benötigt, um eine Kommunikation mit DB2 zu ermöglichen: Regulärer Domino-Benutzer / Domino-Benutzer-ID Jeder reguläre Benutzer, der Domino und die neuen NSFDB2-Funktionen nutzen soll, muss wie gewohnt über eine Domino-Benutzer-ID verfügen. Alternativ genügt auch ein so genannter Web User Account. Dies ist ein Benutzer, der nicht über den Domino Administrator und die dort verfügbaren Registrationswerkzeuge angelegt wird, sondern der lediglich im Domino-Adressbuch ein Benutzerdokument mit Benutzername und Internetkennwort besitzt. Ein derartiger Benutzer kann wie gewohnt über die ACL berechtigt werden und ist für den Zugriff über eine Internet-Session (über DIIOP – s. Kap. 5.3ff.) geeignet.
13.4.3 Sicherheit, Benutzer und Gruppen
531
Ein regulärer Benutzer, der auf DB2 zugreifen soll, benötigt ein DB2-User-Mapping. Dieses kann über den Domino Administrator eingerichtet werden (s. Abb. 13-5). Auch ein Web User benötigt ein DB2-User-Mapping. Die Rechte, die nun dieser gemappte Benutzer (und somit der eigentliche Domino-Benutzer) auf DB2 erhält, wird über die administrativen Möglichkeiten von DB2 (z.B. in der Steuerzentrale – s. Abb. 13-6 – oder über entsprechende SQL
Abb. 13-6 DB2-Benutzer mit Rechten ausstatten GRANT-Befehle) geregelt, wobei auch in DB2 der gemappte Benutzer angelegt werden muss (s. Abb. 13-7). Am besten wird eine Benutzergruppe (z.B. DB2Users – s.u.) auf der während des DB2-Enablements speziell für Domino eingerichteten DB2-Datenbank „DOMINO“ mit den gewünschten DB2-Rechten ausgestattet. Das Domino-DB2-Benutzermapping funktioniert übrigens in beide Richtungen. Versucht ein DB2-Benutzer auf einen DB2 Access View zuzugreifen, werden zunächst anhand des Mappings und des dort vorgefundenen Domino-Benutzers seine Domino-Rechte geprüft und durchgesetzt. Hierin liegt begründet, dass die Mappings ein-eindeutig sein müssen. Regulärer Domino-Server / Domino-Server-ID Jeder Domino-Server benötigt eine reguläre Server-ID. Diese ID regelt die Berechtigungen des Servers und wird bei der Registrierung eines Servers angelegt. Diese Server-ID wird immer in einer herkömmlichen Domino-Infrastruktur verwendet.
532
13 Objekt- und Speichertechniken
Abb. 13-7 Neuen Benutzer in DB2 anlegen
Während des DB2-Enablements des Domino-Servers (s.u.) wird nun in dieser Domino-Server-ID gespeichert, mit welchem Benutzernamen und Passwort der Domino-Server sich mit DB2 verbinden soll. Über den Domino Administrator kann über den Befehl „Edit DB2 Login Information“ dieses Login verändert werden (s. Abb. 13-8). Domino Access Server / Domino Access Server-ID Für die Verwendung der DB2 Access Views wird zusätzlich zu einer StandardDomino-Installation die Installation des so genannten Domino Access Servers benötigt, der die Domino-Berechtigungen im DAV durchsetzt.
13.4.3 Sicherheit, Benutzer und Gruppen
533
Abb. 13-8 DB2-Super-User – Anmeldung des Domino-Servers an DB2
Um diese Installation durchführen zu können, muss vor (!) der Installation des Domino Access Servers über den Server-Registrationsprozess im Domino Administrator ein zusätzlicher Server registriert werden. Hierbei wird die Server-ID für den Domino Access Server generiert, wobei kein Passwort für die Server-ID vergeben werden darf. Diese ID wird dann vom Domino Access Server verwendet. Die „normale“ Server-ID des eigentlichen Domino-Servers, der mit DB2 verbunden werden soll, darf hierfür nicht verwendet werden. Auf Betriebssystemebene (unter Windows) gibt es folgende Gruppen, die im Zusammenhang mit Domino und DB2 interessant sind: DB2Users In der Gruppe DB2Users sollen sich alle „regulären“ DB2-Benutzer befinden, die nicht-administrativen Zugriff auf DB2 erhalten sollen. Werden Domino-Benutzer mit DB2-Benutzern gemappt, müssen die gemappten DB2-User in diese Gruppe eingetragen werden. Alternativ können sich reguläre DB2-Benutzer auch in der Standard-Benutzergruppe des Betriebssystems („Users“ oder „Benutzer“) befinden.
534
13 Objekt- und Speichertechniken
DB2Admns In der Gruppe DB2Admins befinden sich die Administratoren der lokalen DB2Instanz. Die DB2-Administratorengruppe muss auf diese Gruppe verweisen87. Bei der Installation von DB2 wird ein so genannter Instanzeigner festgelegt, der als Ausführender der DB2-Prozesse fungiert. Dieser heißt in der Regel DB2admin (unter Windows und db2inst1..n unter Unix), muss sich in der Administratorengruppe des Betriebssystems befinden und zusätzliche Rechte (s.u.) erhalten. Er sollte ebenfalls in dieser Gruppe eingetragen sein. DB2Dom In der Gruppe DB2Dom befindet sich der „Domino Super User“ (s.o. DominoServer-ID), über den die Kommunikation zwischen Domino und DB2 abgewickelt wird. Diese Betriebssystem-Gruppe muss der DB2 SYSCTRL-Gruppe zugeordnet werden88. Fazit: Die Kommunikation zwischen Domino und DB2 wird über einen Super User geregelt. Dies ist ein Betriebssystem-User, der sich in der Gruppe DB2DOM befindet und dessen Anmeldeinformationen in der Domino-Server-ID gespeichert werden. Die Kommunikation in der umgekehrten Richtung zwischen DB2 und Domino wird über den Domino Access Server gesteuert, der separat installiert und zuvor registriert werden muss, wobei eine eigene Domino Access Server-ID erzeugt wird. Möchten Domino-Benutzer über Query Views auf DB2-Daten oder umgekehrt DB2-User über DB2 Access Views auf Domino-Daten zugreifen, muss es ein eindeutiges Mapping zwischen einem Domino-User und einem DB2-User geben, anhand dessen die jeweiligen Rechte ermittelt werden. Domino-User werden in Domino wie gewohnt mit Berechtigungen versehen, DB2-User sind System-User und werden in DB2 mit Rechten versehen. Für Domino-Anwender ist es etwas ungewohnt, dass DB2-User identisch mit Betriebssystem-Usern sind. Insbesondere hat dies zur Folge, dass sich die DB2-Rechte aus der aktuellen Anmeldung am Betriebssystem ergeben. Wird also z.B. die DB2Steuerzentrale mit einem angemeldeten Betriebssystem-User gestartet und versucht, auf einen DAV zuzugreifen, wird versucht über das Mapping einen Domino-Benutzer zu ermitteln, der auf diesen Betriebssystem (also DB2)-Benutzer gemapped ist. Unter Umständen, kann es also erforderlich sein, sich neu am Betriebssystem anzumelden, bzw. über „runas“89 die Steuerzentrale als anderer Benutzer zu starten. 87
88 89
In der DB2-Befehlskonsole kann der Parameter SYSADM_GROUP gesetzt werden, um dies zu erreichen. Mit dem Befehl get dbm cfg kann der Parameter ausgelesen und mit dem Befehl update dbm cfg using SYSADM_GROUP DB2Admns gesetzt werden. Die SYSCTRL Group kann ebenso über update dbm cfg using SYSCTRL_GROUP DB2DOM gesetzt werden. Um z.B. die DB2-Steuerzentrale mit den Rechten des Benutzers DB2ACC zu starten, verwenden Sie unter Windows den Befehl „runas /user:DB2ACC C:\Programme\IBM\ SQLLIB\BIN\db2cc.bat“. Dieser kann zur Vereinfachung auch in einer Verknüpfung verwendet werden.
13.4.4 Setup 13.4.4
535
Setup
Damit das Zusammenspiel zwischen DB2 und Domino reibungslos funktioniert, müssen bei der Installation von Domino, DB2 und DB2 Access Server bestimmte Regeln und Reihenfolgen eingehalten werden. Zur Installation (unter Windows) sind folgende Schritte notwendig: • • • • • • • • • •
Installation von Domino R7. Nachbereitung der Installation von Domino R7 – Aktivierung des Transaktionsloggings. Vorbereitung von Benutzern für die Installation und den Betrieb von DB2. Installation von DB2. Optional: Registrieren einer Domino-Server-ID für den DB2 Access Server. Optional: Installation des DB2 Access Servers. Enablement des Domino-Servers für die Verwendung mit DB2 durch das DB2Enablement-Tool im Domino Administrator. Nachbereitung der Installation von DB2 – Update der DOMINO-DB2-Datenbanken. Neustart und Testen der Verbindung. Mapping eines Domino-Benutzers auf einen DB2-Benutzer.
Installation von Domino R7 Der Domino R7 Server wird für die Vorbereitung für DB2 wie ein normaler DominoServer installiert. Ein vorhandener älterer Domino-Server kann durch Über-Installieren upgedated werden, wobei entsprechende Backups nicht vergessen werden sollten. Um die folgenden Schritte für die Konfiguration vornehmen zu können, wird ein Benutzer mit Server-Administrationsrechten für den Domino-Server benötigt. Nachbereitung der Installation von Domino R7 Damit Domino mit einer DB2-Datenbank zusammenarbeiten und das NSFDB2-Format aktiviert werden kann, muss das Transaktionslogging für Domino aktiviert werden. Dies ermöglicht es Domino im Falle eines Serverausfalls, die letzten Änderungen aus dem Transaktionslog zu rekonstruieren und so für eventuell beschädigte Datenbanken wieder einen konsistenten Zustand herzustellen. Vor der Aktivierung muss sichergestellt sein, dass alle Datenbanken, für die das Transaktionslog geschrieben werden soll, sich im Domino-Data-Directory befinden. Die Parameter können für eine Testumgebung entsprechend Abb. 13-9 eingestellt werden. Nach der Anpassung muss der Domino-Server neu gestartet werden.
536
13 Objekt- und Speichertechniken
Abb. 13-9 Domino-Transaktionsprotokollierung
Vorbereitung von Benutzern für die Installation und den Betrieb von DB2 Um DB2 korrekt (auf einer Windows-Maschine) installieren zu können, werden bestimmte Benutzerrechte während der Installation und ein ebenfalls mit speziellen Rechten ausgestatteter Benutzer für den Betrieb von DB2 benötigt. Am einfachsten ist es, einen Benutzer für beide Zwecke vor der Installation zu erstellen: • • •
Standardmäßig sollte der Benutzer db2admin genannt werden. Der Benutzer muss mit lokalen Administrationsrechten ausgestattet sein (er sollte sich in der lokalen Gruppe Administratoren befinden). Soll der Benutzer nicht lokal, sondern über ein Active Directory angemeldet werden, so muss er während des Installationsvorgangs entsprechende Domänenadministrationsrechte erhalten oder für die Installation ein separater Benutzer mit diesen Rechten verwendet werden.
13.4.4 Setup •
537
Der Benutzer muss folgende zusätzliche Rechte90 besitzen: – – – – –
Als Dienst anmelden (Log on as a service) Anpassen von Speicherkontingenten für einen Prozess (Increase quotas) Einsetzen als Teil des Betriebssystems (Act as part of the operating system) Ersetzen eines Tokens auf Prozessebene (Replace a process level token) Erstellen eines Tokenobjekts (Create a token object)
Installation von DB2 DB2 wird mit dem zuvor erstellten Benutzer installiert. Hierfür ist es erforderlich, sich zuvor als dieser Benutzer am Betriebssystem anzumelden. Das DB2-Installationsprogramm91 ist ein komfortabler Wizard, für den nur wenige Einstellungen vorgenommen werden müssen. DB2 sollte als alleinstehender, nicht partitionierter Server mit einer Instanz installiert werden. Hierfür werden die Standardvorgaben bei der Installation gewählt. Der während der Installation vergebene Instanzname muss notiert werden, da er für die spätere Einrichtung des Domino-Servers benötigt wird. Während der Installation kann eine DB2-Benachrichtigungsliste eingerichtet werden. Hierfür wird ein SMTP-Server benötigt, wobei Sie die TCP-Adresse des Domino-Servers verwenden können, sofern der SMTP Task für Domino aktiviert ist. Als DB2-Funktionsuser und Administrator wird der zuvor erstellte Benutzer db2admin eingesetzt. Soll der DB2-Server nicht auf derselben Maschine wie Domino, sondern auf einer entfernten Maschine installiert werden, muss auf der Maschine des Domino-Servers zusätzlich der DB2-Runtime-Client installiert werden, damit die Verbindung zwischen Domino und und dem entfernten DB2-Server hergestellt werden kann. Weitere Optionen werden während der Installation nicht benötigt. Optional: Registrieren einer Domino-Server-ID für den DB2 Access Server Sollen Domino-Daten in einem DB2 Access View für DB2 sichtbar gemacht werden, so wird der DB2 Access Server benötigt. Um diesen Server installieren zu können, wird zunächst im Domino Administrator eine neue Server-ID registriert.
90
91
Unter Windows muss der installierende und der DB2 ausführende Benutzer spezielle Benutzerrechte erhalten. Diese werden für Windows XP unter „Systemsteuerung->Verwaltung->Lokale Sicherheitsrichtlinie->Sicherheitseinstellungen->Zuweisen von Benutzerrechten“ eingestellt. Wird der Benutzer über ein Active Directory berechtigt, müssen dort die entsprechenden Rechte in der zugehörigen Policy eingestellt werden. Für die Installation unter AIX wird ein root login benötigt und es sind zusätzliche Schritte notwendig, die der Domino R7-Administrationshilfe entnommen werden können. Zum Stand von Domino R7 wird mindestens ein DB2-UDB-Server der Version 8.2.2 benötigt. Diese Version wird zum Teil auch als 8.1 Fixpack 9 bezeichnet. Die aktuell harmonierenden Versionen von Domino und DB2 müssen unbedingt beachtet werden und können den jeweils aktuellen Installationshinweisen und Systemvoraussetzungen entnommen werden.
538
13 Objekt- und Speichertechniken
Die Registrierung eines neuen Servers wird im Domino Administrator gestartet, indem der Domino-Server geöffnet und im Bereich „Configuration“ der Befehl „Registration“->„Server“ gewählt wird. Wichtig ist, dass die neue Server-ID kein Passwort erhält und die ID in einer Datei gespeichert wird, um für die DB2 Access Server-Installation zur Verfügung zu stehen (s. Abb. 13-10 und Abb. 13-11).
Abb. 13-10 DB2 Access Server registrieren
Optional: Installation des DB2 Access Servers Der DB2 Access Server wird auf der Maschine installiert, auf der sich der DB2-Datenbankserverbefindet. Er ist ein (reduzierter) Domino-Server mit einem Domino-Adressbuch, wobei dieses nur für Konfigurationszwecke verwendet wird. Versucht ein Benutzer auf einen DAV zuzugreifen, wird der DB2 Access Server nicht nur die ACL durchsetzen, sondern auch die Verbindung zum zugehörigen Domino-Server herstellen. Hierfür wird unter anderem ein so genanntes Verbindungsdokument im Adressbuch des Access Servers benötigt, das Informationen vorhält, wie
13.4.4 Setup
539
Abb. 13-11 DB2 Access Server: Eigenschaften
der Domino-Server erreicht werden kann. Wird der Access Server nach dem Enablements (s.u.) des Domino-Servers installiert, kann das Fehlen dieses Verbindungsdokument Ursache von Fehlern sein und muss nachträglich erstellt werden. Vor der Installation sollten DB2 und Domino angehalten werden. Der DB2 Access Server sollte unbedingt im FUNCTION92-Verzeichnis des DB2-Servers installiert werden. Ansonsten kann der Installer mit seinen Standardeinstellungen durchgeführt werden. Wird der DB2 Access Server installiert, nachdem der Domino-Server für die Verwendung von DB2 aktiviert (s.u.) wurde, so muss im Adressbuch des Domino-Servers in dessen Serverdokument im Abschnitt „DB2“ dafür gesorgt werden, dass dort die Informationen entsprechend dem DB2-Server eingestellt sind (s. Abb. 13-12). Zusätzlich muss in der Domino-Serverkonsole der Befehl DB2 ACCESS SET abgesetzt werden. Die Konfiguration kann mit dem Domino-Konsolenbefehl DB2 INFO abgefragt werden. 92
Unter Windows ist der Default für dieses Verzeichnis C:\Programme\IBM\SQLLIB\FUCTION. Dort wird bei der Installation eine Notes.ini Datei für den Access Server und ein Data-Verzeichnis erstellt.
Während der Installation muss der Speicherort der zuvor erzeugten Server-ID angegeben werden. Diese ID wird vom Installationsprozess im Dataverzeichnis des DB2 Access Servers (im FUNCTION-Verzeichnis von DB2) unter dem Namen „user.id“ abgelegt. Enablement des Domino-Servers für die Verwendung mit DB2 Für die Aktivierung der DB2-Fähigkeit von Domino müssen verschiedene Einstellungen gesetzt werden. Hierfür wurde ein Werkzeug vorbereitet, das über den Domino Administrator gestartet werden kann. Es wird über den Befehl „Enable Server for DB2“ in der Sektion „DB2 Server“ im Bereich „Configuration“ gestartet. Wurde der Server bereits aktiviert, ist dieser Befehl deaktiviert93. 93
Muss das Setup wiederholt werden, können die DB2spezifischen Parameter in der notes.ini-Datei des Domino-Servers entfernt und der Domino-Server neu gestartet werden. Dabei ist zu beachten, dass Domino versuchen wird, eine neue DB2-Datenbank zu erstellen. Es kann notwendig sein, entweder die vorhandene DB2-Datenbank zu löschen oder während des Enablement Prozesses einen anderen DB2-Datenbanknamen zu verwenden.
13.4.4 Setup
541
Für das Enablement wird ein lokaler Betriebssystem-Benutzer benötigt, der die Kommunikation mit DB2 übernimmt (siehe Domino-„Super-User“ in der Gruppe DB2DOM in Kapitel 13.4.3). Wird DB2 auf derselben Maschine installiert wie Domino, kann hierfür der DB2-Administrator „db2admin“ verwendet werden. Andernfalls wird ein mit denselben Rechten ausgestatteter Benutzer auf dem Domino-Server benötigt. Dieser Benutzer muss sich in der Gruppe DB2DOM befinden. Diese Gruppe muss als SYSCTRL_GROUP in DB2 registriert sein. Dies kann mit dem Befehl GET DBM CFG in der DB2-Befehlskonsole kontrolliert werden. Der Parameter SYSCTRL_ GROUP muss den Wert DB2DOM haben, bzw. kann über den Befehl UPDATE DBM CFG USING SYSCTRL_GROUP DB2DOM neu gesetzt werden, wobei dies einen Neustart des DB2-Datenbank Managers erfordert, was in der DB2-Konsole durch DB2STOP FORCE und DB2START erreicht werden kann. Beachten Sie, dass der Parameter force aktive Datenbanken anhält, auch wenn Benutzer verbunden sind. Vor dem Starten des Enablement-Prozesses müssen DB2 und Domino wieder gestartet und mit dem Domino Administrator der Domino-Server geöffnet werden. Nach dem Start des Enablement Prozesses werden verschiedene Parameter abgefragt, unter anderem der DB2-Instanzname, der während der Installation von DB2 angelegt wurde (s. Abb. 13-13)94. War der Enablement-Prozess erfolgreich, wird eine entsprechende positive Rückmeldung angezeigt und der Domino-Server muss neu gestartet werden. Nachbereitung der Installation von DB2 Während des Neustarts von Domino wird die DB2-Datenbank für Domino erstellt. Hierbei sollten eventuelle Fehlermeldungen in der Domino-Serverkonsole beachtet werden. In der DB2-Steuerzentrale kann nun kontrolliert werden, ob die Datenbank erfolgreich erstellt wurde. Nun muss der Domino-Server gestoppt und auch der Domino Administrator beendet werden. Um die neu erstellten Datenbanken auf den neuesten Stand der DB2-Installation zu bringen, muss in der DB2-Befehlskonsole (auch als CLP – Command Line Prozessor bezeichnet) ein Update-Befehl eingegeben und nachfolgend der DB2-Datenbankmanager durchgestartet werden: db2updv8 -d DOMINO -u db2admin -p db2adminPasswort db2stop force db2start
Hierbei ist „DOMINO“ der zuvor gewählte Name der DB2-Datenbank, db2admin der DB2-Administrator-Account (s.o.) und db2adminPasswort das zugehörige Passwort. Nun muss noch der Domino-Server und der Domino Administrator neu gestartet werden. Wurde der DB2 Access Server installiert, muss in der Dominokonsole der Befehl 94
Der Name des DB2 Access Servers sollte im Hostfile mit der IP des Servers verknüpft werden. Hierbei wird der Namensbestandteil (CN) verwendet. Alternativ kann der Name als CNAME im verwendeten DNS eingetragen werden oder anstelle des vollqualifizierten Notesnamens die IP des DB2 Access Servers verwendet werden.
542
13 Objekt- und Speichertechniken
Abb. 13-13 Domino-DB2-Enablement-Prozess
13.4.4 Setup
543
DB2 ACCESS SET
ausgeführt werden. Sicherheitshalber sollte mit der DB2-Befehlskonsole überprüft werden, ob die für Domino in DB2 angelegte Datenbank im UTF-8-Format angelegt wurde. get db config
sollte die Ausgabe Database code page = 1208 Database code set = UTF-8
liefern. Neustart und Testen der Verbindung Der Erfolg der Installation kann anhand der initialen Servermeldung „The map for DB2 errors was successfully created“ erkannt werden. Informationen über die DB2Verbindung liefert der Domino-Konsolenbefehl „DB2 INFO“. Für den Test des DB2 Access Servers wurde im Domino Administrator unter „Configurations“ in der rechten Befehlsleiste und „DB2 Server“ der Befehl „Test DB2 Access“ zur Verfügung gestellt (s. Abb. 13-14). Über den Befehl „Edit DB2 Access
Abb. 13-14 Test: DB2 Access
Connection können die Verbindungseinstellungen nochmals kontrolliert werden (s. Abb. 13-15). Domino-Datenbanken können nun im NSFDB2-Format angelegt werden. Dies erfolgt automatisch, wenn als Default-Format „DB2“ gewählt wurde (s. Abb. 13-13). Domino-Datenbanken im NSF-Format können im Domino Administrator mit dem Move-Befehl (s. Abb. 13-16) ins NSFDB2-Format überführt werden. Dieser steht im
544
13 Objekt- und Speichertechniken
Abb. 13-15 Test: DB2 Access Server-Verbindung
Bereich „Files“ in der rechten Befehlsleiste unter „Database“ zur Verfügung. Der Formatwechsel wird nachgelagert vom Administrationsprozess „adminp“ durchgeführt. Eventuelle Meldungen werden in der Domino-Serverkonsole ausgegeben.
Abb. 13-16 Formatwechsel zwischen NSF und NSFDB2
Mapping eines Domino-Benutzers auf einen DB2-Benutzer Bevor nun mit NSFDB2 gearbeitet werden kann, muss mindestens für einen Domino-Benutzer ein DB2-Username-Mapping eingerichtet werden.
13.4.5 DB2 Access View
545
Im Domino Administrator in der Ansicht „People and Groups“ wird der gewünschte Domino-Benutzer gewählt und der Befehl „Set DB2 User Name“ ausgeführt (s. Abb. 13-5). Sollten sich Schwierigkeiten mit dem Benutzermapping bei der Verwendung von DAVs ergeben, so kann in der Notes-INI-Variablen des DB2 Access Servers der Parameter DEBUG_DOMUDF_DIAG=C:\Programme\IBM\SQLLIB\FUNCTION\data\ diag.txt gesetzt werden95. Beim Mapping ist zu beachten, dass aus Sicht von Domino auch Benutzer auf den Server zugreifen können, die kein Personendokument im Domino-Adressbuch (names.nsf) besitzen. Solche Benutzer können sehr wohl im Serverdokument und in den ACLs der Datenbanken berechtigt werden. Selbst administrative Rechte (Serverdokument) sind denkbar. Für die Verwendung von NSFDB2 sind solche Benutzer aber ungeeignet, da ihnen kein Mapping zugeordnet werden kann. Fehler aufgrund fehlender Personendokumente können schwer erkannt werden, da in diesem Fall keine Meldungen in der Dominokonsole erscheinen. Abhilfe kann geschaffen werden, indem für solche Personen Personendokumente erstellt werden, die lediglich den Benutzernamen und das DB2-User-Mapping enthalten. Dies ist ausreichend, um Domino die Zuordnung zwischen Domino- und DB2-Benutzern zu ermöglichen.
13.4.5
DB2 Access View
Nachdem nun in den vorigen Kapiteln die Voraussetzungen für die Arbeit mit NSFDB2 geschaffen wurden, wird in den folgenden zwei Kapiteln die praktische Arbeit mit den beiden neuen Designelementen DB2 Access View und Query View behandelt. Damit mit DB2 Access Views gearbeitet werden kann, müssen zwei Voraussetzungen erfüllt werden. Die Domino-Datenbank muss im NSFDB2-Format erstellt worden sein bzw. muss in dieses überführt werden (s. Abb. 13-16) und der DB2 Access Server muss auf demjenigen Server installiert sein, auf dem die DB2-Datenbank betrieben wird. Ziel des DB2 Access Views (DAV) ist die Bereitstellung von Domino-Daten in einer DB2-Datenbank. Er wird im Designer erstellt und findet sich in der Rubrik „Shared Resources“ (s. Kap. 3.1). Der DAV ist kein Designelement im eigentlichen Sinne (daher auch die Einordnung als Shared Resource und nicht etwa als View), sondern lediglich die Definition des Mappings, über das festgelegt wird, wie die Domino-Daten in DB2 dargestellt werden sollen. Der Rest ist transparent und wird im Hintergrund erledigt. Nach dem Erstellen eines DAV im Designer wird über einen Dialog (s. Abb. 1317) festgelegt, welche Felder und deren Inhalte in DB2 sichtbar werden sollen. Hierbei 95
Nicht zu verwechseln mit der notes.ini-Datei des eigentlichen Domino-Servers. Wird der Parameter in die letzte Zeile der notes.ini geschrieben, muss nachfolgend noch eine Leerzeile eingefügt werden. Ein Neustart des DB2-Datenbankmanagers ist anschließend erforderlich. Danach wird in der angegebenen Datei ausführlich das Benutzermapping und eventuell dabei auftretende Probleme protokolliert.
546
13 Objekt- und Speichertechniken
Abb. 13-17 DB2 Access View – Felderdefinitionen
ist zu beachten, dass nur einfache Datentypen in DB2 zur Verfügung gestellt werden können. RichText kann nicht in DB2 für die Verwendung via SQL zur Verfügung gestellt werden, auch wenn aufgrund des NSFDB2-Formats die RichText-Inhalte in DB2 gespeichert sind. Im Dialogfenster stehen über eine Auswahlliste die in der Datenbank definierten Masken und die darin verwendeten Felder zur Auswahl. Weitere Feld-Mappings können später frei definiert werden. Im nächsten Schritt müssen die Mappings zwischen den Domino- und den DB2Datentypen hergestellt werden. Schwierig wird dies bei Domino-Datentypen, die in DB2 keine Entsprechung haben. Dies sind insbesondere Listenfelder. Der DAV bietet hierfür die Möglichkeit festzulegen, wie solche Daten in DB2 dargestellt werden sollen. Für Strings ist dies einfach: Listenfelder können z.B. durch Semikolon getrennt in einer VARCHAR-Spalte in DB2 dargestellt werden. Für Listenfelder, die in Domino-Zahlen enthalten, wird dies schwieriger. Hier besteht als einziges die Möglichkeit, solche Werte ebenfalls in einem VARCHAR zu speichern (s. Abb. 13-18 und Abb. 13-19). Andere Kombinationen werden durch die im Designer eingebaute Validierung abgelehnt. Alternativ kann die Einstellung „Store multiple values as“ auf „Single Value only“ geschaltet werden, hier werden dann aber Laufzeitfehler auftreten, sollten sich dennoch mehrere Werte in den Document Items befinden. Neben dem Datentyp müssen auch Zusatzinformationen wie z.B. die Spaltenbreite bei VARCHAR angegeben werden. Dem DAV können beliebige Items (Felder) hinzugefügt werden. Dies können auch Items sein, die zum gegenwärtigen Zeitpunkt in keinem der Dokumente der Domino-Datenbank vorliegen. Die so benannte Spalte des DAV in DB2 wird dann jeweils den Wert NULL zeigen. Es liegt in der Natur der Domino-Dokumente, dass ih-
13.4.5 DB2 Access View
547
Abb. 13-18 DB2 Access View – Typendefinitionen
nen beliebige Items – unabhängig von einem Schema – hinzugefügt werden können. Wird also einem Dokument in Domino ein so im DAV referenziertes Item hinzugefügt, so wird in der entsprechenden Spalte in DB2 dieser Wert angezeigt. Nachdem die Bedingungen für die Konvertierung der Datentypen feststehen, müssen nun noch Einstellungen vorgenommen werden, die festlegen, wie Dokumente in Domino behandelt werden sollen, die direkt per SQL erstellt wurden.
Abb. 13-19 DB2 Access View – Listenwerte bei Zahlen
548
13 Objekt- und Speichertechniken
Abb. 13-20 DB2 Access View – Dokumentenzuordnung
Dies gilt insbesondere für die Zuordnung von Datensätzen zu Domino-Masken (s. Abb. 13-20). In den Eigenschaften des DAV kann festgelegt werden, ob und welche Masken den Datensätzen zugeordnet sein sollen und ob z.B. ein so genannter computeWithForm (s. Kap. 7.1.4.4) erfolgen soll, wenn via DB2 neue Datensätze eingefügt werden. Hierbei ist zu berücksichtigen, dass dieser Vorgang performancerelevant ist. Nach Abschluss der Felddefinitionen muss nun diese DAV-Konfiguration an DB2 promotet werden (s. Abb. 13-21). Dies erfolgt durch Nacheinander-Betätigen der Funktionen „Create / Update in DB2“, „Populate in DB2“ und „Refresh Status“. Eventuelle spätere Änderungen erfordern die erneute Ausführung dieser Befehle.
Abb. 13-21 DB2 Access View in DB2 aktualisieren
13.4.5.1 DB2 Access View – Ein Beispiel
549
Ging alles glatt, dann wird der Status als OK gemeldet. In DB2 wird dann eine neue Tabelle und eine neue Ansicht erstellt worden sein. Die neue Tabelle für den DAV hat den Namen des DAV mit dem Suffix „_T“. Diese Tabelle ist so konfiguriert, dass der Benutzer, der im Domino Designer den DAV erstellt hat, keine Rechte in DB2 auf dieser Tabelle hat. Zugriff erhält der im Domino-Adressbuch eingetragene Funktionsuser, der für die Kommunikation zwischen Domino und DB2 zuständig ist (s. Kap. 13.4.3). Zusätzlich wird eine Ansicht mit Namen des DAV erstellt. Diese erhält Zugriff für das Schema PUBLIC. Allerdings ist innerhalb der Ansicht und durch den Domino Access Server gewährleistet, dass die Berechtigungen, die für die ursprünglichen Daten in Domino galten, auch in DB2 eingehalten werden – insbesondere Leser- und Autorenfelder werden hier respektiert. Hierin liegt auch der Grund für die Erstellung sowohl einer Tabelle als auch einer Ansicht. Die Tabelle dient lediglich der Speicherung der Daten, die Ansicht setzt die Berechtigungen durch96. Umgekehrt folgt hieraus, dass beim Zugriff auf die Daten per SQL / Java nur die Ansicht referenziert werden soll. Zu beachten ist, dass die erstellte Tabelle tatsächlich redundante Daten enthält. Das heißt, Domino führt einerseits seine Domino-Daten im NSFDB2-Format und andererseits wird bei der Verwendung eines DAV der hierdurch definierte Datenbestand in der neuen DAV-Tabelle in DB2 redundant geführt. DB2 nutzt hierfür Mechanismen, die es DB2 ermöglichen, den Datenbestand dieser Tabelle aktuell zu halten und umgekehrt. Domino trennt also konsequent die im NSFDB2 gehaltenen Domino-Daten von den nach außen zur Verfügung gestellten DAV-Daten. Vom direkten Zugriff auf die in DB2 vorliegenden Domino-Daten (in der Regel im Schema DOMINO) wird dringend abgeraten; er wird durch die DB2 Access Views auch überflüssig. Die beiden neuen DB2-Elemente (Tabelle und Ansicht) werden von Domino in einem neuen Schema angelegt. Dieses Schema wird nach dem Datenbanknamen aus Domino benannt.
13.4.5.1
DB2 Access View – Ein Beispiel
Der Einsatz eines DAV soll anhand eines Java-Beispiels demonstriert werden. Im Code zum Buch befindet sich die Datenbank djbuchdb2.nsf. Sie enthält lediglich eine Ansicht und eine Maske, die Felder mit verschiedenen Datentypen enthält, um das Mapping zwischen Domino und DB2 aufzuzeigen. Die Maske basiert auf FO_Dokument_k2, einer Maske mit verschiedenen Feldern unterschiedlicher Datentypen (siehe Beispieldatenbank). 96
Beachten Sie beim Zugriff immer, mit welchem Benutzer dies erfolgt. Dies gilt sowohl für die Verwendung des Domino Designers bei der Erstellung des DAV, als auch für die Benutzung der DB2-Steuerzentrale zur Visualisierung der DAV-Daten. Da unter Windows lokale Administratoren auch immer administrative Rechte auf DB2 erhalten, ist hier Vorsicht bei der Entwicklung geboten. Anwendungen sind immer mit Benutzern zu testen, die Rechte innehaben, die denen der späteren Benutzer in Produktion entsprechen.
550
13 Objekt- und Speichertechniken
Nun soll ein DAV mit Namen VDB2_Access eingerichtet werden. Die Abbildungen Abb. 13-18 bis Abb. 13-21 im vorigen Kapitel zeigen bereits die notwendigen Schritte. Nach der Erstellung des DAV sollten sich in der DB2-Datenbank DOMINO die Tabelle VDB2_Access_T und die Ansicht VDB2_Access befinden. Um nun mit Java auf diese SQL-Ansicht zugreifen zu können, sind einige Vorbereitungen notwendig. Bevor Sie mit der Java-Programmierung beginnen, sollten Sie sich mit einem geeigneten SQL IDE versorgen, damit Sie Ihre Arbeiten kontrollieren können. Wer mit Eclipse als Java IDE arbeitet, wird sehr gut mit dem QuantumDB Plugin für Eclipse [QuantumDB] zurechtkommen. Natürlich ist auch die von DB2 mitgelieferte Administrations- und Entwicklungskonsole hervorragend geeignet. Der Vorteil eines eigenen IDE liegt in der Integration ins Java IDE. Gleichzeitig ist das Wechseln von Benutzern durch die Verwendung von Datenbankprofilen recht einfach. Der Vorteil der DB2-Administrations- und Entwicklungskonsole liegt darin, dass sie sämtliche DB2Objekte verfügbar hält, die zum Teil in anderen IDEs nicht dargestellt werden (z.B. Zugriffsrechte auf DB2-Objekte). Wichtig ist, dass beim Aufbau einer JDBC-Verbindung nicht nur die DB2-JDBC Treiberklasse im ClassPath eingebunden wird, sondern auch die zugehörige LizenzKlasse. Dies ist eine Jar-Datei, in der die Lizenz für DB2 gespeichert ist (Beispiel für
Abb. 13-22 SQL Plugin
13.4.5.1 DB2 Access View – Ein Beispiel
551
QuantumDB – s. Abb. 13-22). Für eine Windows-Standardinstallation sind dies die Dateien db2jcc.jar und db2jcc_license_cu.jar97 im Verzeichnis C:\Programme\IBM\ SQLLIB\java. Für Ihre Anwendung gelten die gleichen Anforderungen für den ClassPath, d.h. auch hier müssen sich diese beiden Dateien im ClassPath befinden (Abb. 13-23). Um die Domino-Daten im DB2 Access View per JDBC und SQL auslesen zu kön-
Abb. 13-23 Java ClassPath für DB2-JDBC-Anwendungen
nen, müssen lediglich die durch DB2 Access Server und DAV definierten Mappings, die sich in DB2 als Tabellenspalten darstellen, ausgelesen und die zugehörigen Items und Datentypen korrekt interpretiert werden. Das Listing 13-13 zeigt eine einfache Anwendung, die den zuvor definierten DB2 Access View ausliest. Hierbei kommen einfache JDBC-Techniken zur Anwendung. Der JDBC Connection String setzt sich für DB2 aus der Kennung jdbc:db2://, dem Server oder dessen IP auf dem sich die Datenbank befindet, dem Port, unter dem die DB2-Instanz betrieben wird, gefolgt von einem Slash „/“ und dem Datenbanknamen zusammen: 97
Ohne die korrekte Lizenz erhalten sie eine SQL Exception com.ibm.db2.jcc.b.SqlException: The version of the IBM Universal JDBC driver in use is not licensed for connectivity to QDB2/NT databases. To connect to this DB2 server, please obtain a licensed copy of the IBM DB2 Universal Driver for JDBC and SQLJ. An appropriate license file db2jcc_license_*.jar for this target platform must be installed to the application classpath. Connectivity to QDB2/NT databases is enabled by any of the following license files: { db2jcc_license_cu.jar, db2jcc_license_cisuz.jar } at com.ibm.db2.jcc.b.o. eb(o.java:3323) ...
552
13 Objekt- und Speichertechniken
package djbuch.kapitel_13; import java.sql.*; import djbuch.kapitel_06.DemoTools; public class Db2AccessView { private static final String db2Url = "jdbc:db2://localhost:50000/DOMINO"; private static final String PASSWORD = "geheim"; private static final String USER = "db2admin"; private static final String SCHEMA = "VDB2_ACCESS"; private static final Column [] COLUMNS = { new Column ("#NOTEID",Column.TYPE_STRING), new Column ("F_TITEL",Column.TYPE_STRING), new Column ("F_DATUM",Column.TYPE_DATE), new Column ("F_DATUM_LISTE",Column.TYPE_DATELIST), new Column ("F_NAME",Column.TYPE_STRING), new Column ("F_NAME_LISTE",Column.TYPE_STRING), new Column ("F_PASSWORT",Column.TYPE_STRING), new Column ("F_TEXT",Column.TYPE_STRING), new Column ("F_TEXT_LISTE",Column.TYPE_STRING), new Column ("F_TITEL",Column.TYPE_STRING), new Column ("F_ZAHL",Column.TYPE_LONG), new Column ("F_ZAHL_LISTE",Column.TYPE_LONGLIST), new Column ("FORM",Column.TYPE_STRING) }; public static void main(String[] args) { Connection con=null; Statement stmt=null; try { Class.forName("com.ibm.db2.jcc.DB2Driver"); con = DriverManager.getConnection( db2Url, USER, PASSWORD ); stmt = con.createStatement(); String query = "SELECT" + generateColumnSQL() + "FROM DJBUCHDB2.VDB2_ACCESS AS VDB2_ACCESS"; System.out.println (query); ResultSet result = stmt.executeQuery(query); int marker=0; while (result.next()) { printColumns (result,marker++); } stmt = con.createStatement(); stmt.executeUpdate(generateRandomInsertSQL()); } catch (SQLException e) { e.printStackTrace(); ... } finally { try { if (stmt!=null) { stmt.close();} if (con!=null){ con.close(); } } catch (SQLException e1) { e1.printStackTrace(); } } } private static void printColumns(ResultSet result, int marker) throws SQLException { StringBuffer res=new StringBuffer(); res.append ("=== row ").append (marker).append (" =========\n"); for (int i = 0; i < COLUMNS.length;i++) { switch (COLUMNS[i].getType()) { case (Column.TYPE_STRING): case (Column.TYPE_DATELIST): case (Column.TYPE_LONGLIST): String val = result.getString(COLUMNS [i].getName());
13.4.5.1 DB2 Access View – Ein Beispiel
553
if (val==null) {break;} String[] arr = val.split(";"); for (int j = 0; j < arr.length; j++) { res.append ("\t").append (COLUMNS [i].getName()) .append ("(").append(j).append("): ") .append (arr[j]).append("\n"); } break; case (Column.TYPE_DATE): java.sql.Date dt = result .getDate(COLUMNS [i].getName()); if (dt==null) {break;} res.append ("\t") .append (COLUMNS [i].getName()).append ("(0): ") .append (dt.toString()).append("\n"); break; case (Column.TYPE_LONG): long l = result.getLong(COLUMNS [i].getName()); res.append ("\t") .append (COLUMNS [i].getName()).append ("(0): ") .append (l).append("\n"); break; default: break; } } System.out.println (res.toString()); } private static final String generateColumnSQL () { StringBuffer res = new StringBuffer(" "); for (int i = 0; i < COLUMNS.length; i++) { res.append(SCHEMA).append(".").append(COLUMNS [i].getName()) .append ((i
Listing 13-13 DB2 Access View per JDBC und SQL anzeigen
„jdbc:db2://localhost:50000/DOMINO“ . Die Verbindung wird über den DriverManager erzeugt, die verwendete Klasse für den DB2-JDBC-Treiber hört auf den Namen „com.ibm.db2.jcc.DB2Driver“. Der für die Verbindung verwendete Benutzername bestimmt die Rechte, die DB2 später gewähren wird. Das in Domino definierte User-Mapping (s. Kap.
554
13 Objekt- und Speichertechniken
DB2 Access View Darstellung von Domino-Daten in DB2 Daten werden in einer Tabelle mit Suffix „_T“ repräsentiert und über eine Ansicht verfügbar gemacht. Die Tabelle wird ohne Zugriffsrechte für den Benutzer ausgestattet und soll nicht referenziert werden. Daten werden über eine Ansicht, die denselben Namen trägt wie der DAV, zur Verfügung gestellt. In der Ansicht werden die ACL und Leser- und Autorenfelder respektiert. Die Tabelle und die Ansicht wird in einem neuen Schema mit Namen der Domino-Datenbank abgelegt. Daten, die über DAV zur Verfügung gestellt werden, werden redundant in der korrespondierenden Tabelle gehalten. Domino-Daten, die im NSFDB2-Format gehalten werden, können nur über DAV, nicht aber direkt angesprochen werden.
13.4.3f.) bestimmt außerdem, welcher Domino-Benutzer beim Zugriff auf die Daten verwendet wird, schließlich bildet der DB2 Access View ja orginäre Domino-Daten ab, für die auch deren Rechte gelten sollen. Das Beispiel wird über ein SELECT Statement den SQL View „DJBUCHDB2.VDB2_ACCESS“, basierend auf der zugrunde liegenden Domino-Datenbank djbuchdb2.nsf und dem im Domino Designer definierten DAV „VDB2_ACCESS“ auslesen. Für die Ausführung der Query wird ein Statement und ein String benötigt, der die Query darstellt. Dieser wird über die Methode generateColumnSQL() erzeugt. Hierbei kommt ein Array aus Column-Objekten zum Einsatz, das das Mapping von Spaltennamen und Datentypen vereinfacht. Die Idee an diesem Verfahren ist, die Domino-Document-Items möglichst flexibel vorhalten und in SQL Statements umwandeln zu können und gleichzeitig beim Auslesen immer den zugehörigen ursprünglichen Domino-Datentyp zur Verfügung zu haben. Die Klasse Column (Listing 13-14) bedarf keiner weiteren Erläuterung, sie hält neben ein paar Konstanten für die Datentypen zwei getter-Methoden zum Auslesen von Item-Name und -Typ vor. Die Methode generateColumnSQL() kann nun anhand des Arrays COLUMNS und des Schemanamens SCHEMA die gültige SELECT-Query zusammenbauen. Falls später einmal der DB2 Access View im Domino Designer angepasst wird, so wird Domino die Tabellen in DB2 entsprechend anpassen. Nun ist lediglich eine Ergänzung im Array COLUMNS notwendig, um dieser Änderung Rechnung zu tragen.
13.4.5.1 DB2 Access View – Ein Beispiel
555
package djbuch.kapitel_13; public class Column { private String name=null; private int type = -1; public static final int TYPE_UNDEFINED= -1; public static final int TYPE_STRING = 0; public static final int TYPE_DATE = 1; public static final int TYPE_LONG = 2; public static final int TYPE_DATELIST = 3; public static final int TYPE_LONGLIST = 4; public Column (String fieldName, int type) { if (type < TYPE_STRING || type > TYPE_LONGLIST) { type = TYPE_UNDEFINED;} this.name=fieldName; this.type=type; } public String getName () { return name;} public int getType () { return type;} }
Listing 13-14 Klasse Column
Wird nun die Query ausgeführt , steht in einem ResultSet das Ergebnis zur Verfügung und kann ausgelesen werden. Um die Ergebnisse entsprechend ihrer Datentypen richtig interpretieren zu können, werden in der Methode printColumns() die in COLUMNS definierten Datentypen ausgewertet. Damit nach getaner Arbeit die Connection und das Statement wieder korrekt (und ausfallsicher) geschlossen und aufgeräumt werden kann, wurde deren Deklaration außerhalb des try-catch Statements platziert und im finally die entsprechenden close() Statements aufgerufen . Diese Konstruktion stellt sicher, dass auch im (unerwarteten) Fehlerfall zumindest versucht wird, die Verbindungen zu schließen, und ähnelt nicht zufällig dem für das Recycling von Domino-Objekten notwendigen Aufbau. Das Auslesen der Daten muss nicht nur die Datentypen, sondern auch die im DAV vorgenommenen Einstellungen berücksichtigen. Diese Einstellungen werden durch die Typen in den Column-Objekten des COLUMNS Array repräsentiert. Nach dem Abfeuern der SQL Query liegt das SQL ResultSet vor und kann ausgelesen werden. Das Ergebnis wird zeilenweise aus dem ResultSet der Methode printColumns() zur weiteren Verarbeitung übergeben. Dort können nun die Spaltenwerte jeder Zeile anhand ihres in COLUMNS definierten Typs korrekt interpretiert werden. Im Beispiel wurden folgende Datentypen vorgesehen: •
TYPE_UNDEFINED
Wird der Konstruktor von Column mit einem ungültigen Wert aufgerufen, wird dieser Datentyp angenommen. •
TYPE_STRING
556
13 Objekt- und Speichertechniken Domino kennt Text und Textliste als Datentyp für ein Item. Beide werden durch diesen Typ repräsentiert. Bei einer Auswertung muss die entsprechende Methode respektieren, dass mehrere Werte durch ein Trennzeichen getrennt in der DB2Tabellenspalte abgespeichert sind. Das Trennzeichen wird bei der Definition des DAV (s. Abb. 13-18 – DB2 Multivalue Delimiter) festgelegt und ist per Default das Semikolon „;“.
•
TYPE_DATELIST TYPE_LONGLIST
Da DB2 keine Listenelemente kennt, ist die einzige Chance, solche Domino-Objekte in DB2 abzulegen, diese in VARCHAR-Spalten, getrennt durch ein Trennzeichen, zu speichern. Folglich können und müssen diese ebenso wie TYPE_STRING per getString () aus der ResultSet-Zeile ausgelesen werden. Im Beispiel wurde bisher für die hierdurch geladenen Listenelemente noch keine Rückwärtskonvertierung implementiert. Diese Möglichkeit besteht natürlich weiterhin, da anhand des Typs ja bekannt ist, welcher Datentyp sich in der VARCHAR-Liste eigentlich verbirgt. •
TYPE_DATE
Domino und DB2 kennen einen Datumstyp, der bei korrekter Einstellung des Fieldmappings im DAV problemlos konvertiert werden kann. Wird über diesen Typ der Datumstyp bekanntgegeben, so kann beim Auslesen dieser Datetyp als java.sql.Date geladen werden. •
TYPE_LONG
Äquivalent zu TYPE_DATE kann mit TYPE_LONG
verfahren werden.
Wenn Sie Kapitel 13.2.1 gelesen haben, haben Sie dort das DJCacheDocument kennen gelernt. Es ist eine Repräsentation der Daten eines Domino-Document, das sich weitestgehend auch wie ein Document verhält, ohne auf eine Session angewiesen zu sein. Nun liegt es nahe, die Daten nicht einfach nur, wie im Beispiel, anzuzeigen , sondern entsprechend der ursprünglichen Datenstruktur abzubilden. Hierfür ist das DJCacheDocument geradezu prädestiniert. Beim Auslesen der Daten aus dem SQL Resultset können insbesondere die ansonsten schwer handhabbaren Listentypen im DJCacheDocument per replaceItemValue (String, Vector) abgelegt werden. So kann mit einfachen Mitteln die ursprüngliche Datenstruktur wiederhergestellt werden. Ein Ansatz zur Umsetzung wäre ein neuer Konstruktor für das DJCacheDocument mit einer Signatur in der Art DJCacheDocument (ResultSet row, Column[] columns). Mittels des Parameters row werden Domino und DB2 Hand in Hand dann die Daten einer Zeile Das DJCacheDocument – Kapitel 13.2.1 – ist ein gutes Hilfsmittel für das Auslesen der ursprünglichen Domino-Daten aus DB2. Listenelemente können wie gewohnt als Item abgelegt werden.
13.4.5.1 DB2 Access View – Ein Beispiel
557
übergeben, wobei columns definiert, wie die einzelnen Datentypen der jeweiligen Spalten zu interpretieren sind. Ist erst einmal dieses Dokument derart eingeführt, ergeben sich viele Möglichkeiten. Denkbar ist zum Beispiel, einen eigenen Wrapper um die JDBC-Verbindung zu definieren, der eine Methode insert (DJCacheDocument newData) kennt, die als Argument ein DJCacheDocument kennt und automatisch ein entsprechendes INSERT Statement generiert und ausführt oder umgekehrt über eine Methode DJCacheDocument load (String noteID) ein SQL Statement ausführt, das einen Datensatz mit einer bestimmten NoteID lädt und als DJCacheDocument zurückgibt. Das Beispiel zeigt auch den umgekehrten Weg auf: Die Erstellung neuer Datensätze durch ein SQL INSERT Statement . Hierbei wird ein neuer Datensatz erzeugt, der dann – aufgrund der Definition als DAV – auch in Domino sichtbar wird. Erwähnenswert ist, dass natürlich auch hier Listenfelder definiert werden können, indem Strings durch Semikolon getrennt übergeben werden, wie im Beispiel für ein TEXT-Listenfeld demonstriert . Selbstverständlich können hier auch Zahl-Listenfelder durch einen entsprechend durch Semikolon getrennten String-Wert gefüttert werden, sofern nur in der Definition des DAV das Mapping entsprechend eingerichtet ist. Abb. 13-24 zeigt ein Item, das durch das Beispiel generiert wurde. Deutlich zu erkennen ist, dass, obwohl ein String in eine VARCHAR Spalte Abb. 13-24 Zahl Liste –per SQL Insert erzeugt eingefügt wurde, Domino über die Definition des DAV eine Liste aus Long erzeugt hat. Da die Daten per SQL in eine Ansicht eingefügt wurden, die per DAV definiert wurde, stehen diese Daten ebenfalls in Domino zur Verfügung. Folglich können sie wie ganz normale Dokumente der Domino-Datenbank behandelt und z.B. über Ansichten gelistet werden. Darüber hinaus wird über die Definition im DAV den Datensätzen (Dokumenten) eine Maske (Item Form) zugeordnet und gleichzeitig durch den DB2 Access Server eine NoteID (Spalte #NoteID in DB2) erzeugt. Derart über DB2 und SQL INSERT erstellte Dokumente verhalten sich aus Sicht von Domino wie normale Domino-Dokumente. Im Beispiel erhalten solche Dokumente über die Definition im DAV die Default-Form „FO_dokument_k13“, sind über eine Ansicht mit einer entsprechenden SELECT-Formel in Domino anzeigbar und werden durch Doppelklick im NotesClient wie gewohnt in einer Notes-Maske geöffnet. Gleichzeitig stehen diese Dokumente auch sämtlichen Java-Funktionen zur Verfügung, die Sie bereits zur Verarbeitung von Domino-Dokumenten kennengelernt haben.
558
13 Objekt- und Speichertechniken
13.4.6
Query View
Während ein DAV also Domino-Daten in DB2 verfügbar macht, ermöglicht der Query View DB2-Daten in Domino zur Verfügung zu stellen. Die Erstellung eines Query View erfolgt über den Domino Designer. Im Gegensatz zum DAV ist kein DB2 Access Server notwendig, aber selbstverständlich muss die Datenbank im NSFDB2-Format vorliegen. Ein Query View wird als normale Ansicht behandelt, enthält alle gewohnten Funktionen und Elemente, wie z.B. Spaltendefinitionen etc., und unterscheidet sich lediglich dadurch von einer gewöhnlichen Domino-Ansicht, dass die SELECTFormel nicht als @Formel, sondern als SQL-Befehl definiert wird. Ein weiterer Unterschied ist natürlich, dass eine solche Ansicht – wie alle DB2basierten Domino-Designelemente – nicht lokal, sondern nur auf einem DominoServer zur Verfügung steht. Um einen Query View zu erstellen, wählen Sie im Domino Designer im Abschnitt „Views“ den Button „new View“. Für das folgende Beispiel wurde zunächst in DB2 eine neue Tabelle angelegt: CREATE TABLE DJBUCHDB2.NON_DOMINO_SQL_DATA ( ID BIGINT NOT NULL GENERATED ALWAYS AS IDENTITY (START WITH 0, INCREMENT BY 1, NO CACHE), TEXT VARCHAR (20), ZAHL_INT BIGINT, ZAHL_DECIMAL DECIMAL (7, 3) NOT NULL, CONSTRAINT CC1130852963422 PRIMARY KEY (ID) ) ;
Abb. 13-25 Query View erstellen
13.4.6 Query View
559
Abb. 13-26 Query View im Domino Designer
Diese Tabelle wird über die DB2-Administrationskonsole mit Testdaten gefüllt und kann nun in dem neuen Query View mit einer SQL SELECT-Formel ausgelesen werden: "SELECT NON_DOMINO_SQL_DATA.ID, NON_DOMINO_SQL_DATA.TEXT, NON_DOMINO_SQL_DATA.ZAHL_INT, NON_DOMINO_SQL_DATA.ZAHL_DECIMAL FROM DJBUCHDB2.NON_DOMINO_SQL_DATA AS NON_DOMINO_SQL_DATA ORDER BY NON_DOMINO_SQL_DATA.ZAHL_DECIMAL"
Zu beachten ist, dass die Formel in Anführungszeichen zu setzen ist. Dies rührt daher, dass Domino es erlaubt, die SQL-Formel dynamisch (!) zu berechnen. Im Eingabefeld hierfür (Abb. 13-25 – „By SQL Query“) ist eine @Formel vorgesehen, die die eigentliche SQL Query berechnet. Die einfachste @Formel ist ein statischer String, gekennzeichnet durch die Anführungszeichen. Angenehmerweise ist es an dieser Stelle erlaubt, auch interaktive @Formeln zu verwenden, so ist z.B. die Verwendung von @Prompt zur Eingabe einer Benutzervorgabe möglich, so dass SQL-Abfragen dynamisch von Benutzereingaben abhängig gemacht werden können: "SELECT * FROM DJBUCHDB2.VDB2_ACCESS AS VDB2_ACCESS Where F_Text like '" + @Prompt ([OkCancelEdit];"Anfangsbuchstabe eingeben";"Feld F_Text beginnt mit ";"") + "%'"
560
13 Objekt- und Speichertechniken
Bedingungen für Query Views In der SQL Query eines Query Views dürfen (im Wesentlichen aus Sicherheitsgründen) nur solche SQL Statements verwendet werden, die ein ResultSet als Ergebnis haben. Spalten der DB2-Tabelle, die in Domino dargestellt werden sollen, müssen sowohl in der SQL Query des Query View selektiert, als auch in der Spaltendefinition der Domino-Ansicht definiert werden. Beim Öffnen und Anzeigen der derart aus DB2 geladenen Daten wird derjenige (DB2-) Benutzer verwendet, der dem aktuellen Domino-Benutzer über das Domino Directory (names.nsf) zugeordnet wurde. JEDER Domino-Benutzer, der einen Query View benutzen soll, muss über einen zugehörigen DB2-Benutzer verfügen. Jeder Domino-Benutzer bzw. dessen zugeordneter DB2-Benutzer muss über ausreichend Rechte auf dem DB2-Objekt verfügen. Die DB2-Berechtigungen müssen über DB2 administriert werden. Werden mehrere DB2 Access Views über einen JOIN als Query View dargestellt, können nur Dokumente der aktuellen Datenbank auf direktem Weg geöffnet werden (Doppelklick im Notes Client oder ViewEntry.getDocument in Java). „Entfernte“ Dokumente können referenziert werden, wenn im Query View für diese Dokumente im JOIN die Note ID und der Server und Replik ID der Datenbank bekannt ist. Da im Query View die Daten des geJOINten Dokuments natürlich über getColumnValues() verfügbar sind, ist es die NoteID und somit das entfernte Dokument. Serverseitig (DB2) ist das ResultSet per Default auf 500 Dokumente beschränkt.
Abb. 13-27 Voraussetzungen für die Verwendung von Query Views
Jede Spalte der DB2-Tabelle, die im Notes Client angezeigt oder über die entsprechenden Java-Methoden der Klasse View und ViewEntryCollection geladen werden soll, muss in der Ansichtendefinition (Abb. 13-26) als Spalte definiert und über die SQL Query selektiert werden. Gleichzeitig ist zu berücksichtigen, dass der dem DominoBenutzer zugeordnete DB2-Benutzer über ausreichende Berechtigungen auf dem DB2-Objekt verfügen muss (s. Abb. 13-27). Ist der Query View definiert, kann er wie eine gewöhnliche Domino-Ansicht verwendet werden (s. Abb. 13-28). Allerdings öffnet ein Doppelklick auf einen Eintrag in
13.4.6 Query View
561
Abb. 13-28 DB2 und Domino – Ansicht des Query View in Domino und DB2
der Liste keinesfalls ein Dokument in einer Maske, sondern dies wird durch eine Fehlermeldung quittiert, die darauf hinweist, dass derartige SQL-Daten nicht in Domino-Masken dargestellt werden können. Dies ist ein kleiner Wermutstropfen, zumal (zur Zeit) keine Möglichkeit besteht, dies abzufangen und z.B. das Dokument durch ein eigenes GUI darzustellen (Applet, Maske, die die gewünschten Daten per Java/SQL lädt und anzeigt etc.). Die eigentliche Leistungsfähigkeit des Query View erwacht, wenn durch einen Query View Daten eines DAV per SQL geladen werden. Warum sollte man dies tun, fragen Sie sich vielleicht? Stellt man zwei Ansichten – möglicherweise zweier verschiedener Datenbanken – als DAV zur Verfügung, dann können deren Daten in einem JOIN in einen Query View geladen werden: Voilà! Domino hat die Datenbankgrenzen überwunden! Wird in der SQL Query des Query View die Spalte #NoteID eines der DAV geladen, dann wird Domino dies automatisch erkennen und das Domino-Dokument aus diesem DAV bei einem Doppelklick auch in der zugehörigen Maske öffnen. Aber Achtung: Dies gilt nur für Dokumente in der aktuellen Datenbank. Das Gesagte lässt sich nun auf die Java-Programmierung der bekannten Klassen View und ViewEntryCollection übertragen.
562
13 Objekt- und Speichertechniken
Dort besteht natürlich zusätzlich die Möglichkeit, im JOIN die NoteID beider beteiligten Dokumente der beiden DAV zu laden und entsprechend damit umzugehen (Wie wär`s mit einem neuen DJCacheDokument, das die Items beider Domino-Dokumente in sich vereinigt?) Angenommen, es gibt auf dem Domino-Server zwei Datenbanken DJBUCHDB2_ THISDB.nsf und DJBUCHDB2_OTHERDB.nsf und dort sind jeweils ein DAV mit Namen V_DAV_THIS_DB und V_DAV_OTHER_DB definiert, dann kann durch folgende Query ein beispielhafter JOIN auf beide DAV ausgeführt und als Query View vereinigt werden: SELECT V_DAV_THIS_DB.#NOTEID, V_DAV_THIS_DB.F_DATUM, V_DAV_THIS_DB.F_DATUM_LISTE, V_DAV_THIS_DB.F_NAME, V_DAV_THIS_DB.F_NAME_LISTE, V_DAV_THIS_DB.F_PASSWORT, V_DAV_THIS_DB.F_TEXT, V_DAV_THIS_DB.F_TEXT_LISTE, V_DAV_THIS_DB.F_TITEL, V_DAV_THIS_DB.F_ZAHL, V_DAV_THIS_DB.F_ZAHL_LISTE, V_DAV_THIS_DB.FORM, V_DAV_OTHER_DB.#NOTEID AS NOTEID2, V_DAV_OTHER_DB.F_DATUM AS DATUM2, V_DAV_OTHER_DB.F_DATUM_LISTE AS DLISTE2, V_DAV_OTHER_DB.F_NAME AS NAME2, V_DAV_OTHER_DB.F_NAME_LISTE AS NLISTE2, V_DAV_OTHER_DB.F_PASSWORT AS PWORD2, V_DAV_OTHER_DB.F_TEXT AS TEXT2, V_DAV_OTHER_DB.F_TEXT_LISTE AS TLISTE2, V_DAV_OTHER_DB.F_TITEL AS TITEL2, V_DAV_OTHER_DB.F_ZAHL AS ZAHL2, V_DAV_OTHER_DB.F_ZAHL_LISTE AS ZLISTE2, V_DAV_OTHER_DB.FORM AS FORM2 FROM DJBUCHDB2_THISDB.V_DAV_THIS_DB AS V_DAV_THIS_DB LEFT JOIN DJBUCHDB2_OTHERDB.V_DAV_OTHER_DB AS V_DAV_OTHER_DB ON V_DAV_THIS_DB.F_ZAHL = V_DAV_OTHER_DB.F_ZAHL
Angenommen, in der Datenbank DJBUCHDB2_THISDB.nsf befindet sich dieser Query View und hieße V_Query_View. Dann könnte dieser als View in Java geladen und z.B. mit einem Loop über getAllEntries pro Entry sämtliche Spaltenwerte beider DAV per entry.getColumnValues() gelesen werden. Da in der Spalte V_DAV_OTHER_ DB.#NOTEID AS NOTEID2 die NoteID der entfernten Datenbank DJBUCHDB2_ OTHERDB zur Verfügung steht, kann – sofern deren Name und Pfad oder Replik ID bekannt ist – das dort befindliche Dokument geöffnet werden.
13.4.6.1 Query View – Ein Beispiel
563
Für einen Query View, der aus einem oder mehreren DAV besteht, können also sehr wohl alle beteiligten Dokumente geladen werden. Beim obigen Beispiel würde bei einem Doppelklick auf eine Zeile dieses Query View im Notes Client lediglich das Dokument aus DJBUCHDB2_THISDB.nsf geöffnet. Während über die NoteID in Java zusätzlich das zweite Dokument recht einfach geladen werden konnte, ist dies im Notes Client komplizierter. Dort müsste der Event „Queryopendocument“ abgefangen werden und das entfernte Dokument relativ aufwändig geladen und desssen Inhalt zu allem Überfluss auch noch im aktuellen oder einem völlig neuen Dokument gespeichert werden, um angezeigt werden zu können. Grundsätzlich gilt die Regel, dass ein DAV wie eine DB2-Tabelle behandelt wird. Folgerichtig ist jeder aus Sicht von SQL denkbare JOIN oder SELECT zulässig. Ist ein DAV an einem JOIN beteiligt und wird die dort bereitgestellte Spalte #NoteID selektiert, so wird ein Query View, der diese Spalte liest, automatisch die zu diesen NoteIDs gehörigen Dokumente referenzieren, sofern sie sich in der aktuellen Datenbank befinden. Dies gilt sowohl für den Notes Client, der dann beim Doppelklick auf eine Zeile im Query View das Dokument in seiner Maske öffnen wird, als auch für die (Java-) API-Funktionen. Auch dort wird z.B. ein View.getAllDocumentsByKey eine Collection der gefundenen Document-Objekte liefern, die wie gewohnt als Document verarbeitet werden können. Ein Unterschied zu herkömmlichen Domino-Views ist zu beachten: Query Views werden nicht automatisch aktualisiert und müssen im Notes Client über die Taste F9 und über das API mit View.refresh() aktualisiert werden. Erst dann wird die SQL Query neu ausgeführt.
13.4.6.1
Query View – Ein Beispiel
Wurde die DB2-Tabelle anhand des CREATE TABLE-Beispiels und der Query View mit der SELECT-Anweisung aus Kapitel 13.4.6 erstellt, kann dieser mit dem CodeBeispiel aus Listing 13-15 gelesen werden. Er hat den Namen V_NON_DOMINO_SQL_k13 und die SELECT-Formel (s. Abb. 13-26): "SELECT NON_DOMINO_SQL_DATA.ID, NON_DOMINO_SQL_DATA.TEXT, NON_DOMINO_SQL_DATA.ZAHL_INT, NON_DOMINO_SQL_DATA.ZAHL_DECIMAL FROM DJBUCHDB2.NON_DOMINO_SQL_DATA AS NON_DOMINO_SQL_DATA ORDER BY NON_DOMINO_SQL_DATA.ZAHL_DECIMAL"
Die Java-Anwendung Db2QueryView (Listing 13-15) liest einen einfachen Query View aus. Sie wird mit den bekannten Mitteln – im Beispiel als Implementierung von Runnable – aufgebaut . Dies gilt auch für die Variablendeklarationen und den typischen try-catch-finally-Block für das Recycling . Die Datenbank wird mit den klassischen Mitteln geöffnet. Bemerkenswert ist, dass sie, obwohl im NSFDB2-Format, also als DB2-Datenbank vorliegend, über ihren Pfad referenziert werden kann.
Zunächst soll das Verhalten eines Query View untersucht werden, der reine SQLDaten lädt, die also ausschließlich in DB2 vorliegen. Dieser Query View V_NON_DOMINO_SQL_k13 wird wie ein herkömmlicher View geladen und liefert seine Treffer als ViewEntryCollection über getAllEntries. Sämtliche Methoden dieser Objekte sind implementiert und können verwendet werden. Erst wenn man bis auf die Ebene der Daten vorstößt, werden Unterschiede sichtbar. Jeder ViewEntry hat zwar noch ein Document, das über getDocument bezogen werden kann, da es jedoch kein „eigentliches“ NotesDocument gibt, handelt es sich hierbei um einen „leeren Rumpf“ eines Document. So liefert getItems() einen leeren Vector (Abb. 13-29, Teil 1).
13.4.6.1 Query View – Ein Beispiel
565
Abb. 13-29 Ergebnisse eines Query View aus reinen SQL-Daten und aus einem DAV
Andere allgemeingültige Properties wie z.B. die beinhaltende Datenbank getParentDatabase() werden wie gewohnt zur Verfügung gestellt. Erst die Verwendung von getColumnValues() in ViewEntry ermöglicht den Zugriff auf alle im Query View ausgewiesenen Spalten und deren Spaltenwerte. Anders formuliert: Egal welche SQL Query einem Query View zugrunde liegt: Über getColumnValues stehen die zugehörigen Spaltenwerte zur Verfügung, wie sie im Query View und der zugehörigen SQL Query definiert wurden. Zum Vergleich wurde im Beispielcode ein Query View erstellt, der sich auf die SQL-Daten eines DAV bezieht . Dort stehen zusätzlich, wie für einen herkömmlichen View, die Document-Objekte für jeden ViewEntry zur Verfügung. Dementsprechend haben die Dokumente auch Items, die wie gewohnt verarbeitet werden können. In Abb. 13-29 (Teil 2) ist gut zu sehen, dass der DAV sowohl in Domino veränderte, als auch über DB2 veränderte Dokumente enthält, da sich in den Dokumenten unterschiedlich viele Items befinden. Grundsätzlich lässt sich sagen, dass die Darstellung der Query Views recht elegant gelöst ist. Dadurch, dass in jedem Fall ein Document und dessen „Rumpf“-Methoden zur Verfügung stehen, ist vielen potentiellen Fehlern vorgebeugt (NullPointerExceptions, wg. fehlender Objekte) und in jedem Fall lassen sich die Daten über die Spaltenwerte und getColumnValues auslesen.
566
13 Objekt- und Speichertechniken
13.4.7
Federated Data – Entfernte SQL-Daten und Domino in DB2
Das bisher beschriebene Konzept der Query Views hat eine Grenze: Die Daten müssen sich innerhalb der zu Domino in DB2 korrespondierenden Datenbank (in der Regel DOMINO) befinden. DB2 sieht über den Mechanismus so genannter zusammengeschlossener Datenbanken die Möglichkeit vor, Tabellen entfernter Datenbanken über eine Art „AliasVerknüpfung“ in einer dritten Datenbank zur Verfügung zu stellen. Hierdurch können also auch in der für Domino zuständigen Datenbank externe Daten referenziert werden, ohne dass diese ausdrücklich und physisch in der DB2-Datenbank DOMINO liegen müssten. Nur dann, wenn die Tabellen auch physisch (oder eben über zusammengeschlossene Datenbanken) in der von Domino verwendeten DB2-Datenbank vorliegen, können diese über einen Query View erreicht werden. Im Code zur Datenbank befindet sich die Datei djbuch/kapitel_13/ federateddata.sql mit einem SQL-Skript zur Erzeugung einer solchen externen Anbindung98.
13.5
Zusammenfassung
Die Domino-Objekte sind entsprechend dem Domino-Objekt- und Speicherkonzept (fast) alle aus der Domino-Session abgeleitet bzw. werden durch diese bereitgestellt. Dementsprechend sind sie nicht nur von dem Session-Objekt, sondern auch von einer bestehenden Domino-Session abhängig. Gleichzeitig sind alle Domino-Java-Objekte als Implementierung von Interfaces aufgebaut, deren tatsächliche Ausprägung als lotus.domino.cso bzw. lotus.domino. local-Implementierungen aber in der Verwendung durch die NotesFactory außerhalb des eigenen Einflussbereichs erzeugt werden. In diesem Kapitel wurden die sich hieraus ergebenden Konsequenzen und Techniken vorgestellt und Möglichkeiten aufzeigt, diese zu beherrschen. Das DJCacheDocument ist schließlich eine eigenständige Implementierung eines Dokuments, das in seinem Verhalten dem lotus.domino.Document sehr ähnlich ist, so dass es an dessen Stelle zum Einsatz kommen kann und gleichzeitig als Hauptspeicher-basierter Container für Document-Daten geeignet ist. In Multithreading-Umgebungen sind besondere Regeln zu beachten, wenn mit Domino-Java-Objekten gearbeitet wird. Objekte dürfen in der Regel nicht oder nur unter bestimmten Voraussetzungen zwischen Threads geteilt werden. Beim Multithreading ist in jedem Fall das Recycling 98
In der aktuellen Version 7.0 arbeitete Domino leider auf dem vorliegenden Testsystem unter Windows nicht sauber mit solchen Datenquellen (DB2 8.1.9) zusammen und gab einen SQL-Fehler in der Dominokonsole (SQL0428N Die SQL-Anweisung ist nur als erste Anweisung in einer Arbeitseinheit zulässig) zurück, während die zusammengeschlossenen Datenbanken bei einer reinen Verwendung in DB2 fehlerfrei arbeiteteten, so dass zumindest der Verknüpfung von DAV- und SQL-Daten bei der Verwendung in DB2 nichts im Wege stand.
13.5 Zusammenfassung
567
ein besonderes Thema, da dieses an die Domino-Session gebunden ist. Die DominoSession kann entweder zwischen den Java Threads geteilt werden oder jeder Thread baut eine eigene Domino-Session auf. Neben den klassischen Document Containern in einer Domino-NSF-Datenbank gibt es seit Domino Version 7 die Möglichkeit, Domino-Datenbanken in einer DB2Datenbank zu speichern. Dieses so genannte NSFDB2-Format ermöglicht es über DB2 Access Views, Domino-Daten in DB2 verfügbar zu machen und umgekehrt über Query Views DB2Daten in Domino zur Verfügung zu stellen. Der Einsatz von DB2 erfordert vom Domino-Programmierer neue Kenntnisse nicht nur in der SQL-Programmierung, sondern auch in der Administration von DB2. Hierbei ist neben dem Setup des Zusammenspiels von Domino-und DB2 auch die Verwaltung der Berechtigungen und die Abstimmung derselben zwischen beiden Systemen ein Thema, mit dem sich der Java-Programmierer auseinandersetzen muss. Der Einsatz von DB2 sprengt aber einige Schranken der bisherigen Domino-Datenbanken. Hierzu gehören: Die datenbankübergreifende Darstellung von Daten, die dynamische, durch eine Benutzerinteraktion gesteuerte Selektion von Dokumenten in einer Ansicht und nicht zuletzt der effiziente und nahtlos integrierte Zugriff auf Enterprise-Daten aus RDBMS-Datenbanksystemen.
14
14 Recyling von Domino-Objekten
In diesem Kapitel: Gründe für das Recycling Die wichtigsten Regeln Besonderheiten Unterschiede zwischen den Domino-Versionen beim Recycling
570
14 Recyling von Domino-Objekten
Domino-Java-Objekte konfrontieren den Java-Programmierer mit einer Notwendigkeit, die für Java scheinbar nicht existiert. Das Recycling von Java-Objekten. Die Notwendigkeit, Domino-Java-Objekte durch das Recycling freizugeben, und viele Anwendungsfälle finden sich in sämtlichen Beispielen dieses Buches. Dieses Kapitel wird nun diese Beispiele und Regeln zusammenfassen und einige erweiternde Informationen beisteuern. Beim Recycling in Domino ist zu beachten, dass dieser Bereich durchaus der Überarbeitung unterliegen kann. Insbesondere von Domino R5 nach R6 sind hier viele Verbesserungen erfolgt, die in R7 erneut überarbeitet wurden. Die folgenden Betrachtungen beziehen sich vor allem auf Domino R6.x, sind aber weiterhin für R7 gültig. Bei Abweichungen wird darauf hingewiesen.
14.1
Notwendigkeit des Recyclings
Die Domino-Objekte und Klassen sind weitestgehend in C++ implementiert und stehen plattformübergreifend als entsprechende Bibliotheken zur Verfügung. Um ein Java-API mit vollem Funktionsumfang nahtlos implementieren zu können, wurde von IBM der Weg gewählt, in Java lediglich Wrapper um die C++ Klassen zu implementieren und dort mittels JNI und entsprechenden native-Verweisen auf C++ zurückzugreifen. In Java wird ein Objekt, auf das keine Referenz mehr verweist, von der Garbage Collection erfasst und freigegeben. Die Java Garbage Collection weiß nun nichts von den dahinterliegenden, eigentlichen Objekten in C++. Daher würden bei einem turnusmäßigen Recycling durch die Garbage Collection diese C++ Objekte nicht erreicht und freigegeben. Daher ist es notwendig, dass der Programmierer sämtliche Domino-Java-Objekte, die er erzeugt, auch wieder freigibt. Die Methode recycle() existiert für jedes Domino-Java-Objekt. Die Ausführung dieser Methode markiert ein Objekt für das Recycling. Ein nachfolgender Zu-
Recycling von Domino-Java-Objekten Session, Database, View, DocumentCollection, ViewEntryCollection, EmbeddedObject, ViewEntry und Document sind speicherhungrige Domino-Objekte und müssen immer recycelt werden Domino-Objekte müssen recycelt werden, ehe sie dereferenziert werden Achtung in Loops Domino-Objekte werden von demjenigen recycelt, der sie erstellt hat Das Recycling sollte immer in einem finally-Block stattfinden
14.2 Grundregeln des Recyclings
571
griff auf das Objekt ist ungültig und kann zu unerwarteten Ergebnissen führen. Die Ausführung von recycle ist nicht (!) gleichbedeutend damit, ein Objekt auf null zu setzen. So einfach ist das. Und so kompliziert. Nicht in jedem Fall ist es ohne größeren Aufwand möglich, jederzeit sämtliche Domino-Objekte so lange im Focus zu behalten, bis sie endgültig nicht mehr gebraucht werden, um sie dann freigeben zu können. Nicht umsonst wurde in Java die Garbage Collection eingeführt. Dennoch ist es möglich, wenn einige Grundregeln eingehalten werden, effizienten Code, der vor allem frei von Speicherleaks ist, zu programmieren. Die Freigabe der Objekte erfolgt, sofern möglich, immer in einem finallyBlock, um sicherzustellen, dass das Recycling in jedem Fall, auch bei Exceptions und unerwarteten Abbrüchen, durchgeführt wird.
14.2
Grundregeln des Recyclings
§1 Das Recycling erfolgt auf der Basis und innerhalb einer Domino-Session. §2 Recyceln Sie alle Domino-Java-Objekte. §3 Recyceln Sie Domino-Java-Objekte immer, aber nur, wenn sie nicht mehr gebraucht werden. §4 Recyceln Sie Domino-Java-Objekte nur, wenn kein anderer sie mehr braucht. §5 Jeder, der ein Domino-Java-Objekt erzeugt oder bezieht, ist für das Recycling zuständig. §6 Recyceln Sie kein Domino-Java-Objekt, das als Parameter erhalten wurde. §7 Recyceln Sie Domino-Java-Objekte, bevor die Referenz auf sie verloren geht. §8 Recyceln Sie Dokumente in zu durchlaufenden Dokumentkollektionen oder Views. §9 Recyceln Sie Domino-Java-Objekte in einem finally-Block. §10 Objekte, die recyclingpflichtige Felder enthalten, benötigen selbst ein recycle(). §1 Das Recycling erfolgt auf der Basis und innerhalb einer Domino-Session Domino-Objekte werden direkt oder indirekt über die Domino-Session bezogen. Folglich erfolgt auch das Recycling im Kontext der Session. In der Theorie werden in einem Domino-Java-Agenten (s. Kap. 4.10) beim Recycling eines Objektes alle Objekte, die aus diesem erzeugt wurden, ebenfalls recycelt. Daher genügte es theoretisch, lediglich die Session zu recyceln – diese muss in jedem Fall (nach Gebrauch) immer recycelt werden. In der Praxis hat es sich bewährt, alle – insbesondere die großen, speicherintensiven – Objekte jeweils explizit zu recyceln. Für alleinstehende DominoJava-Anwendungen (s. Kap. 4.12) ist dies immer Pflicht. §2 Recyceln Sie alle Domino-Java-Objekte Session, Database, View, DocumentCollection, ViewEntryCollection, EmbeddedObject, ViewEntry und Document sind speicherhungrige Domino-Ob-
572
14 Recyling von Domino-Objekten
jekte, genauer gesagt sind es deren C++ Backendrepräsentationen. Daher macht es keinen Sinn, diese bis zum Ende der Session im Hauptspeicher zu lassen, neben dem langfristigen Speicherverbrauch entsteht sonst auch ein Verwaltungsüberhang für viele ungenutzte Objekte. Gleichzeitig besteht die Gefahr, durch verlorene Referenzen, die unter bestimmten Umständen entstehen könnten, Speicherleaks zu verursachen. Daher gilt die Regel (mit wenigen Ausnahmen), alle Domino-Java-Objekte, die vom Programmierer selbst erzeugt wurden, zu recyceln. Die eingangs genannten Objekte sind gleichzeitig diejenigen, bei denen das Recycling auf keinen Fall ausbleiben darf. Kleine Domino-Java-Objekte, insbesondere das Item, sollten nicht recycelt werden. Sie werden bereits beim Recycling des Document mit freigegeben99. §3 Recyceln Sie Domino-Java-Objekte immer, aber nur, wenn sie nicht mehr gebraucht werden Wird für ein Domino-Objekt die Methode recycle () aufgerufen, so ist dieses Objekt für das Recycling markiert. Dies bedeutet nicht, dass eine Garantie dafür besteht, dass das Objekt sofort aufgeräumt wäre. Daher ist eine strenge Kontrolle, wann ein Objekt dem Recycling zugeführt wird, notwendig. Nach dem Aufruf von recycle () darf ein Domino-Java-Objekt in keinem Fall mehr benutzt werden. Die Ergebnisse und das Verhalten eines solchen Objektes sind nicht vorhersehbar. In der Regel wird eine NotesException (id 4376) „Object has been removed or recycled“ geworfen. Eine Garantie dafür besteht aber nicht (daher s. Kap. 14.4). §4 Recyceln Sie Domino-Java-Objekte nur, wenn kein anderer sie mehr braucht Gemeint sind zwei Fälle. Der erste ist einfach erläutert. Ein Domino-Java-Objekt, das an anderer Stelle später im Code noch verwendet werden soll, darf erst danach recycelt werden. Ein Database-Objekt, das Dokumente enthält, die an anderer Stelle noch verwendet werden, darf nicht recycelt werden: Document doc = someDocument(); Item item = doc.getFirstItem("Form"); doc.recycle(); System.out.println (item.getText());
Das Item item ist Bestandteil des Document doc. Wird doc recycelt, so wird auch item recycelt, d.h. das Document war noch in Verwendung und hätte erst nach der letzten Verwendung von item recycelt werden dürfen. Der zweite Fall ist komplizierter. Domino-Java-Objekte sind nur Referenzen auf interne Objekte. In Domino R6 werden verschiedene Java-Objekte, die auf verschie-
99
In Version 6.5.2 gab es einen Bug, der zur Folge hatte, dass das Recycle eines Items unter bestimmten Bedingungen zur Folge hatte, dass das gesamte Document recycelt wurde.
14.2 Grundregeln des Recyclings
573
dene Weise eine Referenz auf das gleiche (z.B.) Domino-Document erhalten, letztendlich auf ein und dasselbe C++ Objekt verweisen. Folglich wird das Recycle des einen Java-Objekts Auswirkungen auf das andere haben: Document doc1 = db.getAllDocuments().getFirstDocument(); String aDocumentID = doc1.getUniversalID(); Document doc2 = db.getDocumentByUNID(aDocumentID); doc1.recycle(); System.out.println("Dokument " + doc2.getUniversalID() + " erfolgreich geladen.");
Die Anweisung System.out.println wird nie erfolgen, da beim Versuch, auf doc2 zuzugreifen, eine NotesException „Object has been removed or recycled“ geworfen wird. Auch wenn doc2 eigentlich ein eigenständiges (Java-) Objekt ist und dazu noch unabhängig von doc1 erzeugt wurde (doc1 verweist auf ein Element in einer DocumentCollection , während doc2 direkt auf ein Element einer Database zeigt ), verweisen beide auf ein und dasselbe C++ Objekt. Dieses Verhalten ist insofern sauber, als offensichtlich innerhalb einer DominoSession ein und dasselbe Datenbankobjekt ebenfalls durch ein und dasselbe Speicherobjekt repräsentiert wird. Der Bezug auf eine Session ist wichtig, da natürlich unterschiedliche Benutzer mit unterschiedlichen Rechten auf die Datenbank zugreifen und sich daher auch ein Speicherobjekt nicht teilen können. Das Verhalten war für Domino R5 noch anders. Dort musste doc2 zusätzlich recycelt werden. Wären die Zeilen und vertauscht, wäre der Code selbstverständlich in Ordnung. doc2 würde erneut in den Hauptspeicher geladen, müsste dann allerdings nachträglich ebenfalls dem Recycling zugeführt werden. §5 Jeder, der ein Domino-Java-Objekt erzeugt oder bezieht, ist für das Recycling zuständig Um den Überblick zu behalten, wer welches Objekt dem Recycling zuführen muss, ist es wichtig hier klare Hierarchien einzuhalten. Die Formulierung „Erzeugen oder Beziehen eines Domino-Java-Objektes“ meint Folgendes: Wenn im Code durch Benutzung der API-Funktionen z.B. ein View oder eine Database, aber auch eine DocumentCollection oder ein Document geladen wird, so ist hiermit der recyclingpflichtige Bezug eines Domino-Java-Objektes gemeint. Beispiele hierfür sind: Database db = session .getDatabase(session.getServerName(),"djbuch/djbuch.nsf"); View view = db.getView("V_meineAnsicht"); DocumentCollection dc = view.getAllDocumentsByKey("myKey"); Document doc1 db .getDocumentByID("10796010FBE23C5DC125704F005866F9"); Document doc2 = dc.getFirstDocument();
574
14 Recyling von Domino-Objekten
Alle diese Anweisungen haben recyclingpflichtige Objekte als Ergebnis, da diese Objekte erst ausdrücklich durch Aufruf der Methoden aus der Datenbank geladen und in neu erzeugten, korrespondierenden C++ Speicherobjekten vorgehalten werden. Gleiches gilt für das Erzeugen von neuen Objekten: View view2 = db.createView(); Database db2 = dbDir .createDatabase("djbuch/neuesbeispiel.nsf");
Objekte jedoch, die als Property eines anderen bezogen werden, sind nicht recyclingpflichtig. Solche Instanzvariablen müssen vom Objekt selbst, bzw. von Dritten, die für diese Objekte verantwortlich sind, verwaltet und dem Recycling zugeführt werden: Database db3 = doc.getParentDatabase(); Session session2 = db3.getParent(); Document doc2 = myObject.getContainedDocument(); DocumentCollection dc = db.getAllDocuments();
In diesen Beispielen verweist db3 auf die Datenbank, aus der das Dokument doc geladen wurde. Je nachdem, wer diese Datenbank ursprünglich bezogen hat, muss diese auch wieder recyceln. Gleiches gilt für session2, die ja durch getParent() nicht neu erzeugt oder geladen wird, sondern es handelt sich lediglich um einen Verweis auf die Session, innerhalb derer db3 geöffnet worden ist. In beiden Fällen sind Dritte für das Recycling verantwortlich – auch wenn das natürlich in der Regel der eigene Code an anderer Stelle ist. Anders verhält es sich im dritten Beispiel. myObject besitzt die Methode getContainedDocument, die ein von myObject im (z.B.) Konstruktor neu erzeugten Document zurückgibt. In diesem Fall ist myObject und damit derjenige, der myObject erzeugt hat, für das Recycling zuständig. §6 Recyceln Sie kein Domino-Java-Objekt, das als Parameter erhalten wurde Wird ein Domino-Java-Objekt als Parameter in eine Methode gegeben, ist die Methode nicht selbst für das Recycling zuständig, sondern derjenige, der den übergebenen Parameter erzeugt hat: Document doc = db.getDocumentByID("10796010FBE23C5DC125704F005866F9"); System.out.println(getSomeStringFromDoc (doc)); doc.recycle();
Die Methode getSomeStringFromDoc darf in keinem Fall selbst das Recycling übernehmen, da sonst externe Referenzen „zerstört“ würden. Stattdessen wird außerhalb, in der dritten Zeile dieses Beispiels, das Recycling angestoßen.
14.2 Grundregeln des Recyclings
575
§7 Recyceln Sie Domino-Java-Objekte, bevor die Referenz auf sie verloren geht In Java ist es gängige Praxis, sich darauf zu verlassen, dass Objekte, deren Referenz freigegeben (auf null gesetzt) wird, automatisch von der Garbage Collection aufgeräumt werden. Code der folgenden Art ist in Java eigentlich unkritisch, bei der Verwendung von Domino-Java-Objekten jedoch höchst problematisch: Document doc = getSomeDocument (); doSomeThingWithDoc (doc); doc = getOtherDocument(); doSomeThingWithDoc (doc);
Durch die neuerliche Zuweisung von doc geht die Referenz auf das ursprüngliche Speicherobjekt verloren. Ein Speicherleak ist entstanden. Vor der neuerlichen Zuweisung von doc hätte ein doc.recycle() erfolgen müssen. Bei Verkettungen von Anweisungen ist für jeden Einzelfall zu klären, ob hierdurch möglicherweise Referenzen auf recyclingpflichtige Objekte verloren gehen könnten. Die Anweisung Document doc1 = db.getAllDocuments().getFirstDocument(); beinhaltet zwar eine solche Verkettung mit der DocumentCollection getAllDocuments(). Allerdings ist diese DocumentCollection nicht recyclingpflichtig100. Wäre sie es, würde durch diesen Aufruf eine DocumentCollection erzeugt, deren Referenz sofort mit der Zuweisung von doc1 wieder verloren geht. Dann müsste diese Collection zunächst in ein separates Objekt geladen werden, um danach wieder recycelt werden zu können. Dies ist der Fall in folgendem Beispiel: System.out.println (session.getDatabase (session.getServerName(),"djbuch/djbuch.nsf").getTitle());
Die Referenz auf die Datenbank djbuch.nsf ist mit Ausgabe des Titels der Datenbank verloren. Hier hilft nur eine vorangestellte Zuweisung: Database db = session.getDatabase (session.getServerName(),"djbuch/djbuch.nsf"); System.out.println (db.getTitle()); db.recycle(); 100
Für einige Methoden ist unklar, ob nun neue, recyclingpflichtige Objekte zurückgegeben werden, oder ob dies intern, dauerhaft vorgehaltene Objekte sind, die nicht recycelt werden dürfen, zumal dieser Bereich auch regelmäßigen Code-Überarbeitungen unterliegt. Da die Collection getAllDocuments() als Property (Eigenschaft) der Datenbank angesehen werden kann, sollte aber auf das Recycling verzichtet werden (und es führt bei der Verwendung von DIIOP sogar zum Fehlerfall). Als Faustregel kann immer die Frage nach der Property erfolgen. Wurde ein „vorgefertigtes“ Objekt, also eine „Eigenschaft“, zurückgegeben, sollte kein Recycling erfolgen, wurde ein neues Objekt erstellt, ist es Pflicht.
576
14 Recyling von Domino-Objekten
Wobei natürlich eine Database in der Regel nicht nur für die Anzeige ihres Titels geladen wird. In der Regel wird solch eine Referenz zumindest für einen Großteil der Laufzeit einer Anwendung verfügbar gehalten, schließlich wird sie in der Regel an mehr als einer Stelle benötigt (§4). §8 Recyceln Sie Dokumente in zu durchlaufenden Dokumentkollektionen oder Views Der im vorigen Paragraphen angesprochene Punkt kommt insbesondere zum Tragen, wenn eine DocumentCollection oder ViewEntryCollection durchschritten wird. Mit jeder Zuweisung der Art doc=documentCollection.getNextDocument() geht die Referenz auf das vorherige Speicherobjekt verloren. Bei großen DocumentCollections können so erhebliche Speicherleaks erzeugt werden. Folgender Code löst dieses Problem: Database otherDb = null; DocumentCollection docCollection = null; Document thisDoc = null, nextDoc = null; int count = 1, size=0; try { docCollection = otherDb.getAllDocuments(); size= docCollection.getCount(); thisDoc = docCollection.getFirstDocument(); while (thisDoc != null) { System.out.println("Dokument " + count++ + " / " + size + ":\t" + thisDoc.getUniversalID()); nextDoc = docCollection.getNextDocument(); GC.recycle(thisDoc); thisDoc = nextDoc; } } finally { GC.recycle (thisDoc); GC.recycle (nextDoc); GC.recycle (docCollection); GC.recycle (otherDb); }
Durch die Zuhilfenahme eines zusätzlichen JavaObjekts kann eine Referenz auf das jeweils nächste Document eines Loops geladen werden. Nun kann das aktuelle Document dem Recycling zugeführt werden und die Schleifenvariable auf das zuvor bezogene, nächste Document verweisen. Abschließend werden alle Objekte im finally-Block dem Recycling zugeführt, so ist sichergestellt, dass auch in Ausnahmefällen ein Recycle erfolgt. Wichtig ist hierbei, das Recycling von den kleinen Objekten beginnend durchzuführen (erst das Document, dann die DocumentCollection, zuletzt die Database) . Gleiches gilt auch z.B. für ViewEntryCollection oder für Schleifen über einen View. Eine sehr elegante Lösung dieses Problems wird in Kapitel 10.9 vorgestellt.
14.2 Grundregeln des Recyclings
577
§9 Recyceln Sie Domino-Java-Objekte in einem finally-Block Wie im vorigen Paragraphen angesprochen: Das recycle erfolgt in einem finally-Block. Für die Schleifenvariablen thisDoc und nextDoc gilt dies nicht für das recycle innerhalb der Schleife . Die aktuelle Referenz von thisDoc und nextDoc würde in einem Ausnahmefall dennoch im finally erfasst. Der finally-Block wird immer durchlaufen, egal wie der Programmfluss unterbrochen wird. Selbst bei einer ThreadDeathException würde er noch abgearbeitet. Aus Java-Sicht hat diese Konstruktion durchaus auch Nachteile. Nicht nur, dass die Übersichtlichkeit leidet und sich die Fehleranfälligkeit erhöht, wenn Objekte außerhalb des Scope zur Verfügung stehen, in dem sie benötigt werden, wie die Objekte thisDoc, nextDoc, docCollection und otherDb, die ganz am Anfang des Codes definiert werden, sondern sie belegen daher für eine unnötig lange Zeit Speicher im JavaHeap. BestPractice in Java ist es (eigentlich), Objekte so nah an dem Context zu deklarieren, in dem sie verwendet werden, wie es geht. Aus den vorgenannten Gründen gibt es hierfür jedoch für die beschriebene Vorgehensweise leider keine Alternative. §10 Objekte, die recyclingpflichtige Felder enthalten, benötigen selbst ein recycle()
Wenn eigene Objekte definiert werden, die selbst Domino-Java-Objekte erzeugen oder beziehen, muss eine externe Möglichkeit geschaffen werden, um diese Objekte recyceln zu können. Ein einfacher Weg ist es, diesen Objekten selbst eine recycle Methode mitzugeben, um die Recyclingpflicht nach außen erkenntlich und zugänglich zu machen. Listing 14-1 zeigt ein einfaches Beispiel für verschiedene Situationen, die in diesem Zusammenhang entstehen können. Das Beispiel implementiert auf Basis einer Domino-Database einen kleinen Spielzeugladen mit der Klasse ToyStore. Beim Initialisieren wird dem ToyStore-Objekt der Name einer Datenbank mitgegeben, in der der Laden abgespeichert werden soll. Dieser Name wird intern in ToyStore verwendet, um eine Domino-Datenbank zu öffnen und als Instanzvariable vorzuhalten. Hierin begründet sich nun die Recyclingpflicht dieses-Objekts, denn diese Instanzvariable wurde erst innerhalb des ToyStore-Objektes erzeugt, so dass bei der Freigabe einer Referenz auf ein ToyStore-Objekt sichergestellt werden muss, dass ein recycle für die Instanzvariable mit der Database erfolgt. Zunächst soll die Klasse ToyStoreDemo näher betrachtet werden, die den ToyStore zum Leben erweckt. Ein neues ToyStore-Objekt wird mit dem Namen einer Datenbank und einer Domino-Session erzeugt. Mit der Methode addToy können Paare aus Artikelnummer und Name dem Store hinzugefügt werden . Um diese Objekte laden zu können, bietet die Klasse zwei verschiedene Methoden. getToy liefert lediglich den Namen eines Spielzeugs zu einer ID, wogegen getToyDocument das gesamte Domino-Document zurückgibt, in dem es gespeichert ist. Die removeToy Anweisungen dienen lediglich dazu, bei der Ausführung des Beispiels nicht unnötigen „Datenmüll“ zu hinterlassen.
578
14 Recyling von Domino-Objekten
package djbuch.kapitel_14; import djbuch.kapitel_06.GC; import lotus.domino.*; public class ToyStore { private Database db = null; private Session session = null; private static final String ITEM_ID = "F_ID"; public static final String ITEM_NAME = "F_NAME"; public static final String ITEM_FORM = "FORM"; public static final String DEFAULT_FORM = "TOYFORM"; private Vector trash = new Vector(); public ToyStore (String dbName, Session session) throws NotesException { db = session.getDatabase(session.getServerName(),dbName); if (db==null || !db.isOpen()) { throw new NotesException (999, "Konnte " + dbName + " nicht öffnen."); } this.session = session; } public String getToy (int id) throws NotesException { Document doc = null; DocumentCollection dc = null; try { dc = db.search(ITEM_ID + "=" + id); if (dc==null || dc.getCount()==0) { return null; } doc = dc.getFirstDocument(); return (doc==null?null:doc.getItemValueString(ITEM_NAME)); } finally { GC.recycle (doc); GC.recycle (dc); } } public Document getToyDocument (int id) throws NotesException { DocumentCollection dc = null; try { dc = db.search(ITEM_ID + "=" + id); if (dc==null || dc.getCount()==0) { return null; } Document doc = dc.getFirstDocument(); return doc; } finally { trash.add(dc); } } public void addToy (int id, String name) throws NotesException { Document doc = null; try { doc = db.createDocument(); doc.replaceItemValue (ITEM_ID, new Integer (id)); doc.replaceItemValue (ITEM_NAME, name); doc.replaceItemValue (ITEM_FORM, DEFAULT_FORM); doc.save(); } finally { GC.recycle (doc); } } public void removeToy (int id) throws NotesException { Document doc = null; try { doc = this.getToyDocument(id); if (doc!=null) {
Listing 14-1 Recycling von Objekten mit recyclingpflichtigen internen Domino-Objekten
Nun zum Recycling. Da die Klasse ToyStore anhand des im Konstruktor mitgegebenen Datenbanknamens eigens ein Datenbank-Objekt erzeugt , entsteht eine recyclingpflichtige Instanzvariable db .
580
14 Recyling von Domino-Objekten
Um diesem Sachverhalt Rechnung zu tragen, wurde kurzerhand der Klasse eine hinzugefügt, die bei einem Aufruf die interne Daeigene recycle()-Methode tenbank recycelt. Folglich wird ein ToyStore-Objekt äquivalent zu einem anderen Domino-Java-Objekt dem Recycling zugeführt. Wie gewohnt wird es zunächst außerhalb eines try-catch-Blockes deklariert und nach Verwendung im finallyBlock wieder freigegeben . Beachten Sie, dass die im Konstruktor von ToyStore übergebene Session zwar ebenfalls als Instanzvariable geführt wird, aber nicht durch den recycle() von ToyStore erfasst wird. Schließlich handelt es sich hier nur um einen übergebenen Parameter und §6 kommt zur Anwendung. Neben der Behandlung dieser internen, recyclingpflichtigen Database erläutert der ToyStore noch weitere Recycling-Situationen. Die Methoden addToy und removeToy verwenden zwar intern ein Document-Objekt, das aber in beiden Fällen nach der Beendigung dieser Methoden nicht mehr benötigt wird. Also wird nach getaner Arbeit (save bzw. remove ) in beiden Fällen das Document doc wieder dem Recycling zugeführt – auch nachdem das Dokument aus der Datenbank gelöscht wurde, muss noch dessen Speicherobjekt recycelt werden. Der Benutzer dieser Methoden muss sich nicht mit dem Recycling auseinandersetzen. Recyclingpflichtige Objekte wurden gleich dort, wo sie entstanden, und sobald sie nicht mehr gebraucht wurden, wieder freigegeben. Ähnlich gelagert ist der Fall bei getToy . Auch diese Methode verlangt weder als Parameter ein Domino-Java-Objekt, noch wird ein solches zurückgegeben. Sämtliche vorübergehend für den Ladevorgang benötigten Domino-Objekte werden im finally-Block recycelt . Wie praktisch der Einsatz des finally-Blockes ist, wird hier besonders deutlich. Es kann bedenkenlos die Methode mit einem return verlassen werden, in jedem Fall wird die Programmflusssteuerung von Java dafür sorgen, dass dieser Block zum Schluss durchlaufen wird. Dass dies tatsächlich erst am Ende nach Abschluss aller Programmschritte der Methode erfolgt, wird deutlich bei der Rückgabe des Ergebnisstrings, der ja noch innerhalb des return-Statements auf das Document zugreift. Der Vollständigkeit halber sei darauf hingewiesen, dass diese Methode zwei erhebliche Vereinfachungen macht. Die Methode db.search () ist zwar sehr einfach zu handhaben und benötigt neben der Datenbank keine weiteren Objekte, sollte aber, da hier über die IDs eindeutige Schlüssel vorliegen, durch getDocumentByKey() in View ersetzt werden. Außerdem wurde in addToy darauf verzichtet, die Eindeutigkeit der Schlüssel sicherzustellen, daher wird in getToy einfach der erste gefundene Treffer zurückgegeben . Eine andere Situation findet sich in der Methode getToyDocument, die ein Domino-Document zurückgibt. Selbstverständlich darf dieses Document nicht innerhalb der Methode recycelt werden. Ganz im Gegenteil. Dies ist eine typische Methode, die recyclingpflichtige Objekte zurückgibt, d.h. Objekte, die durch den Aufruf dieser Methode erst erzeugt wurden (§5). Folglich muss externer Code, der getToyDocument verwendet, selbst dafür sorgen, dass das zurückgegebene Document ein recycle erfährt. Unangenehm ist die für getToyDocument benötigte interne DocumentCollection. Die DocumentCollection enthält das zurückgegebene Document und kann
14.2 Grundregeln des Recyclings
581
nicht dem Recycling übergeben werden. Stattdessen wird sie im Vector trash hinzugefügt und kann so im recycle von ToyStore entsorgt werden. Diese Technik hat einige Nachteile (s. Kap. 14.3.1), insbesondere, da ToyStore nicht wissen kann, wann die zurückgegebenen Dokumente nicht mehr benötigt werden und daher für jeden Aufruf von getToyDocument eine DocumentCollection zwischengespeichert werden muss. Ein echter Speicherfresser. In Kapitel 15 werden andere Suchtechniken vorgestellt, die helfen werden, dies zu vermeiden. Eine Alternative wäre, an Stelle eines Document einfach die DocumentCollection zurückzugeben, dann könnte der externe Code, der weiß, wie lange er das Document benötigt, sich um das Recycling kümmern. Sie sehen, manchmal ist das Recycling nicht trivial. Die Klasse ToyStore kann noch eine weitere Verbesserung erfahren. Wurde erst einmal die Methode recycle in ToyStore aufgerufen, so wird jeder Aufruf einer der Methoden dieser Klasse ungewisse Ergebnisse liefern, da der Kern, die Database, bereits recycelt ist. Daher ist es sinnvoll, nachzuhalten, ob das ToyStore-Objekt recycelt wurde, und im Falle einer Benutzung nach einem recycle eine entsprechende Exception zu werfen. Hierfür wurde zunächst die Methode recycle modifiziert und eine zusätzliche Methode checkRecycled eingeführt: public void recycle () { try { if (trash != null && trash.size() > 0) {session.recycle(trash);} } catch (NotesException e) {e.printStackTrace();} db = GC.recycle(db); } private void checkRecycled () throws NotesException { if (db==null) { throw new NotesException(NotesError.NOTES_ERR_DELETED, "Object has been removed or recycled"); } }
Die Methoden in GC (s. Kap. 14.4) wurden so implementiert, dass sie immer null zurückgeben, um schnell und einfach Objekte nach dem recycle auf null setzen zu können. Ist db erst einmal auf null gesetzt, kann sehr einfach erkannt werden , ob es noch gültig ist, und eine entsprechende Exception geworfen werden. Hierbei wird als ID der NotesException die von Domino ohnehin für diesen Fall vorgesehene Fehlernummer NotesError.NOTES_ERR_DELETED (4376) verwendet . Nun muss nur noch in jede Methode der Aufruf von checkRecycled als erste Zeile eingefügt werden und jede Benutzung eines ToyStore-Objektes nach Aufruf von recycle wird eine entsprechende Exception zur Folge haben:
Das Recycling der Domino-Java-Objekte ist regelmäßig Gegenstand von Überarbeitungen von Domino-Versionen. Sollten hier Unklarheiten bestehen, ist es ratsam, zunächst nochmals den gesamten Code auf fehlendes Recycling zu überprüfen. Als Faustregel sollten mindestens die in Kapitel 14.2 §2 genannten Objekte recycelt werden. Die gravierendsten Unterschiede zwischen verschiedenen Versionen oder Umgebungen finden sich beim Recycling zwischen Domino R5 und R6 und je nachdem, ob lokale Domino-Sessions oder DIIOP verwendet wird.
14.3.1
Recycle (Vector)
Theoretisch bietet Domino die Möglichkeit, über recycle (Vector) (diese Methode steht allen Domino-Objekten zur Verfügung), andere Domino-Objekte, die der Vector referenziert, dem Recycling zu übergeben. Anstatt dann in einer Schleife, wie im Beispiel zu §8 in Kapitel 14.2, das jeweilige Document gleich freizugeben, würden dann durch ein vector.add (doc) die Referenzen eingesammelt und abschließend durch ein session.recycle(vector) freigegeben. Diese Vorgehensweise hat in Theorie und Praxis einen leichten Performance-Vorteil beim Einsatz von DIIOP. In Tests konnten 5% Laufzeit eingespart werden, wobei das gleiche Szenario bei einer lokalen Session ohne DIIOP rund 20% längere Laufzeiten erforderte, so dass diese Vorgehensweise für lokale Domino-Sessions nicht anzuraten ist. Sieht man sich allerdings den Speicherverbrauch in einer DIIOP-Anwendung an, bei dem Versuch mehrere Hundert Document-Objekte in einer Schleife in einem Vector zwischenzuspeichern, um sie erst später zu recyceln, kann auch hier nur abgeraten werden. Abb. 14-1 und Abb. 14-2 zeigen eindrucksvoll das unterschiedliche Verhalten des Java Heap. Absolut gesehen wurde im zweiten Beispiel rund die zwanzigfache Menge an Java-Hauptspeicher belegt, der natürlich abschließend durch die Java Garbage Collection wieder freigegeben wird. Dieser Speicherverbrauch ist nicht zu verwechseln mit dem Hauptspeicher, der für die Domino C++ Objekte belegt wird (beim Einsatz von DIIOP liegen diese schließlich auf dem Server und die Grafiken zeigen den Speicherverbrauch der lokalen Java-Anwendung), sondern es handelt sich hier um den Java Heap, der für die Java-Wrapper (und IIOP-Cache-Objekte) benötigt wird. Dennoch ist diese Vorgehensweise aufgrund der Größe des temporär benötig-
14.3.2 Unterschiede zwischen R5 und R6/7
583
Abb. 14-1 Java-Speicherverbrauch, bei sofortigem Recycling
Abb. 14-2 Java-Speicherverbrauch bei nachgelagertem Recycling
ten Java Heaps nicht akzeptabel. Zusätzlich geben die Grafiken eine anschauliche Vorstellung davon, wie gravierend das Speicherleak ausfallen würde, wenn überhaupt kein Recycle erfolgte.
14.3.2
Unterschiede zwischen R5 und R6/7
Der wesentliche Unterschied zwischen Domino R5 und Domino R6 / R7 beim Recycling liegt im unterschiedlichen Umgang bei Verweisen von unterschiedlichen JavaObjekten auf ein und dasselbe Domino-Objekt.
584
14 Recyling von Domino-Objekten
Während seit Domino R6 sehr durchgängig dafür gesorgt wird, dass ein und dasselbe Domino-Objekt immer auch durch ein und dasselbe Speicherobjekt repräsentiert wird, konnte davon in R5 nicht ausgegangen werden. View thisView = otherDb.getView("V_dokumente_k6"); View otherView = otherDb.getView("V_dokumente_k6"); thisView .recycle(); System.out.println(otherView.getName());
Da thisView und otherView auf ein und dasselbe Domino-Objekt (die Ansicht V_dokumente_k6) verweisen, wird das recycle() von thisView ein recycle von otherView zur Folge haben. In Domino R6 / R7 hätte eine nachfolgende Verwendung von otherView eine NotesException zur Folge, in Domino R5 hätte otherView weiterhin verwendet werden können und ebenfalls recycelt werden müssen. Beachten Sie, dass mit dem Verweis auf ein und dasselbe Domino-Objekt nicht gemeint ist, dass thisView und otherView etwa das gleiche Java-Objekt wären. Eine Zuweisung der Art View otherView = otherDb.getView("V_ganzAndereAnsicht");
hätte lediglich zur Folge, dass nun otherView auf eine andere Ansicht verweist, während sich an thisView nichts ändert. Lediglich beim recycle() zweier Java-Objekte, die auf dasselbe Domino-Obejekt verweisen, kommt das beschriebene Verhalten zum Tragen. Dies gilt auch für andere Domino-Objekte, wie Document oder Database. Eine Einschränkung ist hierbei zu machen. Öffnet man Datenbanken per openDatabaseByReplicaID101 in DbDirectory, so besteht diese Beziehung auch in Domino R6 und R7 nicht: dbdir= session.getDbDirectory(null); db = dbdir.openDatabaseByReplicaID("C12570400064011C"); otherDb = dbdir.openDatabaseByReplicaID("C12570400064011C");
Ein recycle von db hat hier kein (!) recycle von otherDb zur Folge. otherDb muss zusätzlich recycelt werden.
14.3.3
Temporäre Dateien
Die Methoden Reader getReader(), InputSource getInputSource() und InputStream getInputStream() in den Klassen Item, RichTextItem und MIMEEntity erzeugen temporäre Dateien, die erst beim Recycling dieser Objekte wieder gelöscht werden. Wird hier das Recycling vergessen, wird man nicht nur mit Memory Leaks kämpfen, sondern es bleiben – je nach Anwendungshäufigkeit – Unmen101
Domino R6.5.x / Domino 7.0
14.4 Vereinfachung – Die DJBuch-GC-Klasse
585
gen von temporären Dateien liegen, was unter Windows erhebliche Probleme verursachen kann, da dessen Dateisystem nicht gut mit Verzeichnissen zurechtkommt, die (sehr) viele Dateien enthalten.
14.4
Vereinfachung – Die DJBuch-GC-Klasse
Bereits seit Kapitel 6 wird zur vereinfachten Darstellung in den Beispielen die Klasse GC verwendet, ohne näher darauf einzugehen. Sie erfüllt folgende Ziele und Anforderungen: 1
2
3
Der Aufruf von recycle soll keine Exception werfen. Die Methode recycle in den Domino-Java-Objekten kann eine NotesException werfen. Der Aufruf von recycle erfolgt in der Regel im finally-Block. Da dieser – das liegt in der Natur des finally-Blocks – sich außerhalb des try-catch befindet, würde die Verwendung von recycle einen zusätzlichen try-catchBlock innerhalb des finally erfordern, bzw. das Code-Objekt selbst müsste eine NotesException werfen. GC soll statische recycle-Methoden zur Verfügung stellen, die die NotesException intern abfangen. Das Verhalten eines Objektes soll nach dem recycle definiert sein. Wurde für ein Domino-Objekt recycle aufgerufen, ist dessen Verhalten normalerweise nicht mehr definiert. Die recycle-Methoden in GC sollen hierfür Abhilfe schaffen. Die Methoden dieser Klasse sollen robust gegen null-Parameter sein.
Die Klasse GC (Listing 14-2) enthält für jede Domino-Objekt-Klasse eine statische Methode recycle. Jede nimmt jeweils ein Domino-Objekt als Parameter . Der Parameter wird auf null überprüft und im Falle, dass er nicht gleich null ist, wird für ihn die Methode recycle() aufgerufen. Es ist wichtig, dass auch GC.recycle(null) aufgerufen werden kann, da aufgrund der typischen Deklaration der Domino-Objekte außerhalb des try-catchBlockes im finally nicht immer davon ausgegangen werden kann, dass die Objekte bereits zugewiesen wurden:
586
14 Recyling von Domino-Objekten
package djbuch.kapitel_06; import djbuch.kapitel_13.DJDocument; import lotus.domino.*; public class GC { public static Session recycle(Session session) { try { if (session != null){ session.recycle(); } } catch (NotesException notesexception) { throw new RuntimeException ("Fatal: Notes Exception while recycling Session."); } return null; } public static Document recycle(Document document) { try { if (document != null) { document.recycle(); } } catch (NotesException notesexception) { throw new RuntimeException ("Fatal Error while recycling Document."); } return null; } ... //usw. private GC() { //hide from Public } }
Würde bei der Initialisierung der Session oder der Datenbank ein Fehler auftreten, kann es beim Recycling des Document oder der Database vorkommen, dass diese Objekte noch nicht zugewiesen wurden und daher noch den Wert null haben. Daher die Forderung, dass GC.recycle auch null-Parameter zulässt. Innerhalb von GC.recycle wird die potentielle NotesException abgefangen . Tritt hier eine solche Exception auf, wird das durch eine RuntimeException quittiert . Durch die bewusste Verwendung einer unchecked Exception an dieser Stelle müssen die recycle-Methoden in GC nicht selbst in einem try-catch-Block untergebracht werden. Alle recycle-Methoden in GC geben ein null-Objekt des jeweiligen Typs zurück. Dies vereinfacht den Umgang mit Objekten, die bereits recycelt wurden. Die Ausführung von GC.recycle (doc) ruft lediglich das recycle auf. Wird versehentlich später doc benutzt, so ist dessen Verhalten unbestimmt (ein Zugriff könnte also unter bestimmten Umständen ohne Fehler zufällig erfolgreich sein). Soll dieser versehentliche nachfolgende Zugriff verhindert werden, kann über doc= GC.recycle(doc) einfach in einer Codezeile erreicht werden, dass das Objekt doc null ist. Das Verhalten wäre zumindest eindeutig (NullPointerException). Die Klasse GC hält für alle denkbaren Domino-Objekte entsprechende recycleMethoden zur Verfügung .
14.5
Recycling im MultiThreading
In Multithreading-Umgebungen ist darauf zu achten, dass Objekte in dem Thread recycelt werden, in dem sie erzeugt wurden. Sofern sich mehrere Threads eine DominoSession teilen und DIIOP verwendet wird, können Domino-Objekte auch threadübergreifend recycelt werden, wobei dann natürlich darauf zu achten ist, dass in keinem anderen Thread dieses Objekt noch verwendet wird.
14.6
Zusammenfassung
Wenn auch für Java-Programmierer ungewohnt, müssen Domino-Java-Objekte recycelt werden. Beherzigt man einige Grundregeln und setzt die vorgeschlagenen Code Pattern ein, so kann dennoch einfach und sicher mit dem Recycling umgegangen werden. Die wichtigsten Regeln sind das Recycling großer Objekte, das Recycling der Referenzen in Schleifen, die Anwendung von recycle in finally-Blöcken und die Einhaltung der Verantwortung, wer für das Recycling zuständig ist – der Erzeuger – und wann dies erfolgen darf – wenn ein Objekt nicht mehr gebraucht wird.
15
15 Wer suchet, der findet
In diesem Kapitel: Index Lookup über eine oder mehrere Ansichtenspalten Und- und Oderverknüpfungen bei getAllEntriesByKey Volltextsuche und Indizierung Einsatzgebiete der verschiedenen Suchstrategien und deren Optionen Performancevergleich
590
15 Wer suchet, der findet
Datenbanken enthalten Daten und die wollen geladen, gespeichert, gesucht und gefunden werden. Wie jedes andere Datenbanksystem bietet auch Domino verschiedene Mechanismen, die den Programmierer bei der Datenhaltung und Wartung unterstützen. Ungewohnt mag auf den ersten Blick das Fehlen einer SQL-Schnittstelle102 sein, stattdessen tritt bei Domino an Stelle von Tabellen und Ansichten der View (Ansicht), über den Dokumente der Datenbank zusammengefasst, indiziert, selektiert und durchsucht werden. Wurden in den Kapiteln 3.3ff., 7.1.4.1 bis 7.1.4.3 und 10 bis 10.8 Views vorgestellt und deren Signaturen und Methoden einerseits und Techniken zur Benutzung andererseits vorgestellt, wird dieses Kapitel die verschiedenen Techniken, die Domino zur Suche und Selektion von Dokumenten und Inhalten bietet, miteinander vergleichen und ihre jeweilige Philosophie näher erläutern. Neben Ansichten werden in diesem Zusammenhang die @Formel-gesteuerte datenbankweite Suche, die Volltextsuche, aber auch erweiterte Suchkonzepte, wie die Domainsearch und spezielle URL-Parameter zur Suche über eine Browserschnittstelle, erläutert.
15.1
Überblick über die Suchkonzepte
Dokumente oder Dokumentensammlungen (DocumentCollection oder ViewEntryCollection) können in Domino entweder direkt über eine eindeutige ID selektiert, über eine Ansicht anhand eines Schlüssels oder über verschiedene Volltextsuchen geladen werden. Bei der Entscheidung, welches Suchkonzept bei der Programmierung Anwendung finden soll, müssen verschiedene Fragestellungen in Betracht gezogen werden: Neben der Menge der zu durchsuchenden Dokumente und deren Größe spielt natürlich eine Rolle, welche Informationen über das oder die zu wählenden Dokumente bekannt sind bzw. anhand welcher Kriterien Dokumente selektiert werden sollen. Lässt sich ein Datensatz anhand der zumindest teilweisen Übereinstimmung eines oder mehrerer Schlüssel identifizieren, so ist immer eine Suche über eine Ansicht der Weg der Wahl. Durch den Einsatz von Spaltenformeln in der Ansicht kann dieses Konzept sogar sehr flexibel eingesetzt werden und bleibt nicht auf den reinen Einszu-eins-Lookup eines Schlüssels beschränkt. Ist die UniversalID eines Dokuments bekannt, wird man es direkt anhand dieser ID laden. Steht weder die UniversalID noch ein geeigneter View zur Verfügung, so kann durch den Einsatz von @Formeln ein Dokument (oder mehrere) direkt aus der Datenbank selektiert werden, wobei dieser Vorgang mit der dynamischen Generierung von SQL Queries verglichen werden kann. Domino versteht sich als Spezialist in der Verwaltung und Speicherung unstrukturierter Daten, einschließlich großer Binär- und Multimediadaten, wie sie in Kollaborationsanwendungen und Knowledgestores vorkommen. Dementsprechend bietet 102
Bis Ende 2004 bot Lotus einen JDBC-Treiber für Domino an, der mit der Einführung von Domino R7 und NSFDB2 wohl überflüssig geworden ist. Weiterhin verfügbar ist ein ODBC-Treiber für Windows [Domino, ODBC]. Dies ermöglicht den Zugriff per SQL auf Domino-Datenbanken.
15.1 Überblick über die Suchkonzepte
591
Domino viele Funktionen zur Volltextindizierung und Suche, die neben ausgefeilten Suchstrategien – z.B. „near“-Anweisungen bis hin auf Absatz oder Satzebene – auch die Indizierung generischer Dokumente wie z.B. von Word oder PDF erlauben. Diese Funktionen sind bereits integriert und können über ein einfaches API angesteuert werden. Im Detail stehen folgende Techniken zur Verfügung: 1
Selektion einzelner Dokumente über Methoden in Database Die Methoden getDocumentByID und getDocumentByUNID (s. Kap. 7.1.4.1) ermöglichen es, Dokumente anhand ihrer NoteID oder UniversalID zu laden. Diese Methoden kommen zum Einsatz, wenn es möglich ist, die UniversalID (oder NoteID) durchgängig als primären Schlüssel in der Anwendung zu verwenden oder wenn diese aus anderen Gründen bekannt ist.
2
Selektion einzelner Dokumente über einen oder mehrere Spaltenschlüssel in Ansichten Über die Methoden getDocumentByKey und getEntryByKey in View (s. Kap. 10.7ff.) wird die Übereinstimmung eines oder mehrerer Suchworte (Key) mit den ersten bis zur n-ten sortierten Spalte eines View überprüft und das erste passende Dokument zurückgegeben. Für getEntryByKey wird das erste Dokument in der Reihenfolge der Ansicht zurückgegeben. Für getDocumentByKey hängt eine Rückgabe von der internen Reihenfolge der Ansicht ab, die nicht näher bestimmt ist. Der Vergleich auf Übereinstimmung kann teilweise (Anfang muss übereinstimmen) oder exakt erfolgen. Voraussetzung ist, dass die ersten n Spalten des View sortiert sein müssen. Es können maximal n=4 Spalten durchsucht werden. Spaltenwerte können über @Formeln bei der Definition des View manipuliert werden, so dass die zu durchsuchenden Daten in einer Form vorliegen, die benötigt wird, um sie mit den Suchworten zu vergleichen. Domino durchsucht mit diesen Methoden interne Indizes, die angelegt werden, sobald eine sortierte Spalte erstellt wird. Dieses Verfahren ist das effektivste nach der direkten Selektion über eine ID.
3
Selektion mehrerer Dokumente über einen oder mehrere Spaltenschlüssel in Ansichten Die Methoden getAllDocumentsByKey und getAllEntriesByKey arbeiten wie die zuvor benannten, geben allerdings alle zu den Schlüsselwörtern passenden Dokumente zurück. Die Reihenfolge für getAllDocumentsByKey ist unbestimmt, bei getAllEntriesByKey entspricht sie derjenigen der Ansicht. Dieses Verfahren ist ebenfalls sehr effizient. Die Verfahren aus 2 und 3 sollten wann immer möglich anderen Techniken vorgezogen werden.
4
Selektion mehrerer Dokumente der Datenbank anhand einer @Formel
592
15 Wer suchet, der findet
Suche über Ansichten Suchen über sortierte Spalten in Ansichten sind effizient und sollten anderen Suchmethoden vorgezogen werden. Und-Verknüpfungen in einer Suche werden über Suchparameter als Vector und die Suche über mehrere Spalten realisiert. Oder-Verknüpfungen in einer Suche werden über Listenelemente aus mehreren Items in einer Spalte realisiert. Sortierte Ergebnisse werden durch getAllEntriesByKey – entsprechend der zugrundeliegenden Ansicht – zurückgegeben. Unsortierte Ergebnisse liefert getAllDocumentsByKey
Die Methode search in Database bietet die Möglichkeit, eine @Formel zu spezifizieren, über die alle Dokumente selektiert werden, die dieser @Formel genügen. Dieses Verfahren kommt zum Einsatz, wenn entweder das Selektionskriterium erst zur Laufzeit bekannt wird oder wenn aus einem Grund keine Ansicht vorliegt, die die benötigte Teilmenge an Dokumenten zur Verfügung stellt und entsprechend sortierte Spalten vorhält, die mit einem Suchwort verglichen werden könnten. Zurückgegeben wird eine unsortierte DocumentCollection. Dieses Verfahren kann verglichen werden mit der dynamischen Erstellung einer SELECT-Formel einer Ansicht zur Laufzeit, die ja selbst nur eine @Formel ist. Wenn auch nicht ausdrücklich als Volltextsuche deklariert, so ist dieses Verfahren nur effizient, wenn der Volltextindex für eine Datenbank aktiviert ist. 5
Selektion mehrerer Dokumente anhand einer Volltextsuche in einer Datenbank oder Ansicht Während die oben beschriebenen Verfahren immer auf einen SELECT oder eine Schlüsselübereinstimmung abzielen, bietet die Volltextsuche FTSearch in Database und View weitergehende Möglichkeiten. Neben dem möglichen Einsatz von Jokersymbolen können unscharfe Vergleiche bis hin zur Fuzzy Search anhand von Schlüsselwörtern und Suchparametern durchgeführt werden. Die Volltextsuche kommt dementsprechend genau dann zum Einsatz, wenn Binärdaten durchsucht oder unscharfe Vergleiche durchgeführt werden müssen. Die Ergebnisse können nach Relevanz oder Dokumentenerstellungs- oder änderungsdatum sortiert werden.
15.2 Zugriff über UniversalID und NoteID
593
Die API ist sehr einfach zu handhaben – es muss lediglich ein Suchstring mit einer eigenen Suchsyntax angegeben werden – und bedarf keines zusätzlichen Installations- oder Administrationsaufwandes. Die benötigten Volltextindizes werden erstellt, indem eine entsprechende Eigenschaft der Datenbank aktiviert wird. Ansonsten ist der Vorgang völlig transparent. Die Volltextsuche ist erwartungsgemäß rechenaufwändig und weniger effizient als die vorgenannten Suchmethoden. 6
Selektion mehrerer Dokumente anhand einer Volltextsuche in einer DocumentCollection oder ViewEntryCollection Neben Ansichten und Datenbanken können analog zu 5 auch DocumentCollection und ViewEntryCollection durchsucht werden.
7
Datenbankübergreifende, domänenweite Suche Domino bietet die Möglichkeit, auf administrativer Ebene die so genannte Domänensuche für einen Server zu aktivieren. Datenbanken, die über eine entsprechende Eigenschaft für diese Suche aktiviert sind, werden in diesen Index aufgenommen. Hierdurch wird es möglich, nicht nur auf einer einzelnen Datenbank oder Ansicht, sondern datenbankübergreifend zu suchen. Diese Suche arbeitet analog zu einer Volltextsuche über eine Datenbank, kann aber neben Datenbanken auch Dateisysteme in die Suche mit aufnehmen.
8
Suche über URL-Querys und Browser Über eine spezielle URL-Syntax können Ansichten und Datenbanken über eine vordefinierte Browserschnittstelle durchsucht werden. Diese Suche arbeitet analog zu einer Volltextuche über eine Ansicht.
15.2
Zugriff über UniversalID und NoteID
Ist die UniversalID oder Note ID eines Dokuments bekannt, so kann es direkt aus der Datenbank geladen werden (s. Kap. 7.1.4.1). Interessant ist die Möglichkeit, die UniversalID als Spaltenwert in einer Spalte mit auszugeben, dann kann effizient auf die UniversalID eines Dokuments über getColumnValues in ViewEntry zugegriffen werden, nachdem Dokumente in einer Ansicht gefunden wurden, und es kann umgekehrt geprüft werden, ob ein bestimmtes Dokument sich in einer Ansicht befindet. Zusätzlich kann die UniversalID genutzt werden, um Spaltenberechnung auf Dokumenten auszuführen und diese über getColumnValues zu laden, nachdem ein Dokument anhand einer Spalte mit UniversalIDs über die Techniken aus Kapitel 15.3 geladen wurde.
594
15 Wer suchet, der findet
15.3
Suche über Views
Die Suche über Ansichtenspalten ist die geeignetste Methode, wenn Dokumente anhand eines oder mehrerer Schlüssel selektiert werden sollen. Ein weiteres wichtiges Merkmal bei der Suche über Ansichten ist, dass durch die Ansicht eine vordefinierte Menge von Dokumenten durchsucht wird und nicht etwa die gesamte Datenbank. Hierfür stehen die vier bekannten Methoden getDocumentByKey, getAllDocumentsByKey, getEntryByKey und getAllEntriesByKey zur Verfügung, die alle jeweils in vier Signaturen mit den Parametern (Object | Vector [, boolean]) vorliegen. Die genauen Signaturen finden sich in den Kapiteln 10.4 und 10.5. Wird ein Object übergeben, so kann dies ein Objekt der Klasse String, lotus.domino.DateTime, lotus.domino.DateRange oder Number sein. Ein Vector im Parameter muss aus Elementen solcher Objekte bestehen. Wird ein Vector übergeben, so korrespondiert jedes Element mit einer zu durchsuchenden Spalte, wobei für n Vector-Elemente die ersten n Spalten durchsucht werden. Nutzen Sie die zur Verfügung stehenden Datentypen. Eine Suche über eine sortierte Ansichtenspalte aus Number-Elementen wird immer performanter sein als eine solche Suche über Strings und wird dazu noch weniger Speicher für den Index belegen, zumal Zahlen in ihrer String-Darstellung anders sortiert werden. Der boolean-Parameter gilt für Suchobjekte der Klasse String und erlaubt zwischen exakter und nur auf den Anfang überprüfender Suche zu unterscheiden. Nach Groß- und Kleinschreibung wird nie unterschieden. Wenn keine exakte Suche gewählt und gleichzeitig ein Vector als Suchparameter mit mehreren Elementen übergeben wird, kann es vorkommen, dass bestimmte Treffer nicht ausgegeben werden. Dies ist der Fall, wenn die Sortierung der zweiten Spalte sich mit der nur teilweisen Übereinstimmung der ersten Spalte ändert. Werden sortierte Suchergebnisse benötigt, muss auf die Methode getAllEntriesByKey zurückgegriffen werden. Das Ergebnis dieser Methode ist, im Gegensatz zu dem unsortierten Ergebnis von getAllDocumentsByKey, entsprechend dem zugrunde liegenden View sortiert. Aus Performance-Gesichtspunkten sollten Sie immer über Views suchen. Bei sehr großen Datenbanken werden Views allerdings ebenfalls performancerelevant für die Gesamtperformance des Servers, da die internen View-Indizes regelmäßig aktualisiert werden müssen. Enthalten viele Views einer Datenbank dieselben Dokumente, so ist zu bedenken, dass jedes Update eines Dokuments das Update ALLER dieser Ansichtsindizes erfordert. Zusätzlich sollten Sie den Überblick über die Views in Ihrer Datenbank behalten, denn jede sortierte Spalte bekommt einen Suchindex, der zusätzlichen Platz in Ihrer Datenbank belegt. Wird über einen Vector nach mehreren Such-Schlüsseln gesucht, so entspricht dies einer Und-Verknüpfung dieser Suchparameter. Oder-Verknüpfungen können über einen kleinen Trick realisiert werden. Wenn z.B. das Suchwort „Martin“ entweder im Item F_Vorname oder im Item F_Nachname gefunden werden soll, definieren Sie eine sortierte Spalte mit der Spaltenformel F_Vorname:F_Nachname. Durch den Doppelpunkt werden die beiden Items als
15.3 Suche über Views
595
Liste zusammengefasst. Nun muss noch die Spalteneigenschaft „Show multiple values as seperate entries“ gesetzt werden. Dies hat zur Folge, dass nun jedes Dokument in der Ansicht doppelt erscheint und in dieser Spalte sowohl der Vorname als auch der Nachname gelistet wird. Ein getAllEntriesByKey („Martin“) würde nun Dokumente finden, die entweder im Vornamen oder im Nachnamen (oder in beiden) den String „Martin“ enthalten. Nutzen Sie die Suche über Ansichten, wann immer möglich. Es ist die effizienteste Art, Dokumente in Domino zu selektieren. Die Möglichkeit, Ansichten per Volltextindex zu durchsuchen, wird weiter unten diskutiert. Zur Demonstration der praktischen Arbeit mit Ansichten wurde der Spielzeugladen aus dem vorigen Kapitel gründlich überarbeitet (Listing 15-1). Hierbei wurde die ID-Verwaltung und das Recycling optimiert und neue Suchfunktionen implementiert. Der wesentliche Umbau der ToyStoreAdvanced-Klasse besteht darin, dass nun interne Ansichten als Instanzvariablen zur Verfügung stehen. Sie werden im Konstruktor geladen und müssen daher wie die Datenbank im recycle der Klasse wieder freigegeben werden. Die Ansichten können nun an verschiedenen Stellen genutzt werden, um Nachschlageoperationen durchzuführen. Der entscheidende Vorteil, die Ansichten als Instanzvariable zu führen, besteht darin, dass das verhältnismäßig teure Laden der Ansicht, aber auch das Recycling jeweils nur einmal durchgeführt werden muss. Anhand der Ansicht VIEW_TOYBYID können nun TOY-Dokumente über deren ID verwaltet werden, ohne den teuren db.search anwenden zu müssen. Diese Ansicht (sie befindet sich in der Beispieldatenbank djbuch/djbuch.nsf unter dem Namen „V_toysByID“) ist einfach aufgebaut und hat lediglich zwei Spalten. Die erste Spalte ist sortiert und zeigt die Werte F_ID der Dokumente. Die SELECT-Formel der Ansicht lautet SELECT FORM="FO_TOYFORM" & @IsUnAvailable($Conflict) und wählt alle ToyStore-Objekte, die mit der Maske „FO_TOYFORM" erstellt werden und sorgt gleichzeitig dafür, dass keine eventuellen Speicherkonflikte in der Ansicht angezeigt werden, die einen eindeutigen Umgang mit den IDs unmöglich machen würden. Dies ermöglicht die Überarbeitung von addToy . Diese Methode muss nun nicht mehr über einen Parameter mit einer ID versorgt werden, sondern diese kann in getNewID berechnet werden , wobei diese neue ID nun auch als Rückgabewert von addToy zurückgegeben wird, so dass ein Anwender Aufschluss darüber erhält, welche neue Artikelnummer das neue Spielzeug erhalten hat. Selbstverständich ist auch dieser Ansatz noch stark vereinfachend. In einem nächsten Abstraktionslevel müssten die Spielzeuge nicht mehr nur anhand ihrer ID, sondern über eine eigene Klasse abgebildet werden. Zusätzlich wurde die im vorigen Kapitel vorgeschlagene Methode checkRecycled eingeführt. Ein Hersteller des Spielzeugs wird ebenso im Toy-Dokument gespeichert wie das Erstelldatum . Grundsätzlich wäre dies nicht erforderlich, da die @Formel @created auch in Spaltenwerten das Erstellungsdatum liefert. Die statische Speicherung dieses Datums in einem Item verbessert jedoch die Performance bei späteren lookup-Operationen auf solch einem Spaltenwert.
15 Wer suchet, der findet GC.recycle (dateSearchView); GC.recycle(db); isRecycled=true;
} private void checkRecycled () throws NotesException { if (isRecycled) {throw new NotesException (NotesError.NOTES_ERR_DELETED, "Object has been removed or recycled");} } }
Listing 15-1 Suchkonzepte im Einsatz – ToyStoreAdvanced
in der Um eine eindeutige und neue ID zu generieren, schlägt getNewID Ansicht VIEW_TOYBYID den letzten Wert nach, der aufgrund der Sortierung in der Ansicht auch der größte ist und gibt einen um eins größeren Wert zurück. Damit der Wert wirklich eindeutig ist, muss synchronisiert und die Ansicht mit den Dokumenten durch ein refresh auf den neuesten Stand gebracht werden. Diese Technik ist ein typisches Designpattern, wenn in einem View der größte Wert in einer Spalte bestimmt werden muss, und eignet sich für die Erstellung aufsteigender Werte in einer Ansicht. Da nun eine Ansicht in der Klasse ToyStoreAdvanced zur Verfügung steht, mit der IDs nachgeschlagen werden können, können die Methoden getToy und getToyDocument optimiert werden. Erstere wurde direkt auf getToyDocument zurückgeführt. getToyDocument selbst konnte stark vereinfacht werden . Es muss lediglich in der Ansicht VIEW_TOYBYID die ID nachgeschlagen werden , allerdings auch hier erst nach einem refresh der Ansicht. Dies erfolgt mit dem int-Wert der ID auf der ersten Ansichtenspalte, die alle IDs aller TOYs zeigt. Der int-Wert muss in einen Integer umgewandelt werden, da getDocumentByKey als Parameter ein Object erwartet. Die Problematik der recyclingpflichtigen DocumentCollection (Listing 14-1) ist beseitigt, da die Ansicht, auf deren Basis der lookup erfolgt, während der gesamten Lebensdauer des ToyStoreAdvanced-Objektes bestehen bleibt und sich anders als die DocumentCollection der ersten Version nicht für jeden Aufruf von getToyDocument ändert. Neben dem Nachschlagen von IDs eigenen sich Ansichten aber auch für einfache Suchoperationen über ein oder mehrere Suchwörter. Hierfür wurden die Methoden findToyByName und findAllToysByName implementiert, beide in jeweils zwei Signaturen mit einem oder zwei Suchwörtern, wobei erstere auf letztere zurückgeführt wird. Die Suchwörter werden in einen Vector eingelesen und in dem durch einen refresh aktualisierten View gesucht . Beide sortierten Spalten der hier durchsuchten Ansicht „V_toysByName“ enthalten den Namen bzw. den Hersteller der Spielzeuge. Die SELECT-Formel ist dieselbe, wie für den lookup der IDs. findToyByName und findToysByName unterscheiden sich ausschließlich durch die Verwendung von findAllDocumentsByKey an Stelle von findDocu-
15.3 Suche über Views
599
. Das Beispiel zeigt eindrücklich, wie einfach Ansichten durchsucht werden können. Der gezeigte Aufbau ist für viele Anwendungsfälle geeignet und stellt ein Standard-Designpattern für derartige Suchen dar. Selbstverständlich ist für derartige Suchen über Ansichten, wie ohnehin schon für Ansichten, gewährleistet, dass nur Dokumente gefunden werden, für die der Anwender mindestens Leserechte hat. Wird in einer Javaanwendung, wie z.B. in dem Beispiel, allerdings mit einem vordefinierten Funktionsuser gearbeitet, so gelten selbstverständlich dessen Berechtigungen. Neben TEXT oder NUMBER Items können natürlich auch DATETIME Items durchsucht werden. Hier ist die Suche aber offenkundig etwas weniger flexibel, da nur eine exakte Übereinstimmung möglich ist (der boolean-Parameter exactSearch hat hier keine Auswirkung). Im Beispiel wurde die Methode findAllToysByDate dahingehend angepasst, dass nur der Datumsanteil des DateTime-Parameters für die Suche eingesetzt wird. Dieser wird innerhalb der Methode kopiert, um Nebenwirkungen auf dem Parameter zu unterbinden. Durch setAnyTime in DateTime wird erreicht, dass diese Methode alle Dokumente zurückgibt , die an dem Datum erstellt wurden, das dem Datumsanteil des Parameters dt entspricht. Der Spielzeugladen hat somit ein umfangreiches Refactoring erhalten. Die IDs der Toys werden intern über eine Ansichtenspalte mit Integer-Werten verwaltet und er kann effizient nach Namen und Herstellern durchsucht werden. Bleibt noch die Volltextsuche findAllTF, auf die in Kapitel 15.5 eingegangen wird. Zum Abschluss ein paar typische Anwendungsfälle:
mentByKey
/* Suche mit einem Key - ein Treffer */ doc = ts.findToyByName("sMal"); System.out.println("\n" + doc == null ? "NULL!" : "Gefunden: " + doc.getItemValueString(ToyStoreAdvanced.ITEM_NAME)); doc.recycle(); /* Suche mit zwei Keys - ein Treffer */ doc = ts.findToyByName("BIG","THIS"); System.out.println("\n" + doc == null ? "NULL!" : "Gefunden: " + doc.getItemValueString(ToyStoreAdvanced.ITEM_NAME)); /* Suche mit einem Key - alle Treffer */ dc = ts.findAllToysByName("biG"); dumpDC (dc); dc.recycle(); /* Suche mit einem Date Key - alle Treffer */ dt = session.createDateTime(Calendar.getInstance()); dc = ts.findAllToysByDate(dt); dumpDC (dc);
Die Suche nach einem Namen (genauer dem Anfang eines Namens) über findgibt den ersten Treffer zurück. Die gleiche Methode kann mit zwei Stringparametern die Spalten F_Name und F_Manufacturer durchsuchen . Werden alle Treffer benötigt, kann eine DocumentCollection bezogen werden.
ToyByName
600
15 Wer suchet, der findet
In allen Fällen erfolgt die Suche ohne Berücksichtigung von Groß- und Kleinschreibung. Das letzte Beispiel liefert nicht etwa nur die in der aktuellen Millisekunde erstellten Dokumente – Calendar.getInstance liefert ein Datum und eine Uhrzeit –, sondern aufgrund der Modifikation in findAllToysByDate , alle Dokumente, die am gleichen Tag erstellt wurden. Das Document und die DocumentCollection verlieren durch die Mehrfachzuweisung ihre Referenz und müssen recycelt werden.
15.4
Suche über Datenbanken mit db.search
Ein Nischendasein zwischen der Suche in Ansichtsspalten und der Volltextsuche nimmt die Methode search in Database ein, deren Signaturen bereits in Kapitel 9.4 beschrieben wurden. Wie eingangs erwähnt, lässt sich diese Suche mit einer Ansicht mit einer „dynamisch generierten SELECT-Formel“ beschreiben. Wie für eine SELECT-Formel einer Ansicht wird auch für search eine @Formel verwendet. Alle Dokumente der Datenbank, die dieser @Formel genügen, werden als unsortierte DocumentCollection zurückgegeben. Auch wenn es sich hier nicht ausdrücklich um eine Volltextsuche handelt, so basiert diese dennoch intern auf dem FTIndex der Datenbank. Sie wird auch ohne den Index arbeiten, aber unter erheblichen Performanceeinschränkungen. Es ist ratsam, die Methode möglichst nicht ohne den FTIndex anzuwenden, dessen Vorhandensein über isFTIndexed() geprüft werden kann. Per Default gibt die Methode maximal 5000 Dokumente zurück. Dieser Wert kann durch den Notes-INI-Parameter FT_MAX_SEARCH_RESULTS verändert werden. Existiert kein Volltextindex, so legt Domino einen temporären während der Suche an, dessen Limit über die Notes-INIVariable TEMP_INDEX_MAX_DOC gesteuert wird. Folgende Beispiele geben einen Eindruck von den Möglichkeiten. Die verwendeten @Formeln liefern jeweils einen Boolean Wert als Ergebnis: •
db.search ("F_Name = \"Big dog\"");
Felder werden einfach über einen direkten Vergleich, wie in @Formeln üblich, abgefragt. Stringliterale werden in Anführungszeichen gesetzt, Feldnamen (Variablen) direkt mit Namen benannt. •
Beide Suchen liefern alle Dokumente, deren Item F_Name mit „Big dog“ beginnt. •
db.search ("one := \"dog\"; (F_Name;7) = two + one");
two
:=
\"Big
\";
@left
15.5 Volltextindizes
601
@Formeln können aus mehreren, durch Semikolon getrennten Befehlen zusammengesetzt werden, Variablen werden durch := zugewiesen. Rückgabewert einer @Formel ist immer das Ergebnis der letzten Anweisung, hier also @left (F_Name;7) = two + one. Die Methode search ist sehr praktisch und universell einsetzbar, aber nicht so performant wie getAllEntriesByKey. Sie sollte immer dann eingesetzt werden, wenn erst zur Laufzeit die Suchbedingungen feststehen und kein geeigneter View vorgehalten werden kann. Die Suche über db.search erfolgt über alle Dokumente einer Datenbank, für die der Benutzer der Session mindestens Leseberechtigung hat.
15.5
Volltextindizes
Volltextindizes sind integraler Bestandteil der Domino-Infrastruktur. Sie sind einfach zu handhaben und werden pro Replik auf einer Datenbank aktiviert. Egal, ob die Volltextsuche über Datenbanken, Ansichten, DocumentCollections oder ViewEntryCollections erfolgt, immer wird auf den Volltextindex der gesamten Datenbank zurückgegriffen. Die verschiedenen Methoden zur Handhabung der Volltextindizes sind in Kapitel 9.4 beschrieben. Bei der Migration einer Datenbank von Domino R5 nach Domino R6 müssen auch die Volltextindizes migriert werden, indem diese zunächst in R5 gelöscht und dann für R6 wieder neu aufgebaut werden. Die Volltextsuche auf den verschiedenen Objekten hat folgende Eigenarten: •
Database DocumentCollection FTSearch (String searchQuery[, int max[, int options[, int otherOptions]]]) throws NotesException DocumentCollection FTSearchRange(String searchQuery, int max, int options,int otherOptions, int start) throws NotesException
Das Ergebnis von FTSearch in Database ist eine DocumentCollection. Durchsucht wird die gesamte Datenbank. Die Treffermenge kann durch den optionalen Parameter max eingeschränkt werden. Für max==0 werden alle Treffer zurückgegeben. Abhängig vom optionalen Parameter options mit Sortieroptionen (Database.FT_SCORES, Database.FT_DATECREATED_DES, Database.FT_DATECREATED_ASC, Database.FT_DATE_DES oder Database.FT_DATE_ASC (s. Kap. 9.4) ist das Ergebnis per Default absteigend
nach Relevanz oder auf- oder absteigend nach Erstell- oder Änderungsdatum der Treffer sortiert. Über den Parameter otherOptions mit zusätzlichen Optionen (Database.FT_FUZZY, Database.FT_STEMS) kann eine Fuzzy-Suche und die Suche über Stammwörter aktiviert werden.
602
15 Wer suchet, der findet Die Methode FTSearchRange besitzt den zusätzlichen Parameter start, der dafür sorgt, dass erst Ergebnisse ab einem bestimmten Treffer zurückgegeben werden. Werden die Parameter max und start gleichzeitig eingesetzt, werden max Treffer ab der Position start zurückgegeben. Um die Ergebnismenge durch eine erneute Suche weiter einzuschränken, kann auf die zurückgegebene DocumentCollection FTSearch angewandt werden. Es gelten die gleichen Randbedingungen wie bereits für search aufgezeigt: Die Volltextsuche basiert auf dem FTIndex der Datenbank. Sie wird auch ohne den Index arbeiten, aber unter erheblichen Performanceeinschränkungen. Es ist ratsam, die Methode möglichst nicht ohne den FTIndex anzuwenden, dessen Vorhandensein über isFTIndexed() in Database geprüft werden kann. Per Default gibt die Methode maximal 5000 Dokumente zurück. Dieser Wert kann durch den Notes-INI-Parameter FT_MAX_SEARCH_RESULTS verändert werden. Existiert kein Volltextindex, so legt Domino einen temporären Index während der Suche an, dessen Limit über die Notes-INI-Variable TEMP_INDEX_MAX_DOC gesteuert wird.
•
View int FTSearch(String searchQuery) throws NotesException int FTSearch(String searchQuery, int max) throws NotesException Die Methode FTSearch in View (Kapitel 10.4) gibt kein Ergebnisobjekt zu-
rück, sondern schränkt die Dokumentenmenge der Ansicht ein. Durchsucht werden lediglich Dokumente, die sich in der Ansicht befinden. Nach Anwendung von FTSearch finden sich in der Ansicht nur noch Dokumente, die der in FTSearch verwendeten Suchquery entsprechen. Das View-Objekt kann nun, wie gewohnt, verwendet werden. Alle Methoden, wie z.B. getNextDocument() etc. verhalten sich unverändert. Das gesamte Verfahren ist transparent. Eine wiederholte Anwendung von FTSearch löst eine komplett neue Suche auf der gesamten, ursprünglichen Ansicht aus, eine weiter einschränkende Suche durch Wiederholen von FTSearch ist also nicht möglich. Um eine Treffermenge weiter einzuschränken, kann aus einer durch FTSearch reduzierten Ansicht per getAllEntries eine ViewEntryCollection geladen werden, auf die ebenfalls FTSearch angewandt werden kann. Mit der Methode clear () lässt sich eine Ansicht nach der Verwendung von FTSearch wieder zurücksetzen. Rückgabewert der Methode ist die Anzahl der gefundenen Treffer. Sortier- und andere Optionen können FTSearch in View nicht mitgegeben werden und es kann auch kein Startpunkt angegeben werden. Lediglich über den Parameter max kann eine Maximal-Trefferzahl angegeben werden. Befinden sich in der Treffermenge mehrere Treffer mit der gleichen Relevanz (SearchScore), so ist bei der Verwendung des Parameters max Vorsicht geboten, da nicht gewährleistet ist, welche dieser Treffer ein- und welche ausgeschlossen werden.
15.5 Volltextindizes
603
Ist die Datenbank volltextindiziert (isFTIndexed()), ist das Ergebnis absteigend nach Relevanz sortiert, andernfalls bleibt die ursprüngliche Sortierung der Ansicht erhalten. Bezüglich der Menge der Rückgabewerte kommen auch hier die Notes-INI-Parameter FT_MAX_SEARCH_RESULTS und TEMP_INDEX_MAX_DOC zum Einsatz. •
DocumentCollection und ViewEntryCollection Die Methode FTSearch in DocumentCollection und ViewEntryCollection arbeitet wie FTSearch in View. Durchsucht werden die Dokumente der Document- bzw. ViewEntryCollection. Die Collections sind nach Anwendung der Suche auf die zur Suchquery passenden Treffer eingeschränkt, der Pointer der Collection zeigt auf das erste Dokument, die Verwendung der Collection und deren Methoden, wie z.B. getNextEntry, arbeiten wie gewohnt. Sortier- und Suchoptionen stehen nicht zur Verfügung, das Ergebnis ist immer absteigend nach Relevanz sortiert.
Der wichtigste Parameter beim Einsatz von FTSearch ist die Suchquery. Sie besteht aus einem Ausdruck, der anhand einer speziellen Syntax gebildet wird. Liegt kein Volltextindex vor, so wird diese Syntax nur eingeschränkt unterstützt. •
Einfaches Suchwort Einfache Suchwörter werden direkt als String übergeben. Groß- und Kleinschreibung wird hierbei nicht berücksichtigt. Gefunden werden Dokumente, die das Suchwort in einem beliebigen Item enthalten. Beispiel: db.FTSearch ("biG DoG");
•
Items durchsuchen Soll das Suchwort in bestimmten Items gesucht werden, so wird der Itemname durch das Schlüsselwort FIELD oder durch eckige Klammern [] kenntlich gemacht. Eine Besonderheit ist zu beachten: Stimmt das Suchwort mit einem einzelnen Wort des Items überein, so werden auch Bestandteile eines Items gefunden, z.B.: db.FTSearch ("FIELD F_Name=smaLL DoG") -> Findet auch „Very small
dog“ db.FTSearch ("[F_Name]=cat") -> Findet weder „tomcat“ noch „Catama-
ran“ aber „Tom's cat“ db.FTSearch ("[$File]=Word") -> Findet ein Dokument, das einen Datei-
anhang besitzt, in dem sich das Wort „Word“ befindet. Hierfür ist erforderlich, dass Dateianhänge mit in den Index aufgenommen wurden (s. Kap. 9.4). $File ist ein reservierter Itemname.
604 •
15 Wer suchet, der findet Joker-Symbole Sollen auch Wortbestandteile gefunden werden, müssen Jokersymbole eingesetzt werden. Das Jokersymbol * kann als Platzhalter für kein, ein oder beliebig viele Zeichen eingesetzt werden. Das Jokersymbol ? wird als Platzhalter für genau ein beliebiges Zeichen eingesetzt. db.FTSearch ("[F_Name]=T?m's cat") -> findet „Tom's cat“ db.FTSearch ("[F_Name]=T? cat") -> findet nicht „Tom's cat“ db.FTSearch ("[F_Name]=T* cat") -> findet „Tom's cat“
•
Vergleichsoperatoren Als Vergleichsoperatoren stehen die Zeichen > < = >= <= zur Verfügung. Sie können für NUMBER und DATE Items angewandt werden. db.FTSearch ("[F_BIRTHDAY] < 24.12.2005");
Zusätzlich stehen die internen Felder _CreationDate und _RevisionDate für den Vergleich des Erstell- oder Änderungsdatums zur Verfügung: db.FTSearch ("[_CreationDate] < 24.12.2005");
Runde Klammern () können verwendet werden, um Operationen zusammenzufassen. •
Logische Operatoren Logische Operationen können mit AND, OR, NOT bzw. mit & | ! durchgeführt werden. Diese können durch den Einsatz von Anführungszeichen escaped werden. db.FTSearch ("doG and not OthEr"); db.FTSearch ("\"Bed and Breakfast\"");
•
Schlüsselwörter Über die Schlüsselwörter PARAGRAPH , SENTENCE, CONTAINS oder EXACTCASE kann die Suche erweitert werden. Währen die Suche per Default wortweise indiziert, kann eine satzweise oder paragraphenweite Indizierung aktiviert werden. Diese Suche muss ausdrücklich beim Erstellen des Index durch die Verwendung spezieller Optionen aktiviert werden (s. Kap. 9.4). db.FTSearch ("dog PARAGRAPH big");
15.6 Weitere Suchkonzepte (Domainsearch / browsergestützt)
605
Die Verwendung des Schlüsselwortes CONTAINS ist gleichbedeutend mit der Verwendung von = bei Stringvergleichen. EXACTCASE überprüft eine Bedingung unter Beachtung von Groß- und Kleinschreibung. Auch für EXACTCASE muss der Index bereits bei der Erstellung vorbereitet werden. Nachdem eine Volltextsuche mit FTSearch durchgeführt wurde, kann für die Document- bzw. ViewEntry-Objekte über getFTSearchScore() die Relevanz (SearchScore) abgefragt werden. Dies ist ein Wert zwischen 0 und 100. Die Methode getFTSearchScore() in Document und ViewEntry liefert 0, falls zuvor keine Volltextsuche erfolgt war oder das Document nachträglich anders als über den durchsuchten View bzw. die Treffer-Collection geladen wurde. Wird ein Document einer DocumentCollection oder ViewEntryCollection neu hinzugefügt oder aus dieser entfernt, so liefert getFTSearchScore() ebenfalls 0. Der SearchScore ist bis zu einer neuen Volltextsuche oder einer der genannten Operationen gültig. Selbstverständlich hat auch der Spielzeugladen ToyStoreAdvanced eine Volltextsuche erhalten. Um nicht in einen Performanceengpass zu geraten, mahnt er daher (ins Log) ein Fehlen des Volltextindexes an . Zwar wird in der Methode findAllFT in ToyStoreAdvanced auch für die Volltextsuche die Ansicht aktualisiert . Hierdurch wird aber lediglich erreicht, dass die Ansicht aktuell ist. Ob die darin gezeigten Dokumente bereits im FTIndex aufgenommen und aktuell sind, kann nicht festgestellt werden. Erst ein updateFTIndex in Database schafft hier Abhilfe, wobei diese Operation recht zeitaufwändig sein kann, so dass davon abgeraten wird, diesen Befehl bei jeder Suchoperation auszuführen. Rückgabewert der Methode ist eine ViewEntryCollection mit allen Treffern. Schließlich muss noch nach getaner Arbeit die Ansicht durch clear wieder auf ihren ursprünglichen Zustand zurückgesetzt werden.
15.6
Weitere Suchkonzepte (Domainsearch / browsergestützt)
Ergänzend werden hier weitere Suchkonzepte vorgestellt – die Domainsearch und die Suche über spezielle URL-Kommandos. Die Domainsearch ermöglicht es, domainübergreifende Suchen durchzuführen. Die DomainSearch wird administrativ eingerichtet. Je Datenbankreplik muss für die Datenbanken festgelegt werden, ob diese Datenbank in die DomainSearch aufgenommen werden soll. Durchsucht werden dann alle Datenbanken, die in die Domain Search aufgenommen wurden und jeweils die gesamte Datenbank. Je nach Anwendungsfall kann dies Vor- und Nachteile haben. Vor allem in Situationen, in denen Datenbanken Dokumente enthalten, die zwar von den Benutzern grundsätzlich gelesen werden dürfen (z.B. Konfigurationen oder Dokumente, die lediglich als Container genutzt und nur in einem größeren Zusammenhang genutzt werden) stößt die DomainSearch an ihre Grenzen. Einzige Möglichkeit ist dann, in jedem Dokument ein Flag abzulegen, über
606
15 Wer suchet, der findet
Multithreaded Search Müssen mehrere Datenbanken durchsucht werden, soll die Suche aber jeweils auf eine Ansicht eingeschränkt werden, muss jede einzelne Datenbank durchsucht werden. Mehrere Suchen können in getrennten Threads durchgeführt werden, z.B. je Datenbank ein Thread. Die Ergebnisse der Suchen werden zentral gesammelt (Achtung Synchronisation!). Gespeichert werden müssen mindestens der Name oder die ReplikID der Datenbank, in der ein Dokument gefunden wurde, die UniversalID des Dokuments und ein Titel oder Subjekt zur späteren Kennzeichnung. Ein zentraler Steuerthread muss die Ausführung überwachen und spätestens nach einer maximalen Wartezeit die Ergebnisse auswerten und anzeigen.
das gekennzeichnet wird, ob es durchsucht werden darf, und jeder Suchoperation eine Abfrage auf dieses Flag in der Art db.FTDomainSearch ("[F_isSearchable]=TRUE and ....") mitzugeben. Anders als die oben beschriebene FTSearch hat FTDomainSearch als Ergebnis ein Dokument, das über eine spezielle Maske geöffnet werden muss, um angezeigt zu werden. Vor dem Einsatz einer DomainSearch ist also gründlich zu prüfen, welcher Art die Dokumente der Datenbanken sind und ob diese ohne weiteres zurückgegeben werden dürfen. Eine Alternative zur DomainSearch ist in solch einem Fall natürlich die Suche über Ansichten. Sollen viele Datenbanken durchsucht werden, besteht eine Möglichkeit darin, über mehrere Java Threads diese Datenbanken gleichzeitig zu durchsuchen. Dies hat sich bewährt, da so die Last der Suche auf mehrere Threads verteilt wird und gleichzeitig der Benutzer schnellere Ergebnisse erhält. Die Klassen DJStressRunnerOneSession und DJStressTestExternalSession aus Kapitel 13.3.7 sind eine gute Vorlage für solch eine Suche. Anstelle der dort verwendeten Suche durch getAllDocumentsByKey können Sie FTSearch über eine vordefinierte Ansicht durchführen, die in alle zu durchsuchenden Datenbanken verteilt wird. Die Ergebnisse der einzelnen Threads müssen natürlich an zentraler Stelle eingesammelt werden. Ein erster guter Ansatz ist es, den Namen oder die Replik ID der Datenbank, in der Sie das Dokument gefunden haben, die UniversalID und z.B ein Subject oder einen Titel der Dokumente einzusammeln. Dem Benutzer wird das Ergebnis als Übersicht dieser Titel präsentiert und anhand der hinterlegten UniversalID können die Dokumente ausgewählt werden. Dies wird sowohl im Web z.B. über ein Servlet funktionieren, in dem Sie dann die Dokumente anhand ihrer URL in der Art http://www.djbuch.de/datenbank.nsf/0/<>?OpenDocument öffnen, als auch über eine Java-Anwendung in der Sie Dokumente ebenfalls über db.getDocumentByUNID() öffnen können.
15.6 Weitere Suchkonzepte (Domainsearch / browsergestützt)
607
Abb. 15-1 Suche mit URL-Parametern
Der Vollständigkeit halber soll noch die Suche über Browser URLs vorgestellt werden (Abb. 15-1). Sie arbeitet sehr ähnlich wie FTSearch in View. Für die Suche wird eine URL der folgenden Art verwendet: http://www.djbuch.de/djbuch/djbuch.nsf/V_toysByID?SearchView& Query=Big+dog&SearchOrder=4&SearchWV=TRUE&SearchFuzzy=TRUE& SearchMax=0&count=3&start=4
Hierbei ist „djbuch/djbuch.nsf“ die zu durchsuchende Datenbank, „V_toysByID“ die zu durchsuchende Ansicht. „SearchView“ ist das Schlüsselwort, über das Domino erkennt, dass diese Ansicht durchsucht werden soll. Mögliche Suchparameter sind: • •
• •
•
Query – Der zu suchende String. Er wird wie für FTSearch aufgebaut.
SearchOrder – Die Sortierung der Ergebnisse: 1=Nach Relevanz, 2=Nach Erstelldatum, aufsteigend, 3=Nach Erstelldatum, absteigend, 4=Sortierung wie in der Ansicht. Default ist SearchOrder=1. SerachWF=TRUE/FALSE und SearchFuzzy=TRUE/FALSE – Wortvarianten durchsuchen ja/nein und Fuzzysearch ja/nein. SearchMax – Maximalzahl der anzuzeigenden Dokumente. Ist SearchMax=0 werden alle Treffer gezeigt. Ist SearchMax >0, so wird maximal diese Anzahl von Treffern angezeigt. Dieser Parameter ist nicht kompatibel mit SearchOrder=4. Die Treffer werden für diese Kombination (intern) nach Relevanz sortiert und davon maximal SearchMax Treffer in der Sortierung der Ansicht angezeigt. Count, Start – Es werden maximal Count Treffer pro Seite bis zur Erreichung von SearchMax angezeigt, beginnend ab Eintrag Start.
608
15 Wer suchet, der findet
15.7
Performanceoptimierung
Die verschiedenen Suchmethoden haben unterschiedliches Performanceverhalten. Grundsätzlich muss natürlich als erster Schritt entschieden werden, welche Suchoperation für das gewünschte Ergebnis geeignet ist. Falls möglich sollte die Suche über eine Ansicht und getAllEntriesByKey oder getAllDocumentsByKey erfolgen. Eine wichtige Voraussetzung für FTSearch in Database, View, DocumentCollection und ViewEntryCollection, aber auch für search in Database ist das Vorliegen eines Volltextindexes, damit eine Suche performant durchgeführt werden kann. Da die Volltextsuche limitiert ist, liefert sie möglicherweise nicht alle Treffer, wenn diese Limitierung nicht durch den Parameter FT_MAX_SEARCH_RESULTS angepasst wird. Um die Performanceunterschiede messen zu können, wurde dem Code zum Buch die Klasse SearchComparison beigelegt. Sie legt in der Datenbank einen Zoo aus einer frei definierbare Anzahl von Dokumenten mit dem Titel „Tier 1...n“ und „Amöbe 1..m“ an und sucht auf verschiedene Weise die Tiere, indem Dokumente beginnend mit dem Titel „Tier 1...“, „Tier 2...“, ...., „Tier 9999“ gesucht werden. Diese Suchworte werden, sofern möglich, der Menge der Dokumente angepasst, um möglichst synchrone Ergebnisse zwischen den einzelnen Suchstrategien zu erhalten. Dies kann über Konstanten, die im Code näher erläutert sind, angepasst werden. Zusätzlich kann eingestellt werden, dass anstelle der recht linearen Benennung der Tiere von 1..n eine zufällige gewählt werden kann, was aber die Ergebnisse natürlich variieren lässt. Performanceunterschiede konnten erwartungsgemäß nicht festgestellt werden, da in jedem Fall immer ein (natürlich sortierter) Index dazwischenliegt. Nur aufgrund der Suche nach Wortanfängen kommt überhaupt ein Vergleich mit getAllDocumentsByKey in Frage. Zum Vergleich lässt sich so im Vorfeld feststellen, dass natürlich nur eine „freie“ Entscheidungsmöglichkeit besteht, die schnelle Methode getAllDocumentsByKey zu verwenden, wenn die Suchaufgabe dies zulässt. Wird die Aufgabe komplexer, fällt diese Methode weg, aber es ist zu erwarten, dass auch die Zeiten für FTSearch und search schlechter werden. Im Test wurde jede Suchmethode 9-mal ausgeführt. Dabei wurden als Suchparameter immer „Tier 1“ bis „Tier 9“ verwendet, d.h. es wurden immer Treffer für alle Tiere zurückgegeben, deren Name mit „Tier 1“ usw. anfängt (alle Suchen wurden so ausgeführt, dass dieses Suchverhalten erreicht wurde), d.h. es wurden Treffermengen mit „Tier 1“, „Tier 10“, „Tier 11“ usw. zurückgegeben. Je nach Gesamtmenge lieferten daher die einzelnen Detailsuchen unterschiedliche Treffermengen, da z.B. bei 5000 Tieren natürlich keine 6000er usw. mehr vorliegen und eine Suche nach „Tier 111“ natürlich nur noch 11 Ergebnisse liefert. So konnte ein verhältnismäßig realitätsnahes Szenario mit mehreren Tests pro Suche erzeugt werden. Durch dieses Verfahren lagen für jedes Beispiel 9 Tests vor, deren Trefferzahl und Ausführungszeit gemittelt wurde (Tabelle 15-1). Für alle Tests lag der Volltextindex der Datenbank vor und war aktuell. Durchgeführt werden folgende Suchanfragen (X=1..9). •
getDocumentByKey ("Tier X")
15.7 Performanceoptimierung
609
Tabelle 15-1 Performancevergleich der verschiedenen Suchmethoden in Domino Datenbank mit 10.100 Dokumenten
Da das Jokersymbol ? für genau ein Zeichen steht, wurden hier zusätzlich Suchen nach „Tier X?“, „Tier X??“ usw. durchgeführt, um dieselbe Treffermenge abzubilden. • •
Der Test wurde mehrfach durchgeführt und lieferte stets Ergebnisse, die ähnliche Verhältnisse wiedergaben, wobei die absoluten Werte nicht ausschlaggebend sind, da sie natürlich vom Testenvironment103 abhängen. Die Ergebnisse stimmen mit den Erfahrungen aus der Praxis überein und es lassen sich folgende Aussagen ableiten: •
•
103
getDocumentByKey und getEntryByKey sind die effizientesten Methoden zur Dokumentenselektion, dicht gefolgt von getAllDocumentsByKey und getAllEntriesByKey. Zwischen der Suche mit get[All]Document[s]ByKey und get[All]Entrie[s]ByKey waren in keinem Fall signifikante Performanceunterschiede feststellbar, es kann also je nach Bedarf auf eine Document- oder eine ViewEntryCollection zurückgegriffen werden.
Im Test kamen Server der Version R6.5 und 7 zum Einsatz. Beide lieferten vergleichbare Ergebnisse.
610 •
•
15 Wer suchet, der findet Interessanterweise waren alle Ergebnisse für FTSearch in Database stets etwas schneller als dieselbe Methode in View. Allerdings waren bei den Tests die Ansicht und die Dokumente so verteilt, dass die Ansicht bis auf rund 1% der Dokumente dieselben Dokumente beinhaltete wie die Datenbank. Enthält die Datenbank wesentlich mehr Dokumente, als im View gezeigt werden, ist zu erwarten, dass dies zum Nachteil der Suche mit FTSearch in Database ausfallen wird. search in Database ist gemessen an der Leistungsfähigkeit und Flexibilität sehr performant.
In jedem Fall ist bei dem Vergleich zu beachten, dass Aufgrund der Absicht, die Suchoperationen vergleichbar zu halten, nur eine bestimmte Art der Suche zum Tragen kam. Für bestimmte Anwendungsfälle wird sich teilweise aufgrund der Anforderungen ergeben, welche der Methoden zum Einsatz kommt. Abschließend sei noch darauf hingewiesen, dass gut gewartete Datenbanken und (in Hard- und Software) gut eingestellte Datenbanksysteme einen signifikanten Einfluss auf die Performance von Suchoperationen haben. Hierbei ist zu bedenken, dass jeder Index Plattenplatz aber auch Rechenleistung für dessen Update belegt. Gleichzeitig wirkt ein compact auf eine Datenbank oft Wunder, denn auch eine fragmentierte Datenstruktur wirkt sich nachteilig aus.
15.8
Zusammenfassung
Wie so oft führen auch bei der Entscheidung, welche Suchoperation in Domino die Richtige ist, viele Wege zum Ziel – aber jeder hat seine eigenen Stärken und Schwächen und somit sein eigenes Einsatzgebiet. Die Suche über Ansichtenspalten mit getAllDocumentsByKey und getAllEntriesByKey ist flexibel genug für viele Anwendungsfälle. Da letztere Methode die Ergebnisse dazu noch in der Reihenfolge der Ansicht zurückgibt, ist sie oft der Ansatz der Wahl. Werden komplexere Suchanforderungen gestellt, so ist search in Database eine flexible Variante neben den reichlich vorhandenen FTSearch Methoden. Müssen verschlüsselte Felder oder Dateianhänge durchsucht werden, so ist FTSearch die richtige Wahl. Keine andere der vorgestellten Techniken bietet diese Möglichkeit. Gerade die Volltextindizierung von Dateianhängen ist in vielen Datenbanksystemen eine komplexe Aufgabenstellung, die in Domino als integrierte API-Funktion nicht nur leicht zu handhaben ist, sondern auch flexible und komfortable Suchvarianten für den Benutzer bietet.
Teil 3
Domino-Enterprise-Anwendungen Domino-Enterprise-Anwendungen haben eine lange Tradition. Von der ersten Stunde an war immer die Integration in große Unternehmenszusammenhänge eine Stärke und ein Ziel von Domino. Mit dem Aufkommen von Java und später J2EE hat Lotus diese Entwicklung erkannt und unterstützt und bereits früh zunächst Java als wichtige API-Sprache unterstützt. Früh wurde auch ein Servlet Container in Domino integriert. Mit der Version 6 sollte Domino ursprünglich einen eigenen JSP-Container erhalten und zur vollen J2EE-Konformität ausgebaut werden. Hierfür wurden die gut ausgebauten TagLib-Bibliotheken Domtags und Domutils entwickelt und liegen Domino seit der Version 6 bei. Allerdings wurde kein eigener JSP-Container für Domino entwickelt, sondern Domino die Möglichkeit gegeben, über einen WebSphere Application Server JSPs zu verarbeiten. WebSphere Application Server WAS liegt inzwischen in der Version 6 vor und unterstützt den J2EE V1.4-Standard. Die Entscheidung, Domino und Application Server zu trennen, hat den großen Vorteil, dass über verschiedene Schnittstellen und Ansätze nun sehr einfach auch andere J2EE-Server zum Einsatz kommen können, um die Domtags Tag Library zu verarbeiten. Die Domtags Tag Library basiert auf dem JSP 1.1 und Servlet 2.2 Standard, so dass (nahezu) jeder J2EE-konforme JSP- bzw. Servlet-Container diese TagLib betreiben kann. Darüber hinaus steht natürlich die Möglichkeit offen, eine Anwendung mit den klassischen Designpattern eines Model View Controllers umzusetzten. Zwar können hier keine Enterprise Java Beans zum Einsatz kommen, aber die bereits in Kapitel 4 und insbesondere in Kapitel 5 beschriebenen Techniken ermöglichen es, aus jeder Java-Anwendung alle Domino-Objekte, wie Datenbanken, Ansichten und Dokumente, zu laden, anzuzeigen oder zu verändern. Klassische J2EE-Java-Anwendungen in der Verbindung mit den Standard-Domino-Java-APIs sind eine wichtige und richtige Möglichkeit, Domino-Unternehmensdaten zu integrieren, und sollten mindestens gleichwertig neben dem Einsatz der Domtags Tag Library für die Entwicklung in Betracht gezogen werden. Sie erlauben die Beibehaltung vorhandener Java-Infrastrukturen und -Designpattern. Vor allem Webanwendungen liegen im Focus der Überlegungen in den folgenden Kapiteln, wobei natürlich nicht vergessen werden soll, dass auch nicht-Web-basierte Anwendungen in einer Multitier-Anwendung auf einem Application Server betrieben werden können. Deren Entwicklung ist jedoch ebenso einfach wie geradlinig über die bereits besprochenen Domino-Java-APIs möglich.
Damit ein Application Server mit Domino nahtlos zusammenarbeitet, sind verschiedene Voraussetzungen zu erfüllen. Neben der Integration und Zusammenarbeit beider Systeme sind Fragen der Infrastruktur, eines eindeutigen Einstiegspunktes und der übergreifenden Authentifizierung zu klären. Neben diesen Themen wird sich der dritte Teil dieses Buches allgemein mit der Projektentwicklung von Domino-Enterprise-Anwendungen und deren Qualitätssicherung auseinandersetzen.
16
16 J2EE Infrastruktur
In diesem Kapitel: Integration von Domino und J2EE Servern Domino und JSP Domino als primärer Server J2EE Server als primärer Server Domino und J2EE Server gemeinsam Single Sign On
614
16 J2EE Infrastruktur
J2EE Application Server gibt es viele am Markt und so vielfältig sind auch die Möglichkeiten, Domino mit diesen Servern zu integrieren. Während die hierbei gewählten Ansätze unterschiedlich sind, werden folgende Themen zu betrachten sein: 1
Domino um JSP-Fähigkeit erweitern Der wohl wichtigste Beweggrund, einen Application Server und Domino miteinander zu integrieren, ist der Wunsch, JSP-Seiten mit Domino-Inhalten zu verarbeiten. Grundsätzlich werden hier zwei Ansätze verfolgt: Entweder wird über ein DSAPI Plugin im HTTP-Server von Domino eine Redirection auf einen dahinter stehenden JSP-Container ermöglicht, so dass Anfragen an JSP-Dokumente, die an Domino gerichtet werden, von diesem an den Anwendungsserver weitergeleitet dort verarbeitet und das Ergebnis wiederum vom Domino-HTTP-Server angezeigt wird. Die zweite Möglichkeit sieht vor, dass ein eigenständiger vorgelagerter HTTPServer die Anfragen auf verschiedene Systeme verteilt: generische Domino-Anfragen an Domino und JSP- oder Servletanfragen an einen Application Server.
2
Application Server um Domino-Fähigkeiten erweitern So wie JSP-Seiten über die Domtags Tag Library und Servlets über die NotesFactory auf Domino zugreiffen können, so kann auch eine klassische J2EE-Anwendung mit Domino zusammenarbeiten. Schließlicht darf nicht vergessen werden, dass die Model-Komponente eines MVC Patterns ebenfalls über die NotesFactory mit Domino verbunden werden kann.
3
Austausch des Domino-Servlet-Containers Gründe für den Austausch des Domino-Servlet-Containers gibt es viele. Neben dem Vorteil einen Container zu nutzen, dessen primärer Zweck es ist, als robuster Servlet Container zu dienen, wird die Clusteringfähigkeit verbessert. Vor allem Umgebungen, in denen bereits derartige Systeme im Einsatz sind, werden hiervon profitieren, da vorhandene Infrastruktur, Administrationskenntnisse und vorhandene Überwachungs- und Loggingmechanismen benutzt werden können. Der Servletcontainer wird mit den gleichen Mechanismen ausgetauscht wie die JSP-Fähigkeit hergestellt wird: Entweder werden Servletanfragen von Domino über einen DSAPI-Filter an den externen Container weitergegeben oder ein vorgelagerter HTTP-Server verteilt die Anfragen an das jeweilige System.
4
SSO – Anwendungsserver und Domino akzeptieren ein Login gegenseitig Arbeiten der Application Server und Domino zusammen, soll den Anwendern eine möglichst einfache und transparente Umgebung präsentiert werden. Hierfür kann es erwünscht sein, dass ein Login, das auf Domino oder dem Application Server eingegeben wurde, auch auf dem jeweils anderen erkannt und akzeptiert wird.
16 J2EE Infrastruktur
615
Für die Ansätze, bei denen Domino der primäre Server ist und ein DSAPI Redirector Filter zum Einsatz kommt, erübrigt sich diese Frage. Domino ist der alleinige Einstiegspunkt und wird ausschließlich für das Login verantwortlich sein. Bei den übrigen Ansätzen gilt es ein gemeinsames Token zu finden, das beide Systeme akzeptieren oder das zumindest die Möglichkeit gibt, anhand des Tokens das Login gegen den anderen Server zu verifizieren und dann zu akzeptieren. Als Produkt desselben Herstellers bietet WebSphere eine generische Möglichkeit, über LTPA und ein entsprechendes LTPA Token ein SSO herzustellen (s. Kap. 5.3.5.1). Für JBoss und somit für Tomcat stehen kommerzielle Produkte104 zur Verfügung, über die ein SSO ermöglicht werden kann. Alternativ kann ein SSO auch recht leicht selbst implementiert werden, indem die Domino-SSO-Infrastruktur mit LTPA Token verwendet wird (s.u.). 5
Domino in anderen Webserver HTTP Stack einbinden (Domino-Webserver austauschen) Neben der Möglichkeit, für die oben beschriebenen Integrationsszenarien HTTP-Anfragen an Domino und Application Server zu verteilen, kann der Austausch des HTTP Stacks von Domino mehrere Vorteile haben. Insbesondere die Verwendung des Apache HTTP Server, als Quasistandard, bietet sich an. Ein primärer HTTP Server bietet z.B. verbesserte Möglichkeiten der Lastverteilung. Zusätzlich kann aus einer Vielzahl von Modulen für URL-Rewriting, Proxiing und vieles andere ausgewählt werden. Die Administration, Überwachung, Stabilität und Sicherheit eines eigenständigen Webservers verbessern Verfügbarkeit und Wartbarkeit des Systems und nicht zuletzt können viele vorhandene Quasistandards, wie PHP oder Perl und darauf basierende „out of the box“Anwendungen einfach eingesetzt werden. Zur Verfügung stehen hier zwei grundsätzliche Techniken. Zum einen stehen für die wichtigsten Application Server Plugins für Apache (und auch z.B. für IIS) zur Verfügung. Gleichzeitig besteht immer die Möglichkeit, über einen so genannten Reverse Proxy Anfragen, die für Domino oder WebSphere bestimmt sind, an den jeweiligen HTTP-Task des jeweiligen Servers weiterzugeben, wobei dann allerdings zwei HTTP Stacks vorgehalten werden müssen.
6
Infrastruktur / Netzwerk Die beschriebenen Ansätze zur Integration von Domino und Application Server können in verschiedenen Infrastrukturen aufgebaut werden. In den Fällen, in denen nicht die Forderung besteht, dass Domino und Application Server auf derselben physischen Maschine betrieben werden, kann der primäre Server (Domino, HTTP) in einer DMZ platziert werden und der sekundäre Server (Domino und / oder Application Server) hinter einer weiteren Firewall in einer gesicherten Anwendungszone platziert werden. So wird neben der erhöhten Sicherheit eine gute Lastverteilung ermöglicht und nicht zuletzt wird jeder der einzelnen Server die Aufgabe übernehmen können, für die er am besten geeignet ist.
104
z.B. Domino/JBoss SSO. KEY Enterprise Solutions, www.keysolutions.com
616
16 J2EE Infrastruktur
Im Folgenden gilt es nun, die verschiedenen Ansätze der Integration vorzustellen, ihre Einsatzgebiete aufzuzeigen, welche Möglichkeiten des J2EE-Standards genutzt werden und wie ein Single Sign On umgesetzt werden kann.
16.1
Application Server
J2EE Application Server gibt es inzwischen viele am Markt. Für die Domino-Integration kommen vor allem JBoss, Tomcat und WebSphere in Frage, für die generische Plugins passend zu verschiedenen Anwendungsfällen zur Verfügung stehen. Werden keine generischen Domino-Web-Funktionen benötigt, kann Domino als Datastore eingesetzt und durch den Einsatz der Domino-Java-APIs oder der Domtags Tag Library selbstverständlich jeder (!) J2EE Application Server eingesetzt werden, solange dieser Application Server oder ein vorgelagerter HTTP Server der primäre Server ist. Erst wenn Domino und der Application Server parallel nebeneinander oder Domino als primärer Server betrieben werden soll, werden SSO und die Integration durch Plugins benötigt. Durch die Aufteilung der verschiedenen Aufgaben auf verschiedene Server – z.B. Apache für HTTP, Domino für NSF-Datenbanken und Tomcat für Servlets und JSPs – übernimmt jeder Server seine Spezialaufgabe. Werden die einzelnen Application Server auf verschiedene physische Hardwareplattformen verteilt, so wird nicht nur die Administration vereinfacht, sondern auch die Skalierbarkeit erheblich verbessert. Gleichzeitig können einzelne Server des Verbundes hinter eine zweite Firewall aus der DMZ in ein Application Environment verlagert und so besser gesichert werden.
16.2
Integrationsszenarien
Integrationsszenarien können anhand verschiedener DSAPI Plugins für Domino und verschiedener so-Module für Apache aufgebaut werden. Jedes Szenario hat unterschiedliche Funktionalitäten und Einsatzgebiete. 1
Domino als primärer Server
2
Application Server als primärer Server
3
Webserver als primärer Server – Domino und Application Server nebeneinander
16.2.1
Domino als primärer Server
Wird Domino als primärer Server eingesetzt, wird der HTTP-Task von Domino verwendet, um WebSeiten auszuliefern. Anfragen an Domino-Datenbanken werden wie
16.2.1 Domino als primärer Server
617
gewohnt ausgeliefert. Anfragen an JSP-Seiten und Servlets werden an einen dahinter liegenden Application Server durchgereicht, der die Seiten rendert und das HTMLErgebnis an Domino zurückgibt, der die Seite wieder ausliefert. Der Application Server ist hierbei nicht von außen erreichbar und lediglich für die Auswertung der Servlets und JSP-Seiten zuständig. Single Sign On muss für keines der im Folgenden aufgezeigten Szenarien aufgesetzt werden, da Domino der alleinige Einstiegspunkt und somit auch für die Verwaltung der Benutzerrechte zuständig ist. Domino WAS Plugin (Bestandteil von WAS) WebSphere Application Server kann über einen DSAPI-Filter (Bestandteil der WebSphere Installation) in den Domino-HTTP-Task eingebunden werden. Nach der Installation des Plugins über ein Installationsprogramm wird dieses konfiguriert und festgelegt, welcher WAS-Server über das Plugin angesprochen werden soll. Zusätzlich muss dieses DSAPI105-Modul in der HTTP-Konfiguration von Domino angemeldet werden. Im Serverdokument des Dominoservers wird im Abschnitt „Internet Protocols“ -> „HTTP“ -> „DSAPI“ der vollständige Pfad des zuvor installierten Moduls angegeben, wobei hier auch mehrere Einträge zulässig sind, falls bereits andere DSAPI-Module installiert waren. Des Weiteren muss noch der Servlet Container in Domino auf den neuen, externen Sevletcontainer des Application Servers umgeschaltet werden. Hierfür wird ebenfalls im Serverdokument im Abschnitt „Internet Protocolls“ -> „Domino Web Engine“ im Feld „Java Servlet Support“ der Wert „Third Party Servlet Support“ eingestellt. Ein großer Vorteil des DSAPI-Moduls ist seine Fähigkeit, auch auf entfernten Servern mit WAS zu kommunizieren. Abb. 16-1 zeigt ein typisches Szenario, bei dem der WAS-Server hinter einer zusätzlichen Firewall betrieben wird. Diese Infrastruktur bietet ein hohes Maß an Sicherheit für den WAS-Server. Diese zweite Firewall ist optional und es ist ebenfalls möglich und für kleine Installationen sinnvoll, den WAS-Server auf derselben physischen Maschine wie Domino zu betreiben. Der Vollständigkeit halber sei erwähnt, dass nun natürlich in den Servlets und JSP-Seiten wiederum über die NotesFactory eine Domino-Session erhalten werden kann, die mit den Domino-Objekten über das Domino-Java-API kommuniziert (s. Kap. 16.2.2). Die Aufgabenverteilung stellt sich wie folgt dar: •
105
Domino • Darstellung und Auslieferung statischer HTML-Seiten • Domino-Masken, -Seiten, -Ansichten, die über den Browser gerendert werden • Webagenten • Weiterleitung von Anfragen nach Servlets und JSP an den Application Server DSAPI ist eine Schnittstelle über die der HTTP Stack des Domino-Servers über ein API erweitert werden kann.
618
16 J2EE Infrastruktur *.html *.nsf *.jsp /servlet/* Web User
Domino WAS DSAPI Plugin
Internet
Domino Server (*.html, *.nsf)
Web User *.html *.nsf *.jsp /servlet/*
WebSphere Server (*.jsp, /servlet/*)
Domino TomCat Redirector
Web User Internet
Domino (*.html, *.nsf) und TomCat (*.jsp, /servlet/*) auf dem selben physischen Server
Firewall optional Serveranwendungen müssen nicht zwingend auf physisch getrennten Maschinen liegen
Abb. 16-1 Integrationsszenarien mit Domino als primärem Server
• • •
•
Auslieferung des durch JSP und Servlet gerenderten HTMLs im Domino-HTTP-Task Bereitstellung der Domino-Datenbank und aller Domino-Objekte Kein SSO notwendig – die Servlets / JSPs laufen innerhalb des Sicherheitskontextes von Domino.
Application Server • Auswertung von JSP • Auswertung von Servlets • Weitergabe des gerenderten HTMLs an den Domino-HTTP-Task • Optional Einsatz weiterer Techniken (SQL-Datenbank, MVC)
16.2.2 Application Server als primärer Server
619
Domino Tomcat Redirector Der Domino Tomcat Redirector (Abb. 16-1 ) geht denselben Weg wie WebSphere und bietet einen DSAPI-Filter an, der die jakarta-tomcat-connectors [Tomcat, Connectors] nutzt, um die Verbindung herzustellen. Eine Kurzanleitung findet sich im Internet [Tomcat, DSAPI Kurzinfo]. Domino wird ebenso konfiguriert wie oben beschrieben, auch die Aufgabenverteilung bleibt unverändert. Im Unterschied zu dem WAS Plugin muss sich der Tomcat-Server auf derselben physischen Maschine befinden, auf der auch Domino eingesetzt wird. Domino und WebServices Ein interessanter Ansatz ist es, Domino über WebServices (Abb. 16-1 ) Anfragen mit einen zurückgelagerten Server zu verbinden. Dies ist natürlich unabhängig vom eingesetzten Application Server, selbst Domino oder z.B. ein LotusScript Agent, der den WebService ausführt, könnte dies sein. Zwar ist auch hier die Infrastruktur flexibel, da WebServices über HTTP und somit beliebig remote ausgeführt werden, allerdings muss man sich im Klaren sein, dass dieses Szenario weniger für Hochverfügbarkeitsumgebungen geeignet ist. Denn anstatt dass wie oben über eine Anwendungsintegration die Anfragen über ein API direkt weitergegeben werden, muss für die Abfrage des WebServices ein weiterer Socket für die benötigte HTTP-Verbindung geöffnet und die Anfrage dann in einer neuen HTTP Session abgewickelt werden, ohne dass hierfür noch ein Connection Pooling oder sonstige Performanceoptimierungen vorgenommen werden könnten.
16.2.2
Application Server als primärer Server
Den Application Server als primären Server einzusetzen ist die einfachste und flexibelste Herangehensweise. Da hier keine Plugins zum Einsatz kommen, kann insbesondere bei diesem Konzept jeder beliebige Application Server eingesetzt werden, sofern er nur Java versteht, denn die Verbindung wird über die in Kapitel 4 und 5 eingeführten Techniken der Domino-Session aufgebaut. Je nach gewünschter Infrastruktur und erwarteter Last, kann entweder die lokale oder Remote-Session im DominoJava-API eingesetzt werden. Auch in diesem Szenario stellt sich die Frage nach Single Sign On (zunächst) nicht, da der Application Server der einzige Einstiegspunkt ist, zumindest solange die Verbindung zu Domino über einen Funktionsuser hergestellt wird. Sollen individuelle Sessions mit deren jeweiligen individuellen Benutzerkennungen hergestellt werden, so wird eine Synchronisation zwischen den in WebSphere erforderlichen HTTP Logins und den Domino-Benutzernamen notwendig. In diesem Fall können die in Kapitel 16.3 beschriebenen Techniken eingesetzt werden.
620
16 J2EE Infrastruktur *.html *.jsp /servlet/*
Domino wird lokal per Domino Java API erreicht
Web User Internet
Tomcat/JBoss, WebSphere (*.html, *.jsp, /servlet/*) und Domino (per lokale Domino Session) auf dem selben physischen Server
Web User
*.html *.jsp /servlet/*
Domino wird remote per DIIOP und Domino Java API erreicht
IBM HTTP Domino Plugin (iSeries) Domino Server (*.nsf)
Optional: Domino Java API Apache Module
JBoss/Tomcat (*.jsp, /servlet/*)
Abb. 16-2 Integrationsszenarien mit WAS oder Apache als primärem Server
Application Server und lokale Domino-Session Wird Domino auf derselben physischen Maschine (Abb. 16-2 ), wie der Application Server installiert, kann mit einer lokalen Session gearbeitet werden. In Bezug auf Performance und Konfiguration ist diese Vorgehensweise vorteilhaft. Es müssen lediglich die Laufzeitbibliotheken von Domino im Path (s. Kap. 4.2) angemeldet sein und eine Domino-Session kann aufgebaut werden. Der Application Server selbst wird wie gewohnt eingesetzt. Der Bezug und die Auswertung von Domino-Daten kann über sämtliche im Application Server zur Verfügung stehenden Techniken, sei es eine JSP-Seite, ein Servlet oder die Model-Componente einer MVC-Umgebung, erfolgen. Sollen in einer JSP-Seite die Domtags (s.u.) zum Einsatz kommen, muss DIIOP auf dem Domino-Server aktiviert sein. Die Aufgabenverteilung stellt sich wie folgt dar:
16.2.2 Application Server als primärer Server
621
•
Application Server • Darstellung und Auslieferung statischer HTML-Seiten • Auswertung und Darstellung von JSP-Seiten • Auswertung und Darstellung von Servlets • Kommunikation mit dem Domino-Server über die lokale Domino-Session in Servlets, JSPs oder MVC. Alle Domino-Objekte stehen entsprechend der Domino-Java-API zur Verfügung. • Fungiert als vollwertiger J2EE-Server • SSO notwendig, sobald die Verbindung nicht mit einem Funktionsuser, sondern mit individuellen Benutzerkennungen stattfinden soll.
•
Domino • Bereitstellung der Domino-Java-Objekte • Domino-Datenbank
Application Server und Remote-Domino-Session Der Einsatz von DIIOP ermöglicht neben dem Einsatz der Domtags Tag Library, den Domino-Server auf einer eigenständigen Hardware (Abb. 16-2 ) zu betreiben und ihn optional hinter eine zweite Firewall zu verlagern. Die Aufgabenverteilung stellt sich dann wie folgt dar: •
Application Server • Darstellung und Auslieferung statischer HTML-Seiten • Auswertung und Darstellung von JSP-Seiten • Auswertung und Darstellung von Servlets • Kommunikation mit dem Domino-Server über die Remote-DominoSession und DIIOP in Servlets, JSPs oder MVC. Alle Domino-Objekte stehen entsprechend der Domino-Java-API zur Verfügung. • Kommunikation mit dem Domino-Server über Domtags und DIIOP • Fungiert als vollwertiger J2EE-Server • SSO notwendig, sobald die Verbindung nicht mit einem Funktionsuser, sondern mit individuellen Benutzerkennungen stattfinden soll.
•
Domino • Bereitstellung der Domino-Java-Objekte • Bereitstellung von DIIOP • Domino-Datenbank
622
16 J2EE Infrastruktur
16.2.3
Domino und Application Server
Der Betrieb von Domino und einem Application Server nebeneinander ist das klassische Szenario für große Unternehmensinstallationen. Ein Webserver – wegen der Verfügbarkeit entsprechender Plugins in der Regel Apache oder der IBM-HTTP-Server, der ein Derivat des Apache-Servers ist – übernimmt hierbei die erste Linie. Sämtliche Anfragen werden vom Webserver entgegengenommen und entweder selbst verarbeitet, sofern es sich um statische Inhalte handelt oder an den zuständigen Server weitergeleitet (Abb. 16-2 ). Dies kann ein Application Server oder Domino sein. Je nach verwendetem Application Server stehen unterschiedliche Plugins und Techniken zur Verfügung, wobei für jeden anzusprechenden Server (Tomcat, Websphere, Domino) ein eigenes Plugin notwendig sein wird. Die Infrastruktur lässt sich mit dieser Herangehensweise mit höchster Flexibilität den Bedürfnissen anpassen. Lediglich der (Apache-) WebServer muss sich in der DMZ befinden, da er den HTTP Thread vorhält. Die Kommunikation zwischen Apache einerseits und Domino, WAS, Tomcat oder JBoss andrerseits erfolgt über das TCP/IP Protokoll, so dass die Application Server und Domino auch hinter einer weiteren Firewall positioniert werden können. WebSphere Apache Plugin Für WebSphere steht ein Apache Plugin zur Verfügung, der die Einbindung sehr einfach macht. Hierbei handelt es sich um ein Apache-Modul. Während der Installation (WAS 6.x) werden Apache und WebSphere bereits konfiguriert. Im Administrator von WebSphere muss lediglich das so generierte Plugin auf die WAS Server verteilt und die J2EE-Enterprise-Anwendungen dem Webserver zugeordnet werden. WebSphere und Apache oder IBM HTTP können sich auf derselben oder auf verschiedenen physischen Maschinen befinden. Domino und Apache Domino steht auf vielen Plattformen zur Verfügung, für die nicht alle ein Plugin106 für Apache ausgeliefert wird. Alternativ kann die Anbindung sehr einfach über das Apache-Modul mod_proxy [Apache, mod_proxy] erfolgen. Dieses Modul wird als so genannter Reverse Proxy eingesetzt. Nach entsprechender Konfiguration wird jede Anfrage an den Webserver, die einem bestimmten URL-Pattern entspricht – in der Regel wird dies ein bestimmtes Verzeichnis sein –, nicht von diesem selbst beantwortet, sondern an eine zugeordnete URL weitergeleitet. Das hierbei zurückgegebene Ergebnis wird ausgeliefert. Dieses Verfahren ist robust und für den produktiven Einsatz geeignet, hat aber zur Folge, dass zwei HTTP Stacks – einer von Domino und einer von Apache – betrieben werden, was nur in Konfigurationen eine Rolle spielen wird, bei der Domino und Apache auf derselben Hardware betrieben werden. Domino liefert weiterhin wie 106
Domino 6 for iSeries Apache plug-in [Redbook, Domino WAS].
nicht benötigt, da die Datenbank über das domino:db-Tag verfügbar ist. Damit Zeileneinträge später im Browser selektiert werden können wurde das Attribut enableselect="true" gesetzt114. Das Tag domino:view referenziert nur das View-Objekt. Die Daten werden über eine domino:viewloopoder domino:docloop-Schleife ausgelesen. 114
Die Auswertung der selektierten Dokumente wird nicht im Beispiel gezeigt und muss über den Einsatz von domino:action, domino:ifserverevent und domino:selectedloop umgesetzt werden. domino:action definiert einen Aktionsbutton, der die Aktion startet, die sich auf alle markierten Elemente beziehen soll, domino:ifserverevent fängt die so ausgelöste Aktion ab und bietet den Container für die Weiterverarbeitung. domino:selectedloop schließlich bietet eine Schleife an, die analog zu domino:viewloop arbeitet, aber nur über die zuvor selektierten Dokumente iteriert.
17.4.4 domino:document
641
Die enableselect-Eigenschaft wird über das domino:selectentry-Tag zum Leben erweckt. Dieses Tag beschreibt die Stelle, an der die CheckBox erscheinen soll (s. Abb. 17-3). Spaltenwerte der Ansicht können über domino:viewitem unter Angabe der Spaltennummer geladen werden. Sollen diese mit einer JSP-Seite, die sich auf eine Maske (diese muss als DominoDesignelement im Designer vorliegen) Abb. 17-3 Ausgabe des domino:view-Beibezieht, angezeigt werden, so können spiels beliebige Textversatzstücke mit automatischen Links versehen werden. Das Tag domino:formlink übernimmt dies. Nicht immer genügt das strikte Raster, das duch eine Maske und deren Funktionen definiert ist, den Ansprüchen. Daher können ohne Weiteres auch eigene Links generiert werden. Sobald im domino:docloop-Tag das Attribut id gesetzt ist, kann unter dem dort verwendeten Wert über Java ein gleichnamiges Document-Objekt gefunden werden. Die Verwendung versteht sich von selbst. Es handelt sich um ein gewohntes Document aus der Domino-Java-API. Zwei verschiedene Ansätze haben im Beispiel Ansichten durchschritten und Zeileneinträge verlinkt. Sowohl der Ansatz, die Ansicht zu durchschreiten, war unterschiedlich (domino:viewloop und domino:docloop) als auch die Methode, die verwendet wurde, um Links zu generieren (domino:formlink und ein scriptlet unter Verwendung des Document-Objekts). Allerdings sei darauf hingewiesen, dass der Einsatz der für die viewloop generierten formlinks sich nicht von den über das Document erzeugten Links unterscheidet (bis auf den Namen der Zielseite). Die Darstellung der Daten in einer Maske (formlink) erfolgt erst in der Zielseite des domino:formlink unter Zuhilfenahme des domino:form-Tags.
17.4.4
domino:document
Bevor auf die Maske und domino:form eingegangen wird, zunächst ein kurzer Abstecher zum Dokument. Über das Tag domino:document können Dokumente direkt manipuliert werden, sofern nur die UniversalID bekannt ist. Die im vorigen Beispiel Listing 17-6 generierten Links wurden mit der jeweiligen UniversalID der Dokumente ausgestattet. So kann eine JSP-Seite, die das domino:document-Tag einsetzt, über den URL-Parameter unid, in dem diese UniversalID übermittelt wurde, einzelne Dokumente manipulieren. Listing 17-7 enthält ein kleines Beispiel, anhand dessen der Einsatz des domino:document, aber auch einiger weiterer Besonderheiten gezeigt werden kann. Die Datenbank wird hier nicht innerhalb eines domino:session-Tags geöffnet. Schließlich wird nur ein einziges Tag verwendet, das eine Session benötigte, so genügt das einzelne domino:db-Tag. Dieses enthält nur ein einziges Attribut id="thisDB"
642
17 J2EE @ Domino
<% String thisUnid=request.getParameter("unid"); DJCacheDocument cache = (djbuch.kapitel_13.DJCacheDocument)request .getSession().getAttribute(thisUnid); if (cache != null) { out.println ("Dokument bereits im Cache"); out.println("
. Dies ist möglich, da in der web.xml-Konfiguration für die Webanwendung alle übrigen Parameter als Defaultwerte definiert wurden. Die Konsequenz ist selbstverständlich, dass diese Seite in einem ganz anderen Benutzerkontext läuft als das vorige Beispiel, bei dem der angemeldete Web-Benutzer (über SSO und user="*webuser") auch die Domino-Session der JSP-Seite geöffnet hat – ein Risiko und eine Chance. So reizvoll die Möglichkeit ist, unterschiedliche Tags mit unterschiedlichen Benutzerkennungen zu betreiben, so hoch ist das Risiko, dass hier Sicherheitslöcher aufgerissen werden. Am besten, ein einmal eingeschlagenes Konzept (angemeldeter Benutzer ist auch Domino-Benutzer versus Anmeldung über Funktionsuser) wird konsequent durchgehalten. Das domino:document-Tag erhält über den Parameter thisUnid eine Universal ID, die zuvor entweder per HTTP GET oder POST übermittelt und in die Variable thisUnid geladen wurde. Dem Beispiel fehlt noch eine vernünftige Fehlerüberprüfung, ob die unid überhaupt geliefert wurde. Alternativ kann – aufmerksame Leser des vorigen Kapitels werden dies längst entdeckt haben – das Attribut unid auch ganz weggelassen werden und das Tag als < domino:document id="thisDoc"> oder gar als <domino:document> codiert werden. Da wurde von den Entwicklern bei Lotus wirklich mitgedacht, denn das unid-Attribut gehört zu den Attributen, für die ein Defaultwert gesetzt werden darf. Das ergibt im Allgemeinen natürlich keinen Sinn, aber im speziellen Kontext einer Seite, die ein domino:document-Tag verwenden soll, sehr viel. Die in der URL über-
17.4.4 domino:document
643
mittelte ID könnte dann für den Seitenkontext über <domino:default unid="< %=thisUnid%>" scope="page"/> als Default gesetzt werden, der dann für alle domino:document-Tags gültig ist. Die Items des Dokuments können über das domino:item-Tag oder natürlich über ein scriptlet und die API-Methoden, wenn zuvor das Attribut id spezifiziert wurde, angezeigt bzw. manipuliert werden. Das Beispiel will noch mehr. Wurde das Document erst einmal geladen, wird es in vielen Fällen vorkommen, dass eine zeitnahe Aktualisierung nicht zwingend notwendig ist. Dann darf das Document gecached werden und kann z.B. in der HttpSession zwischengespeichert werden. Ist das Document erst im Cache , wird es nicht erneut geladen, sondern der Cache angezeigt . Auch hier nochmals der Hinweis, dass das DJCacheDocument keine Benutzerrechte speichert und <domino:document id="Name der Java-Objektvariable" user="Benutzername der Session" password="Passwort" host="DIIOP-Servername" dbname="Name der zu öffnenden Datenbank" dbserver="Servername, auf dem sich diese DB befindet." unid="Universal id des zu ladenden / ändernden Dok" responseto="Universal ID eines opt. Mutterdokuments" schema="Maske, die (neuen) Dokumenten zugeordnet wird." debug="false|true" onfailure="exception|inline" time="false|true" >
Listing 17-8 domino:document
lediglich ein POJO darstellt – in der HttpSession des Benutzers ist dies aber kein Problem. Änderungen am Document können über das Tag domino:savenow , das entspricht einem save () in Document, gespeichert werden. Das Tag domino:document kennt drei neue Attribute (s. Listing 17-8), die noch nicht für domino:db oder domino:session erläutert wurden: •
•
•
unid Die Universal ID (s. Kap. 2.2.1ff. und 7.1.4.1) eines zu öffnenden Documents. Falls dieses Attribut weggelassen wird (und nicht über einen Defaultwert spezifiziert ist, wird ein neues Document-Objekt erzeugt. responseto Wird ein neues Dokument erstellt und soll es ein Antwortdokument werden, so muss über responseto die Universal ID des übergeordneten Dokuments angegeben werden (s. Kap. 7.3) schema
644
17 J2EE @ Domino Domino-sieht vor, dass jedem Dokument eine Maske zugeordnet ist. Diese wird über schema festgelegt. Beim Neuanlegen eines Dokuments wird der Wert aus schema in das Item „Form“ übernommen. Beim Speichern werden eventuelle globale, in der Maske definierten Validierungen ausgeführt.
17.4.5
domino:form
Mit Hilfe des domino:document-Tags können Dokumente geladen und manipuliert werden. Soll dies mit dem Schema einer Maske erfolgen, so kommt domino:form zum Einsatz. Direkt vergleichbar mit dem Designelement „Form“ des Domino Designers (Kapitel 3.2), wird über domino:form eine Maske geladen, in deren Kontext das Dokument dargestellt und bearbeitet werden kann. Einige wichtige Funktionen zeigt das Beispiel in Listing 17-9. Zur Abwechslung wird die Datenbank unter kompletter Nennung von Benutzer und Passwort geladen . Das domino:form-Tag benötigt einen internen Namen, um im Page-Kontext referenziert werden zu können. Zusätzlich kann über schema die korrespondierende Domino-Maske angegeben werden. Dieses Attribut ist kein Pflichtfeld, denn <domino:db id="thisDB" dbserver="CN=Java/O=DJBUCH" dbname="djbuch/djbuch.nsf" user="CN=Administrator/O=DJBUCH" password="geheim" host="www.djbuch.de"> <%=thisDB.getParent().getUserName()%>
dieses JSP-Tag ist nicht zwingend mit einer Domino-Maske verknüpft. Wird das Attribut angegeben, so können nicht nur in der Maske definierte Validierungen und Feldberechnungen ausgeführt werden, sondern neu erstellte Dokumente erhalten eventuelle Vorbelegungen von Feldern. Neue Dokumente werden mit diesem Namen im Item „Form“ gespeichert. Wird das Attribut schema weggelassen, so bleibt dieses Item leer, was zu Problemen beim Einsatz des Notes Clients führen kann, denn dort ist dieses Item notwendig um eine Maske laden zu können, die benötigt wird, um ein Dokument darzustellen. Soll wie im Beispiel das Dokument editiert werden, so muss das Attribut editmode="edit" gesetzt werden. Soll es gespeichert werden, gibt validhref die Target URL an, die nach erfolgreicher Speicherung und Validierung angesteuert werden soll. Ein praktisches Feature ist die Möglichkeit, eine automatische Navigation zwischen Dokumenten aufzubauen. Damit Domino weiß, zwischen welchen Dokumenten hin- und hergesprungen werden soll, wird eine Ansicht mit dem Attribut viewname angegeben, navigiert wird dann mit dem Tag domino:nextdoc oder domino:prevdoc, wobei natürlich „normale“ Links weiterhin Anwendung finden . Einzelne Items werden über domino:input (optional zur Bearbeitung) angezeigt, entscheidend ist das Attribut editmode. Nur für editmode="edit" werden HTML-Input-Felder generiert, für editmode="display" (default) werden lediglich die Itemwerte dargestellt und schließlich für editmode="delete" wird das korrespondierende Dokument gelöscht. Validierungen, ein wichtiges Thema in Masken, werden mit domino:validaterequired angefordert. Diese werden serverseitig ausgeführt und geben im Fehlerfall den Fehlertext des Attributes message aus. Zur abschließenden Verarbeitung eines Dokumentes stehen eine Reihe von Standardfunktionen zur Verfügung. domino:saveclosedoc validiert ein Dokument bzw. die geänderten Eingaben, speichert es, sofern valide und springt an die in validhref (im domino:form-Tag) angegebene Adresse. Die Attribute des Tags domino:form können in drei Gruppen aufgeteilt werden. Die Attribute id, user, password, host, dbname, dbserver, debug, onfailure und time werden, wie zuvor für domino:db beschrieben, verwendet. Die zweite Gruppe von Tags entsprechen Standard HTML-Attributen, werden eins zu eins durchgeschleift und in dem