Wie es unter Java üblich ist werden die einzulesenden Daten der jadice document platform in Form von
InputStream
s übergeben. Im Lesevorgang entstehen daraus
Seitensegmente. Deren zugeordnete PageSegmentSource
referenziert den Datenstrom,
aus dem das PageSegment
entstand. Darüber hinaus können weitere Objekte
entstehen, die ebenfalls den originären Datenstrom (oder einen daraus entstandenen
Sub-Datenstrom) referenzieren. Attachments sind hierfür ein typisches
Beispiel.
Um ein effizientes Speichermanagement und günstiges Laufzeitverhalten zu erreichen, werden die Inhalte von Seitensegmenten typischerweise spät initialisiert. Seiten, die vom Benutzer nie angezeigt werden, müssen unter Umständen nie vollständig geladen werden. Wo dies angebracht ist, werden aufbereitete Informationen im Cache abgelegt. Können sie dort nicht gefunden werden – zum Beispiel weil sie aufgrund von Speicherknappheit automatisch entfernt wurden – müssen sie ebenfalls neu aus dem Dokumentdatenstrom aufbereitet werden. Diese Zusammenhänge führen dazu, dass Datenströme auch nach Abschluss des initialen Lesevorgangs geöffnet bleiben müssen, sodass jederzeit Zugriff darauf genommen werden kann.
Der korrekte Zeitpunkt für das Schließen eines Datenstroms kann durch die jadice document platform nicht automatisch erkannt werden. Es ist Aufgabe der Integration sicherzustellen, dass ein Dokument-Datenstrom erst dann geschlossen wird, wenn keines der aus ihm entstandenen Seitensegmente mehr verwendet wird. Falls weitere Objekte aus dem Lesevorgang entstanden – beispielsweise Attachments – müssen diese ebenfalls in der Entscheidung bedacht werden. Solange sie genutzt werden, muss der Datenstrom offen bleiben.
Die Umsetzung dieser Anforderung kann nur integrationsspezifisch erfolgen und wird je nach Umgebung unterschiedlich ausfallen. Um eine geeignete Lösung zu erarbeiten bieten die folgenden Fragestellungen einen hilfreichen Ausgangspunkt:
-
Welche Daten werden aus dem Dokumentdatenstrom gelesen? In der Hauptsache sind dies Seitensegmente. Es entstehen aber auch Metadaten, in manchen Fällen Attachments oder Outlines, sowie unter Umständen integrationsspezifische Zusatzdaten.
-
Wo werden die Daten verwendet? Seitensegmente können in mehreren Seiten vorkommen. Diese wiederum können Teil mehrerer Dokumente sein. Auch Hintergrundprozesse, wie beispielsweise Druckvorgänge, können die Dokumentdaten verwenden und sind daher relevant für diese Betrachtung.
-
Wie kann erkannt werden, dass die Verwendung abgeschlossen ist? In manchen Fällen – beispielsweise in einer Stapelverarbeitung – gibt es aus dem Programmablauf heraus einen klaren Zeitpunkt dafür. In anderen Fällen müssen Informationen über die Verwendung gesammelt und verwaltet werden. Diese können beispielsweise zur Laufzeit in einer zentralen Datenstruktur gesammelt werden. Alternativ besteht die Möglichkeit, Statusinformationen direkt mit dem Dokument zu transportieren über den Dokumenstatus oder die User Properties.
-
Wer hat die Verantwortung dafür, dass Datenströme geschlossen werden? In vielen Fällen kann die Freigabe von Datenströmen ausgelöst werden, wenn ein Dokument in den Zustand
Document.BasicState
.CLOSED
wechselt. Die Freigabe kann entweder dezentral für jedes Dokument erfolgen, oder von einer zentralen Stelle aus.
Zur effizienten und speicherschonenden Verarbeitung großer Datenmengen
versucht die jadice document platform, sofern es das Dokumentformat erlaubt, nur die für die
aktuelle Seitendarstellung benötigten Daten zu lesen, zu verarbeiten und
dynamisch zu puffern, statt alle Dokumentdaten komplett im Speicher zu halten.
Für dieses Vorgehen werden Datenströme benötigt, deren Dateizeiger beliebig
positioniert werden kann. In jadice stehen zu diesem Zweck die
SeekableInputStream
s zur Verfügung. Die Klasse SeekableInputStream
ist
eine abstrakte Basisklasse, welche die Klasse InputStream
um folgende Methoden erweitert:
seek(long)
-
Setzen der Leseposition im Datenstrom
length()
-
Länge der Daten in Bytes. Falls die Länge nicht bekannt ist, wird der Wert
-1
zurückgegeben. getStreamPosition()
-
Aktuelle Leseposition innerhalb des Datenstroms
Die Verwendung von SeekableInputStream
s stellt sich für Integratoren
einfach dar. Für bestimmte Typen von Datenströmen stellt jadice verschiedene
Arten von SeekableInputStream
s zur Verfügung, die den eigentlichen Datenstrom
umhüllen. Abhängig vom Anwendungsfall und dem Typ des Eingangsdatenstroms sollte
die passende Art von SeekableInputStream
gewählt werden.
Die gebräuchlichsten SeekableInputStream
s werden in den folgenden
Abschnitten kurz erläutert.
RandomAccessFileInputStream
stellt einen SeekableInputStream
für
lokal (als Datei) verfügbare Bilddaten dar.
Die Klasse bietet einen Konstruktor mit einem
File
-Objekt, das auf die Bilddatei zeigt, zur
Instantiierung an. Diese Datei wird mit einem bestimmten
FileInputStream
geöffnet, der für die folgenden
Lesevorgänge gehalten wird. Während der Verarbeitung in jadice bleibt der
FileInputStream
zu der Bilddatei offen und ein
Lesezeiger wird innerhalb des Streams positioniert. Zu beachten ist hierbei,
dass die Quelldatei, während das Dokument im Viewer geöffnet ist, nicht
verändert werden darf. Dies führt, zum Beispiel unter Windows,
gegebenenfalls auch zur Sperrung des Zugriffes auf die betreffende
Datei.
Zur Verarbeitung der Bilddaten in jadice wird eine temporäre Datei erzeugt, in der bereits gelesene Daten abgelegt werden. Nach Gebrauch, spätestens jedoch beim nächsten Start der jadice document platform, werden die nicht mehr benötigten temporären Dateien gelöscht. Falls durch den Integrator keine abweichende Konfiguration vorgenommen wird, werden die temporären Dateien im durch das System vorgegebenen Temporär-Verzeichnis abgelegt.
Zur Instantiierung bietet der FileCacheInputStream
einen Konstruktor,
der einen gegebenen InputStream
aufnimmt und eine
Bearbeitungsdatei im gesetzten Temporär-Verzeichnis anlegt. Ein weiterer
Konstruktor bietet die Möglichkeit neben dem
InputStream
ein Temporär-Verzeichnis und ein
Flag, ob die erzeugte Datei nach Gebrauch zu löschen ist, anzugeben.
Bilddaten werden bei diesem Stream-Typ im Hauptspeicher gehalten. Ähnlich
dem FileCacheInputStream
bietet diese Klasse einen Konstruktor, der einen
gegebenen InputStream
aufnimmt und für Lesevorgänge
hält. Ein zweiter Konstruktor erlaubt zusätzlich eine Angabe der Blockgröße,
in der Daten im Hauptspeicher gepuffert werden sollen. Diese Art der
internen Datenverwaltung wird in Security-Relevanten Kontexten
bevorzugt.
Ein Vorteil dieser Datenhaltung ist ein extrem schneller Zugriff auf die Dokumentrohdaten. Nachteilig ist ein erhöhter Speicherbedarf durch die Datenhaltung im Hauptspeicher.
Ein Dokument, das der Viewer anzeigen soll, erhält er zum Beispiel als PDF- oder TIFF-Strom. Der Viewer muss an verschiedene Stellen im Datenstrom springen können, um die Daten einer bestimmten Seite des Dokuments anzeigen zu können, andernfalls müsste man den gesamten Datenstrom im Hauptspeicher halten.
Die Datenströme werden typischerweise von einem Server – beispielsweise aus
einem Langzeitarchiv – geladen. Da der Server viele Clients versorgt, ist es
wünschenswert die Daten möglichst schnell vollständig abzuholen um dann den
InputStream
zu schließen.
Beide Gründe sprechen dafür, einen Zwischenspeicher für Datenströme zu schaffen, die aktuell angezeigt werden. Im vorhergehenden Abschnitt wurden verschiedene Klassen genannt, die diesem Zweck dienen. Darüberhinaus bietet jadice Komponenten, die eine einefache Umsetzung der Datenzwischenspeicherung ermöglichen. Sie erfüllen die beiden genannten Anforderungen: Es entstehen Datenströme in denen gesprungen werden kann und der Original-Datenstrom kann geschlossen werden, sobald er vollständig im Puffer liegt.
Die Klasse IOUtils
bietet den Einsprungspunkt für die Arbeit mit
gepufferten Daten.
Wenn kein BufferManager verwendet wird, wird die Klasse
RandomAccessFileInputStream
eingesetzt. Dann wird im Verzeichnis für
Temporäre Dateien pro Stream eine Datei angelegt. Der Nachteil dieser
Vorgehensweise ist, dass viele Dateien erzeugt werden und im Ordner liegen.
Diese müssen danach aufgeräumt werden und vertrauliche Daten, die eigentlich
nur angezeigt werden sollten, liegen unverschlüsselt im Dateisystem. Für
einfachere Anwendungsfälle handelt es sich aber um eine funktionale und
leicht umsetzbare Lösung.
Mit dem BufferManager werden gelesene Daten im Hauptspeicher und einer temporären Datei gepuffert. Wenn der gepufferte Stream geschlossen wird, werden die enstprechenden Bereiche zum Überschreiben freigegeben. Dies geschieht, wenn es keine Referenzen mehr auf den gepufferten Datenstrom gibt beziehungszweise zum Zeitpunkt der Garbage Collection.
Das Einlesen der Dokumente erfolgt mithilfe der Methode
IOUtils
.wrap()
. Voraussetzung ist, dass der
BufferManager mithilfe der Klasse BufferManagerConfigurer
korrekt
konfiguriert wurde. Ein Beispiel befindet sich unter Beispiel 7.9, „Puffern von Stream-Inhalten mittels BufferManager“
Das Verhalten bei der Pufferung sieht folgendermaßen aus:
-
Falls es einen freien Bereich im Hauptspeicherpuffer gibt, wird dieser verwendet.
-
Falls der Hauptspeicherbereich keinen freien Platz besitzt, wird der am wenigsten kürzlich verwendete Puffer in die Pufferdatei geschoben.
-
Falls weder in Hauptspeicher noch in der Pufferdatei ein freier Bereich existiert, wird die Pufferdatei vergrößert.
-
Die Pufferdatei wird nicht verkleinert.
-
Freigegebene Bereiche in der Datei werden wieder beschrieben.
-
Beim Neustart der Anwendung wird eine bestehende Pufferdatei gelöscht und eine neue Pufferdatei mit der Größe 0 angelegt.
Es erfolgt ein hierarchisches Speichermanagement. Was kürzlich gebraucht wurde, liegt im Hauptspeicher, wenn der Hauptspeicher voll ist, werden die Daten in eine einzige Datei (für alle gepufferten Inhalte) ausgelagert. Es kann mehrere Buffer geben. Bei einem Buffer handelt es sich um einen physikalischen Datenzwischenspeicher, der genutzt wird, während Daten von einem Ort an einen anderen übertragen werden. Die Größe der Buffer ist einstellbar, 64kB ist der Standard. Die Auslagerung der Daten erfolgt entweder über normales FileIO, oder über MemoryMapping. Beim MemoryMapping wird jedem Abschnitt in der Datei ein bestimmter Buffer zugeordnet. Dadurch wird, wenn in einen bestimmten Buffer geschrieben wird, die Datei an der jeweiligen Stelle aktualisiert. Das Lesen des Buffers erfolgt direkt aus der Datei. Vorteil des MemoryMapping ist, dass es eine Kopier-Operation weniger erfordert.
Beim MemoryMapping wird nur eine einzige Datei pro jadice-Prozess beziehungsweise JVM angelegt. Die Datei wird bei einem ordentlichen Herunterfahren gelöscht. Falls beim Shutdown die Datei noch von Prozessen genutzt wird, wird sie (abhängig vom Betriebssystem) gegebenenfalls zu diesem Zeitpunkt nicht gelöscht. Dann wird die Datei beim nächsten Start gelöscht. Es gibt ein Implementierungslimit unter Windows.[28]
Der BufferManager stellt die folgenden Konfigurationsmöglichkeiten zur Verfügung:
-
Die Größe des Puffers im Hauptspeicher
-
Anzahl der Puffer
-
Wahl der Pufferdatei
-
Speicherstrategie in der Pufferdatei
-
Lesbarkeit beziehungsweise Verschlüsselung der Pufferdatei
Man kann den BufferManager programmatisch konfigurieren: „Datenströme und Verwaltung von Ressourcen“
[28] Durch die Beschränkungen der J2SE Plattform ist es auf Windowssystemen nicht möglich, die Pufferdatei während des Herunterfahrens zu löschen. Siehe
beziehungsweise