Das jadice Document & Co.

Wie schon in der Ankündigung versprochen, wollen wir nicht nur oberflächlich die neuen Funktionen darstellen, sondern in medias res gehen und diejenigen Themen beleuchten, mit denen unsere Integratoren regelmäßig zu tun haben: Programmierschnittstellen und wie sie benutzt werden. Hierbei setzen wir zumindest ein Grundlagenverständnis der bisher vorhandenen Schnittstellen voraus. Sollte dies für einzelne Leser nicht zutreffen, oder von untergeordnetem Interesse sein, so müssen wir sie leider auf folgende Artikel vertrösten, in denen wir eine eher anwenderorientierte Betrachtungsweise einnehmen werden.

Eines der ganz zentralen Objektmodelle der jadice document platform betrifft die Abbildung von Dokumenten, Seiten und den Inhalten von Seiten. So liegt es nahe, die Betrachtung des "Innenlebens" von jadice 5 mit diesem Thema zu beginnen.

Ein immer wieder adressierter Kritikpunkt der Modellierung von Dokumenten in jadice 4 war die, zumindest für komplexere Anwendungsfälle, unzureichende Flexibilität des Documents. So war es z.B. nicht ohne Weiteres möglich, Seiten (Pages) gleichzeitig in mehreren Dokumenten (Documents) zu halten. Jadice 5 verspricht hier gravierende Verbesserungen. Bevor wir jedoch in die Details einsteigen, möchten ich mit einem kurzen Überblick über die wesentlichen Entitäten kurz in Erinnerung rufen, über was wir reden.

Documents stellen eine Ansammlung von Seiten dar. Sie müssen in der alten wie in der neuen Version nicht 1:1 mit physischen Datenströmen, also z.B. einer Anzahl von Seiten aus einem Multi-Page-TIFF, korrelieren, sondern können Seiten aus den verschiedensten Quellen enthalten. Pages entsprechen weitestgehend einzelnen Seiten von Dokumenten, besitzen jedoch in sich nochmals eine Strukturierung in Ebenen. Über diese Strukturierung können z.B. Formularinhalte über Formularhintergründe gelegt oder Annotationen unabhängig von den annotierten Dokumentseiten gehalten werden. Die einzelnen Ebenen der Seite werden von PageSegments eingenommen. An diesen PageSegments "hängen" sämtliche visuellen Inhalte der Dokumente. Seiten ohne PageSegments sind zwar zulässig, haben dann aber weder darstellbare Inhalte noch überhaupt eine Größe.

Alle genannten Elemente dieses Modells finden sich in jadice 5 namensgleich wieder. Wir hatten diskutiert, den abstrakten Charakter einer Seitensammlung, die ein Document repräsentiert, durch einen entsprechend abstrakten Namen (z.B. 'PageCollection') zum Ausdruck zu bringen, haben dies jedoch aufgrund der größeren Prägnanz des Begriffs 'Document' verworfen. An anderen Stellen haben wir in solchen Fällen weniger Rücksicht genommen, wie in folgenden Artikeln noch zu lesen sein wird.

Das Document

 

Auch wenn es in der alten wie in der neuen Version von jadice noch ein Document gibt, so sind doch die Änderungen an diesem nicht unerheblich. Die augenfälligste Änderung dabei ist zunächst, dass aus der Klasse Document ein Interface geworden ist. Diesen Schritt sind wir gegangen, um in der Lage zu sein, unproblematischer neue Verhaltensweisen von Documents zu erlauben. Ein Beispiel für eine abweichende Verhaltensweise werden wir in Form des UIDocuments weiter unten noch vorstellen. Eine Konsequenz aus der Umstellung auf ein Interface ist natürlich die, dass es nun nicht mehr mit einem Document myDocument = new Document() getan ist um ein neues Dokument zu erstellen. Das Äquivalent lautet nun Document myDocument = new BasicDocument() und ist zum Glück nicht viel komplizierter. In einfachen Situationen können Sie das Erzeugen des Documents ohnehin dem Lademechanismus überlassen, sodass sich keine Änderung ergibt.

Seitenverwaltung

Die vornehmste Aufgabe des Documents ist es, eine Sammlung von Seiten zu verwalten. Im Sinne der Designregel, im Zweifel zu Komposition zu neigen, haben wir uns dafür entschieden, Ihnen die volle Mächtigkeit der Java Collections-API zur Verfügung zu stellen: Document.getPages() liefert Ihnen eine EventList<Page>, die sich zunächst wie eine gewohnte java.util.List<Page> verhält - nur noch deutlich mächtiger. Für häufig verwendete Anwendungsfälle wie dem Abruf einer einzelnen Seite bietet das Document nach wie vor einige "Komfortmethoden", z.B. Document.getPage(int pageIndex), die jedoch im Wesentlichen ihre Aufgabe lediglich an die Seitenliste delegieren.

Spielregeln für Seiten

Seiten können sich in jadice 5 in beliebig vielen Documents gleichzeitig befinden, was in jadice 4 nicht zulässig war. Allerdings ist es nicht erlaubt, dass sich die gleiche Seite zweimal im gleichen Document befindet - eine Verletzung dieser Regel führt zu einer IllegalArgumentException. Darüber hinaus ist es auch nicht notwendig, dass sich eine Seite überhaupt in einem Document befinden muss. Es ist also problemlos möglich, eine Seite aus einem Dokument zu entnehmen, zunächst "beiseite" zu legen und später einem anderen Dokument wieder hinzuzufügen.

GlazedLists in der jadice document platform

Ein Entwurfsprinzip der jadice document platform war immer, möglichst wenig externe Abhängigkeiten zu besitzen, um Ihnen als Integratoren das Leben leicht zu machen. Dieses Prinzip haben wir auch in jadice 5 beibehalten, allerdings hat uns eine Bibliothek, die inzwischen in der Java-Welt nicht mehr wegzudenken ist, doch sehr in Versuchung geführt: GlazedLists. Sie erweitert die Java Collections-API um Listen, die Änderungen an ihrem Inhalt über Listener-Schnittstellen propagieren. Die sich hieraus ergebenden Möglichkeiten waren so verlockend, dass wir auf die Funktionalität von GlazedLists nicht verzichten wollten. Um jedoch nicht eine Abhängigkeit von GlazedLists zu schaffen, haben wir den Kern von GlazedLists unter einem anderen Package-Namen in unsere levigo-utils aufgenommen, was uns die Lizenz von GlazedLists dankenswerter Weise erlaubt. Kudos an dieser Stelle an die Entwickler von GlazedLists für deren hervorragende Arbeit.

Die EventList-Funktionalität der Seitenliste des Dokumentes erlaubt es nun z.B. durch Hinzufügen eines ListEventListeners auf Änderungen an der Seitenliste zu horchen. Auch Locking-Funktionen zum Thread-sicheren Ändern der Seitenliste werden von der EventList bereitgestellt. Darüber hinaus existieren eine ganze Reihe von Zusatzfunktionen, wie die Erzeugung von sortierten oder gefilterten Listen, deren Inhalt automatisch aktuell gehalten wird, die sehr mächtige Konstruktionen ermöglichen. Beispiele hierzu werden sich in einem der späteren Artikel finden.

Die Seitenliste und Thread-Sicherheit

Bei Änderungen an der Seitenliste muss, zur Herstellung der Thread-Sicherheit, nun generell mit Locking gearbeitet werden. Dies ist auf den ersten Blick eine Verschlechterung gegenüber der Version 4: dort waren alle Änderungsmethonden für die Seitenliste implizit Thread-sicher. Allerdings war diese Thread-Sicherheit begrenzt - so war es z.B. nicht möglich, mehrere Seiten auf einmal hinzuzufügen, ohne dabei die Sperre aufzugeben. Künftig könnte dies z.B. so aussehen:

final EventList<Page> pages = myDocument.getPages();
final Lock writeLock = pages.getReadWriteLock().writeLock();
writeLock.lock();
try {
  pages.add(somePage);
  pages.remove(someOtherPage);
} finally {
  writeLock.unlock();
}

Eine Ausnahme für die Locking-Regel stellen die Komfortmethoden für die Seitenliste von Document dar: getPage(index, true), und addPage(page) erledigen beide das Locking implizit.

Der Dokumentzustand (State)

Eine weitere Eigenschaft des Documents ist sein Zustand. Auf diesen kann durch getState/setState zugegriffen werden. Mögliche Werte für den Zustand müssen dem Marker-Interface Document.State genügen; standardmäßig existieren jedoch bereits eine Reihe von vordefinierten Zuständen im Enum Document.BasicState. Diese auf den ersten Blick vielleicht etwas umständliche Form der Modellierung erlaubt es uns, den Dokumentzustand einerseits typsicher aber andereseits dennoch erweiterbar zu halten. Genutzt wird der Zustand des Dokumentes von den Standardkomponenten z.B. um ihnen zu signalisieren, ob das Dokument sich in einem "stabilen" Zustand befindet, oder ob es gerade noch geladen wird.

UIDocument und SwingUIDocument

Um den Rahmen dieses Artikels nicht zu sprengen, möchte ich die Betrachtung des Documents für heute mit einer, wie wir finden, sehr eleganten Lösung für die Darstellung von Documents und deren Seiten in einer grafischen Oberfläche beschließen. Ein Problem hierbei ist generell, dass z.B. die Seitenliste Änderungen unterworfen sein kann, die nicht auf dem EventDispatchThread (EDT) von AWT/Swing oder SWT initiiert wurden. Die Herausforderung besteht nun darin, stets eine stabile und konsistente Version der Seitenliste für den EDT vorhalten zu müssen und diese Repräsentation nur so zu aktualisieren, dass die Änderungen jeweils während eines EDT-Zyklus für den EDT sichtbar werden. Um dies nicht wieder und wieder implementieren zu müssen, bietet jadice 5 eine Fassade, die dies erledigt und einem beliebigen Dokument vorangestellt werden kann: das UIDocument bzw. für AWT/Swing das SwingUIDocument. Die Implementierung stützt sich unter anderem auf die GlazedLists-Basisklasse ThreadProxyEventList bzw. SwingThreadProxyEventList. Benutzt man ein solches UIDocument zur "Versorgung" eines Objektes einer der Benutzeroberfläche, so vereinfacht sich diese Aufgabe gravierend:

  • Alle Änderungen der Seitenliste werden immer im Rahmen eines EDT-Zyklus sichtbar.
  • Die Notwendigkeit zum Locking bzw. zur Synchronisation entfällt für alle rein lesenden Zugriffe-
  • Auch die Benachrichtigung über Änderungen über die Listener erfolgt immer auf dem EDT.
  • Dadurch können Oberflächenelemente direkt als Reaktion auf diese Änderungen aktualisiert werden, ohne SwingUtilities.invokeLater(...) o.Ae. benutzen zu müssen.

Wie werden UIDocuments nun konkret benutzt? Aus Performancegründen ist es wünschenswert, zu einem gegebenen Document generell immer nur ein einziges UIDocument vorzuhalten, ganz gleich, wieviele Oberflächenelemente bedient werden müssen. Wir haben deshalb den Zugriff auf das SwingUIDocument in eine statische "Getter-Methode" gekapselt: SwingUIDocument.get(myDocument). Code, der das UIDocument verwendet, könnte z.B. wie folgt aussehen:

// z.B. während der Initialisierung einer Applikation...
myDocument = new BasicDocument();
// [...]

// während der Initialisierung einer Swing-Oberflächenkomponente...
myComponent.add(new JLabel("Anzahl Seiten: "));

// Seitenanzahl darstellen und aktuell halten
final UIDocument<Component> uiDocument = SwingUIDocument.get(myDocument);
final JLabel pageCountLabel = new JLabel(
Integer.toString(uiDocument.getPageCount())
);
uiDocument.addDocumentListener(new DocumentAdapter() {
@Override
public void listChanged(ListEvent<Page> listEvent) {
pageCountLabel.setText(Integer.toString(uiDocument.getPageCount()));
}
});
// [...]

// Später irgendwann, auf einem beliebigen Thread...
myDocument.addPage(somePage);
myDocument.addPage(someOtherPage);
// [...]