Anwendung / Funktionalität
Worker sind serverseitige Implementierungen, die bestimmte Aufgaben
erledigen, die in der Regel rechenintensiv und ressourcenaufwändig sind.
Dazu gehört z. B. die Generierung von großen Dokumenten, das Erstellen
von Datenströmen für die Anzeige / Drucken / Seitenvorschau / -auswahl
usw. Die Worker werden von einem Client durch korrespondierende Node
s
angesprochen und mit Daten bedient.
Die Superklasse für eine Node-Implementation ist die Klasse Node
, für
eine Worker-Implementation NodeWorker
.
Eine ausführliche Beschreibung der in jadice server vordefinierten Nodes
kann der beigelegten Javadoc-Dokumentation entnommen werden. Wie eigene
Node
s und NodeWorker
implementiert werden können, um die
Funktionalität des jadice servers zu erweitern, wird im Abschnitt
Implementierung eigener Nodes /
Worker anhand eines Beispiel
beschrieben.
Aufgabenstellung clientseitig
Clientseitig wird ein Job
erstellt, welchem dann eine oder mehrere
Node
-Implementationen mit den benötigten Daten (Konfiguration /
Datenströme) gesetzt werden. Dieser Job nimmt dann Kontakt zum Server
auf und setzt dort die Nodes in eine Warteschlange. Durch die asynchrone
Kommunikationsschnittstelle wird der Client während der Abarbeitung
durch den Server nicht blockiert.
Die Nodes bilden einen gerichteten, azyklischen Graphen und definieren dadurch den Workflow (z.B. lädt Node 1 Daten und Node 2 bearbeitet diese dann, Node 3 sendet die Daten schließlich an den Client zurück). Wie dies zu implementieren ist, wird in Abschnitt Anwendungsszenarien samt Code-Beispielen anhand einiger typischer Beispiele verdeutlicht.
Aufgabenstellung serverseitig
Der jadice server erstellt aus den vom Job übermittelten Node
s einen
Workflow. Dabei werden die miteinander verketteten Node
s nacheinander
abgearbeitet, indem die korrespondierenden NodeWorker
gestartet
werden. Die erzeugten Daten werden über StreamBundle
-Objekte an den
jeweils nächsten Worker weitergereicht.
Anwendungsszenarien samt Code-Beispielen
Die Anwendungsszenarien und Konfigurationsmöglichkeiten für den jadice server sind ebenso zahlreich wie vielfältig. Deshalb sollen hier nur die gängigsten Szenarien aufgegriffen werden, die als Ausgangspunkt für eigene Implementierungen dienen können.
In den meisten Fällen sind diese nach dem Schema
Datei vom Client empfangen
Serverseitige Verarbeitung
Ergebnis zum Client zurücksenden
aufgebaut. Dies ist der Einfachheit der Beispiele geschuldet. In realen Anwendungen werden die Ergebnisse üblicherweise nicht direkt nach der Verarbeitung zurückgesendet, sondern in kaskadierten Schritten verarbeitet. Auch die Quelle und das Ziel der Datenströme sind nicht zwingenderweise der Client, sondern können beispielsweise ein zentraler Datei-, Mail-, Archivserver o. ä. sein.
Erstellen eines Server-Jobs
Der Job
wird zunächst clientseitig erstellt, um ihm anschließend 1...n Nodes anzuhängen.
Konfigurationsmöglichkeiten des Jobs sind:
JobListener
-Implementierung, siehe Abschnitt Erstellung eines JobListenersTimeLimit
und ähnlicheLimit
s, siehe Abschnitt Konfiguration von LimitsWorkflow (= azyklischer Graph, der durch miteinander verkettete
Node
s definiert wird)Typ: Mit diesem Bezeichner werden
Job
s von der gleichen Art ausgezeichnet. Serverseitig wird diese Angabe u.a. zu statistischen Zwecken verwendet.
Um einen Job erstellen zu können, muss zunächst eine JMSJobFactory
erzeugt und initialisiert werden. Diese stellt die Verbindung zum
Messagingsystem her und ist für die weitere Kommunikation zwischen der
Client-Anwendung und jadice server verantwortlich. Die Funktion
createServerJob()
in Beispiel 6.1. JobFactory initialisieren und Job erstellen (mit ActiveMQ als Messagebroker)
wird als Basis für die kommenden Beispiele verwendet:
final ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616");
final JMSJobFactoryProvider jmsJobFactoryProvider = new JMSJobFactoryProvider(activeMQConnectionFactory, JMSJobFactory.DEFAULT_QUEUE_NAME);
public Job createServerJob() throws JobException, ConnectionException, IOException {
final JMSJobFactory jmsJobFactory = jmsJobFactoryProvider.get();
return jmsJobFactory.createJob();
// Beim erstellten Job muss nach Beendigung des Jobs job.close() aufgerufen werden oder die Methode createServerJob() in ein try-with-resources Statement gepackt werden.
// Wirft ConnectionException, wenn ein Verbindungsproblem mit dem Server auftritt.
// Wirft JobException oder IOException wenn bei Abarbeitung des Jobs ein Fehler auftritt.
}
Beispiel 6.1. JobFactory initialisieren und Job erstellen (mit ActiveMQ als Messagebroker)
Wurde die JobFactory
korrekt initialisiert, ist sie dafür zuständig,
alle nachfolgenden Jobs zur Konvertierung zu erzeugen.
// Create a job
try (Job job = createServerJob()) {
// Apply a timeout limit:
job.apply(new TimeLimit(60, TimeUnit.SECONDS));
// Declare the job type
job.setType("first-example");
// Attach a JobListener (see below)
job.addJobListener(…);
// Assemble the workflow (see below)
job.attach(…);
// Perform the job
job.submit();
}
Beispiel 6.2. Job konfigurieren und ausführen
Sind alle über diese JobFactory
erstellten Jobs beendet worden und
sollen keine weiteren Jobs mehr erstellt werden, so muss die Verbindung
zum Messagingsystem getrennt werden. Andernfalls werden Verbindungen
nicht freigegeben und es kann zu einem Ressourcen-Leck kommen.
public void disconnect() {
if (jobFactory != null) {
jobFactory.close();
jobFactory = null;
}
}
Beispiel 6.3. Beenden der JobFactory am Ende ihres Lebenszyklus
Erstellung eines JobListeners
Mit dem JobListener
können Zustände des Job
s und serverseitige
Fehlermeldungen verarbeitet werden.
public class MyJobListener implements JobListener {
public void stateChanged(Job job, State oldState, State newState) {
dump("stateChanged", job, oldState, newState, null, null);
}
public void executionFailed(Job job, Node node, String messageId, String reason, Throwable cause) {
dump("executionFailed", job, node, messageId, reason, cause);
}
public void errorOccurred(Job job, Node node, String messageId, String message, Throwable cause) {
dump("errorOccurred", job, node, messageId, message, cause);
}
public void warningOccurred(Job job, Node node, String messageId, String message, Throwable cause) {
dump("warningOccurred", job, node, messageId, message, cause);
}
public void subPipelineCreated(Job job, Node parent, Set<? extends Node> createdNodes) {
dump("subPipelineCreated", job, parent);
}
private void dump(String ctx, Job job, Object... args) {
System.err.println("Context: " + ctx);
System.err.println("Job: " + job.toString());
if (args == null) {
return;
}
for (Object arg : args) {
System.err.println(" " + arg.toString());
}
}
}
Beispiel 6.4. Beispiel einer JobListener-Implementation
jadice server liegen zwei Implementierungen dieses Interfaces bei, die in der Integration eingesetzt werden können:
Leitet Fehlermeldung via Apache Commons Logging an das Client-Log weiter
Leere Standard-Implementierung des JobListener
-Interfaces. Davon
abgeleitete Klassen müssen nur die gewünschten Methoden
überschreiben
Konfiguration von Limits
Um den Ressourcen-Verbrauch eines Job
s oder dessen Knoten zu
beschränken, ist es sinnvoll, Limit
s darauf anzuwenden. Folgende
Limit
s sind hierbei möglich:
Typ des Limits | Beschreibung | Kontext | |
---|---|---|---|
Job |
Node |
||
TimeLimit |
Maximale Verarbeitungszeit |
✅ | ✅ |
Maximale Anzahl an |
☐ | ✅ | |
Maximale Größe der |
☐ | ✅ | |
Maximale Anzahl an Seiten eines generierten Dokuments |
☐ | ✅[a] | |
Maximale Anzahl an Knoten, die ein |
✅ | ❌ | |
[a] Für Knoten, die Dokumente generieren, die einen Seiten-Begriff kennen; vgl. javadoc |
Verfügbare Limit
s
✅ | wird direkt an dieser Stelle berücksichtigt |
❌ | wird nicht an dieser Stelle berücksichtigt |
☐ | wird nicht berücksichtigt, aber an die Knoten vererbt (s.u.) |
Bei der Definition eines Limit
s kann ausgewählt werden, was passieren
soll, wenn es überschritten wird:
TimeLimit tl = new TimeLimit(60, TimeUnit.SECONDS);
tl.setExceedAction(WhenExceedAction.ABORT); // default action
NodeCountLimit ncl = new NodeCountLimit(20);
ncl.setExceedAction(WhenExceedAction.WARN);
Beispiel 6.5. Beispielhafte Verwendung von Limits
Bei der Limit.WhenExceedAction
ABORT bricht der komplette Job ab, bei
der Limit.WhenExceedAction
WARN wird dem Client eine Warnung gemeldet.
Da bei der clientseitigen Workflow-Definition möglicherweise nicht alle Knoten bekannt sind oder es nicht sinnvoll ist, jeden einzelnen Knoten mit Limits zu belegen, können diese auch zentral auf den Job angewendet werden. Diese werden an die einzelnen Knoten vererbt. Dabei gelten folgenden Vererbungsregeln:
Limit
s mit derLimit.WhenExceedAction
WARN werden in jedem Fall vererbt.Limit
s mit derLimit.WhenExceedAction
ABORT werden nicht an Knoten vererbt, auf die bereits ein Limit der selben Klasse mit derLimit.WhenExceedAction
ABORT angewendet wurde, auch wenn dieses weniger restriktiv ist.Werden
Limit
s der selben Klasse mitLimit.WhenExceedAction
ABORT sowohl clientseitig als auch über die Security-Schnittstelle gesetzt, haben die restriktiveren Vorrang. Vgl. Abschnitt ???
Identifikation unbekannter Eingabedaten
Der jadice server bietet mächtige Module zur Erkennung unbekannter Dateiformate. Diese werden in den Modulen zur automatischen Konvertierung unbekannter Dateien bzw. E-Mails eingesetzt (siehe Abschnitte Konvertierung unbekannter Eingabedaten in ein einheitliches Format (PDF) und Konvertierung von E-Mails nach PDF).
Darüber hinaus ist es auch möglich, diese Module durch den
StreamAnalysisNode
anzusprechen und für eigene Zwecke zu verwenden.
try (Job job = createServerJob()) {
job.setType("run stream analysis");
// Instantiate nodes:
// 1. data input node
StreamInputNode siNode = new StreamInputNode();
// 2. analysis node
StreamAnalysisNode saNode = new StreamAnalysisNode();
// 3. output node
StreamOutputNode soNode = new StreamOutputNode();
// Assemble the workflow
job.attach(siNode.appendSuccessor(saNode).appendSuccessor(soNode));
// Perform the job and send data
job.submit();
siNode.addStream(…);
siNode.complete();
// Wait for server reply
for (Stream stream : soNode.getStreamBundle()) { (1)
// Reading the meta data
final StreamDescriptor descr = stream.getDescriptor();
final String mimeType = descr.getMimeType();
}
}
Beispiel 6.6. Verwendung des StreamAnalysisNode
(1) The method getStreamBundle() is blocking until jadice server has finished the job. For working in an asynchronous way, a StreamListener can be implemented
Extraktion von Dokument-Informationen
Der JadiceDocumentInfoNode
analysiert ein Dokument. Dazu wird das
Dokument mit der jadice document platform geladen und Metadaten
extrahiert. Mit diesen Informationen wird der StreamDescriptor
angereichtert und an den nächsten Knoten als IDocumentInfo
weitergegeben. Das jeweils untersuchte Format muss dabei von der jadice
document platform 5 unterstützt werden.
Als einfachstes Beispiel kann diese Information mit Hilfe des
NotificationNode
s direkt an den Client übergegeben und dort auf die
Konsole ausgegeben werden.
try (Job job = createServerJob()) {
job.setType("retrieve document info");
// Instantiate info node
JadiceDocumentInfoNode infoNode = new JadiceDocumentInfoNode();
// Create a listener and attach it to a NotificatioNode
DocumentInfoListener documentInfoListener = new DocumentInfoListener();
NotificationNode notifyNode = new NotificationNode();
notifyNode.addNotificationResultListener(documentInfoListener);
// Assemble the workflow
StreamInputNode siNode = new StreamInputNode();
siNode.appendSuccessor(infoNode);
infoNode.appendSuccessor(notifyNode);
// Discard the data at the end of the analysis:
notifyNode.appendSuccessor(new NullNode());
// Perform the job
job.attach(siNode);
job.submit();
// Submit the data to jadice server and end transmission
siNode.addStream(…);
siNode.complete();
// Wait for server reply (see above)
documentInfoListener.waitForDocumentInfo();
// Retrieve and dump document info:
IDocumentInfo documentInfo = documentInfoListener.getDocumentInfo();
System.out.println("Number of pages : " + documentInfo.getPageCount());
// As example here: Details of the first page
System.out.println("format : " + documentInfo.getFormat(0));
System.out.println("size (pixels) : " + documentInfo.getSize(0).getWidth() + "x"
+ documentInfo.getSize(0).getHeight());
System.out.println("resolution (dpi): " + documentInfo.getVerticalResolution(0) + "x"
+ documentInfo.getHorizontalResolution(0));
}
Beispiel 6.7. Verwendung des JadiceDocumentInfoNode
public class DocumentInfoListener implements NotificationListener {
/**
* DocumentInfo will be generated by the JadiceDocumentInfoNode and attached to the StreamDescriptor
*/
private IDocumentInfo documentInfo;
/**
* Latch in order to block the current thread until {@link #documentInfo} is available.
* NOTE: This example does not perform any error handling if the job aborts or no result is available!
*/
private CountDownLatch latch = new CountDownLatch(1);
@Override
public void notificationReceived(StreamDescriptor streamDescriptor) {
final Serializable prop = streamDescriptor.getProperties().get(JadiceDocumentInfoNode.PROPERTY_NAME);
if (prop != null && prop instanceof IDocumentInfo) {
documentInfo = (IDocumentInfo) prop;
latch.countDown();
}
}
public void waitForDocumentInfo() throws InterruptedException {
// Block until documentInfo is available
latch.await();
}
public IDocumentInfo getDocumentInfo() {
return documentInfo;
}
}
Beispiel 6.8. In Beispiel 6.7, „Verwendung des JadiceDocumentInfoNode“ verwendeter NotificationNode.NotificationListener
Zusammenfassen mehrerer PDF-Dokumente
Mit dem PDFMergeNode
ist es möglich, mehrere PDF-Dokumente zu einem
einzigen zusammenzufassen.
try (Job job = createServerJob()) {
job.setType("merge pdfs");
// Instantiate nodes:
// 1. data input node
StreamInputNode siNode = new StreamInputNode();
// 2. merge input data (1...n streams to a single stream)
PDFMergeNode pmNode = new PDFMergeNode();
// 3. output node
StreamOutputNode soNode = new StreamOutputNode();
// Assemble the workflow and perform the job
job.attach(siNode.appendSuccessor(pmNode).appendSuccessor(soNode));
job.submit();
// Send PDF documents
siNode.addStream(…);
siNode.addStream(…);
// ... possible further PDF documents
// Signalise the end of input data
siNode.complete();
// Wait for server reply
for (Stream stream : soNode.getStreamBundle()) {
// Reading the data
InputStream is = stream.getInputStream();
// Work with this data (not shown)
…
}
}
Beispiel 6.9. Verwendung des PDFMergeNode
Konvertierung nach TIFF
Die meisten Konvertierungsvorgänge (z. B. über LibreOffice) erzeugen
PDF. Es ist jedoch möglich, durch das Einfügen des ReshapeNode
das
Ergebnis weiter nach TIFF zu konvertieren.
Im folgenden Beispiel wird der Workflow aus
Beispiel 6.9, "Verwendung des PDFMergeNode" verändert; anstelle
des PDFMergeNode
wird eine Konvertierung nach TIFF mit anschließender
Aggretation angehängt:
// (...)
ReshapeNode reshapeNode = new ReshapeNode();
reshapeNode.setTargetMimeType("image/tiff");
// Join all incoming data to one resulting stream
reshapeNode.setOutputMode(ReshapeNode.OutputMode.JOINED);
// Assemble the workflow and include the TIFF converter node
job.attach(siNode.
appendSuccessor(reshapeNode).
appendSuccessor(soNode));
// (...)
}
Beispiel 6.10. Konvertierung nach Tiff
Dauerhafte Verankerung von Annotationen
Um Dokumente und deren Annotationen mit Standardprogrammen anzeigen zu können, müssen diese als „normale" Objekte im Ausgangsformat verankert werden.
Dies kann ebenfalls mit dem ReshapeNode
realisiert werden. Die
notwendige Assoziation zwischen dem Dokument- und den
Annotationsdatenströmen zeigt das folgende Beispiel:
/**
* stub interface in order to bundle a document and its annotations
*/
interface DocumentAndAnnotations {
InputStream getContent();
List<InputStream> getAnnotations();
}
public void convert(DocumentAndAnnotations doc) throws JMSException, JobException, IOException {
try (Job job = createServerJob()) {
job.setType("imprint annotations");
// Instantiate nodes:
StreamInputNode inputNode = new StreamInputNode();
ReshapeNode reshapeNode = new ReshapeNode();
StreamOutputNode outputNode = new StreamOutputNode();
// Define the target MIME type (e.g. PDF)
reshapeNode.setTargetMimeType("application/pdf");
// Associate the annotations streams with the content
reshapeNode.setOutputMode(ReshapeNode.OutputMode.ASSOCIATED_STREAM);
// Assemble the workflow and perform the job
job.attach(inputNode.appendSuccessor(reshapeNode).appendSuccessor(outputNode));
job.submit();
// Sending document content (with explicitly declared MIME type here)
final StreamDescriptor contentSD = new StreamDescriptor("application/pdf");
inputNode.addStream(doc.getContent(), contentSD);
// Process annotations:
for (InputStream annoStream : doc.getAnnotations()) {
StreamDescriptor annoSD = new StreamDescriptor();
// Associate document and annotation:
annoSD.setParent(contentSD);
// Declare the annotations' MIME type (e.g. Filenet P8):
annoSD.setMimeType(ReshapeNode.AnnotationMimeTypes.FILENET_P8);
// Send annotation stream
inputNode.addStream(annoStream, annoSD);
}
// Signalise the end of input data
inputNode.complete();
// Handle the job result (not shown)
…
}
Beispiel 6.11. Dauerhafte Verankerung von Annotationen
In dieser Konfiguration sind zwei Einstellungen besonders relevant:
Dokument- und Annotationsdatenströme müssen über die
StreamDescriptor
-Hierarchie miteinander verknüpft werden. Dazu muss
der StreamDescriptor
des Dokuments als Parent der StreamDescriptor
en
der Annotationen gesetzt werden.
Für die verfügbaren MIME-Types von Annotationen gibt es in der Klasse
ReshapeNode
vordefinierte Konstanten, die zwingend gesetzt werden
müssen. Weitere Informationen über die Annotationsformate und deren
Eigenschaften können Sie dem Annotationshandbuch der jadice document
platform 5 entnehmen.
Vertrauliche Dokumentinhalte
Bitte beachten Sie, dass keine Analyse des Dokumentinhalts stattfindet. Inhalte, die von Annotationen verdeckt werden, können je nach Datenformat weiterhin im Zieldatenstrom vorhanden sein. Außerdem ist es möglich, dass im zweiten Fall unerwünschte (Meta-)Daten im Dokument verbleiben können, die einer Vertraulichkeit unterliegen.
Entpacken von Archivdateien
Um die Netzwerklast zu verringern, werden Dateien häufig komprimiert. Diese können vor der Weiterverarbeitung durch den jadice server entpackt werden. Dies geschieht je nach Dateiformat in unterschiedlichen Nodeklassen:
Dateiformat | Nodeklasse | Bemerkung |
---|---|---|
ZIP | UnZIPNode |
|
RAR |
UnRARNode |
|
GZIP |
UnGZIPNode |
|
TAR |
UnTARNode |
Wie dies für den UnZIPNode
aussieht, zeigt folgendes Codebeispiel:
try (Job job = createServerJob()) {
job.setType("unpack zip");
// Instantiate nodes:
// 1. data input node
StreamInputNode siNode = new StreamInputNode();
// 2. unpacking of ZIP archives
UnZIPNode unzipNode = new UnZIPNode();
// 3. output node
StreamOutputNode soNode = new StreamOutputNode();
// Assemble the workflow
job.attach(siNode.appendSuccessor(unzipNode).appendSuccessor(soNode));
// Perform the job
job.submit();
// Send data
siNode.addStream(…);
// Signalise the end of input data
siNode.complete();
// Wait for server reply
for (Stream stream : soNode.getStreamBundle()) {
// Reading the data: (1 stream per file in the archive)
System.out.println("file name: " + stream.getDescriptor().getFileName());
InputStream is = stream.getInputStream();
// Work with this data (not shown)
…
}
}
Beispiel 6.12. Verwendung des UnZIPNode
Konvertierung unbekannter Eingabedaten in ein einheitliches Format (PDF)
Eine Vereinheitlichung von Dokumenten ist besonders im Bereich der Langzeitarchivierung von Nutzen. Der Zugriff auf die Datenquelle, die automatische Analyse von Daten, eine zielvorgabenorientierte, dynamische Weiterverarbeitung und eine abschließende Archivierung ins Archiv bringt folgende Vorteile:
Die aufrufende Anwendung braucht keinerlei Kenntnis über Quelldateien und Formate. Es besteht keine Gefährdung durch bösartige Daten oder Dokumente. Darüber hinaus ist eine Minimierung des Netzwerktransfers die Folge. Durch seine Struktur ermöglicht es der jadice server, zu jeder Zeit das Konvertierungsergebnis flexibel zu steuern.
try (Job job = createServerJob()) {
job.setType("convert to pdf");
// Instantiate nodes:
// 1. data input node
StreamInputNode siNode = new StreamInputNode();
// 2. node for dynamic data converserion
DynamicPipelineNode dpNode = new DynamicPipelineNode();
dpNode.setRuleset(new URI("resource:/dynamic-pipeline-rules/default.xml"));
// 3. merge input data (1...n streams to a single stream)
PDFMergeNode pmNode = new PDFMergeNode();
// 4. output node
StreamOutputNode soNode = new StreamOutputNode();
// Assemble the workflow
job.attach(siNode.appendSuccessor(dpNode).appendSuccessor(pmNode).appendSuccessor(soNode));
// Perform the job and send data
job.submit();
siNode.addStream(…);
siNode.complete();
// Wait for server reply
for (Stream stream : soNode.getStreamBundle()) {
// Work with the result
InputStream is = stream.getInputStream();
…
}
}
Beispiel 6.13. Konvertierung beliebiger Datenströme nach PDF
Ergänzen Sie diesen Job um eine eigene Implementierung eines
JobListener
s, so können Sie über die Methode subPipelineCreated
erfahren, welche weiteren Node
s dynamisch von jadice server erzeugt
wurden.
Das verwendete Regelwerk finden Sie im Verzeichnis
/server-config/dynamic-pipeline-rules
. Die XML-basierten Regelwerke
können an eigene Anforderungen angepasst werden. Dazu hilft das
ebenfalls in diesem Ordner zu findenden XML-Schema.
Konvertierung von Office-Dokumenten nach PDF
try(Job job = createServerJob()) {
job.setType("libreoffice to pdf");
// Instantiate nodes:
// 1. data input node
StreamInputNode siNode = new StreamInputNode();
// 2. Conversion via LibreOffice
LibreOfficeConversionNode loNode = new LibreOfficeConversionNode();
// 3. merge input data (1...n streams to a single stream)
PDFMergeNode pmNode = new PDFMergeNode();
// 4. output node
StreamOutputNode soNode = new StreamOutputNode();
// Assemble the workflow
job.attach(siNode.appendSuccessor(loNode).appendSuccessor(pmNode).appendSuccessor(soNode));
// Perform the job and send document data
job.submit();
siNode.addStream(…);
siNode.complete();
// Wait for server reply
for (Stream stream : soNode.getStreamBundle()) {
// Reading the data
InputStream is = stream.getInputStream();
// Work with this data (not shown)
…
}
}
Beispiel 6.14. Ansteuerung von LibreOffice
Der Klassenpfad muss zur Ansteuerung von LibreOffice wie in Abschnitt Konfiguration LibreOffice beschrieben gesetzt werden.
Dokumente im Word2007-Format (Dateiendung
docx
) müssen vor der Konvertierung mit LibreOffice durch denStreamAnalysisNode
vorverarbeitet werden (vgl. Abschnitt Identifikation unbekannter Eingabedaten).
Konvertierung von E-Mails nach PDF
Bei der E-Mailkonvertierung wird die E-Mail direkt vom Mailserver geholt. Hierzu müssen die entsprechenden Zugriffsdaten angegeben werden.
Der Vorgang ist ähnlich der dynamischen Konvertierung (siehe Abschnitt Konvertierung unbekannter Eingabedaten in ein einheitliches Format (PDF)). Die E-Mail wird analysiert, eventuelle Anhänge wie z.B. Office-Dokumente, Bilder usw. werden alle konvertiert, in einer Übersicht zusammengefasst und an den E-Mail-Text angehängt.
Archivdateien werden dabei ausgepackt und deren Inhalt in den Konvertierungsvorgang eingebunden.
try (Job job = createServerJob()) {
job.setType("mail to pdf");
// Instantiate nodes:
// 1. input node that retrieves the mail from a mail server
JavamailInputNode jiNode = new JavamailInputNode();
// Configuration of the mail server
jiNode.setStoreProtocol("<protocol>"); // POP3 or IMAP
jiNode.setHostName("<server>");
jiNode.setUsername("<user>");
jiNode.setPassword("<password>");
jiNode.setFolderName("<e-mail folder>");
jiNode.setImapMessageUID(…);
// 2. Perform the email conversion
ScriptNode scNode = new ScriptNode();
scNode.setScript(new URI("resource:email-conversion/EmailConversion.groovy"));
// 3. merge data (1...n streams to a single stream)
PDFMergeNode pmNode = new PDFMergeNode();
// 4. output node
StreamOutputNode soNode = new StreamOutputNode();
// Assemble the workflow and perform the job
job.attach(jiNode.appendSuccessor(scNode).appendSuccessor(pmNode).appendSuccessor(soNode));
job.submit();
// Wait for server reply
for (Stream stream : soNode.getStreamBundle()) {
// Work with the result
InputStream is = stream.getInputStream();
…
}
}
Beispiel 6.15. E-Mail-Konvertierung mit direktem Abruf vom Server
Sollen E-Mails nicht mit dem JavamailInputNode
über einen IMAP- oder
POP3-Account abgerufen, sondern z. B. als eml
-Datei eingelesen werden,
muss zusätzlich der MessageRFC822Node
zwischengeschaltet werden, der
die Abtrennung von E-Mail-Header und -Body vornimmt:
Job job = createServerJob();
// Instantiate nodes:
// 1. input node the receives the mail from the client
StreamInputNode siNode = new StreamInputNode();
// 2. Separate of mail header and mail body
MessageRFC822Node msgNode = new MessageRFC822Node();
// 3. Perform the email conversion
ScriptNode scNode = new ScriptNode();
scNode.setScript(new URI("resource:email-conversion/EmailConversion.groovy"));
// Further procedure as above
Beispiel 6.16. E-Mail-Konvertierung einer eml-Datei
E-Mails, die im MS Outlook-Format (msg
-Dateien) vorliegen, können
durch den TNEFNode
ohne Aufruf von MS Outlook in ein von jadice server
unterstütztes Format und über den diesen Weg konvertiert werden:
Job job = createServerJob();
// Instantiate nodes:
// 1. input node the receives the mail from the client
StreamInputNode siNode = new StreamInputNode();
// 2. Pre-processing of MSG files
TNEFNode tnefNode = new TNEFNode();
tnefNode.setInputFormat(InputFormat.MSG);
// 3. Perform the email conversion
ScriptNode scNode = new ScriptNode();
scNode.setScript(new URI("resource:email-conversion/EmailConversion.groovy"));
// Further procedure as above
Beispiel 6.17. E-Mail-Konvertierung einer msg-Datei
Bitte beachten Sie, dass der Mailbody in
msg
-Dateien üblicherweise als Rich Text (rtf
) vorliegt und daher in der Standardkonfiguration über LibreOffice nach PDF konvertiert wird.
In der oben gezeigten Konfiguration wird standardmäßig zu jedem
Dateianhang eine Trennseite generiert, die die Metadaten des jeweiligen
Anhangs enthält. Sind keine Trennseiten gewünscht, können diese mit der
folgenden Konfiguration des ScriptNode
s für alle Dateianhänge
deaktiviert werden:
scNode.getParameters().put("showAttachmentSeparators", false);
Eine weitere Konfigurationsmöglichkeit betrifft formatierte E-Mails.
Wurden diese sowohl im HTML- als auch Plaintext-Format gesendet, wird
standardmäßig der HTML-Teil konvertiert. Soll stattdessen der
Plaintext-Teil konvertiert werden, ist folgende Konfiguration des
ScriptNode
s vorzunehmen:
scNode.getParameters().put("preferPlainTextBody", true);
Davon unabhängig ist es möglich, denjenigen Teil, der normalerweise nicht konvertiert wird, als zusätzliches Attachment an die E-Mail anzuhängen. Somit kann die konvertierte E-Mail sowohl im HTML- als auch im Plaintext-Format dargestellt werden. Die nötige Konfiguration ist:
scNode.getParameters().put("showAllAlternativeBody", true);
Mit der folgenden Einstellen kann verhindert werden, dass jadice server Bilder und andere in E-Mails referenzierte Dateien von unbekannten Quellen nachlädt:
scNode.getParameters().put("allowExternalHTTPResolution", false);
Die Behandlung von Attachments, deren Format nicht erkannt wurde oder
nicht für die Konvertierung durch jadice server vorgesehen ist, kann
über den Parameter unhandledAttachmentAction
gesteuert werden:
scNode.getParameters().put("unhandledAttachmentAction", "failure");
Folgende Werte werden hierbei akzeptiert:
Wert | Bedeutung |
---|---|
|
Es wird eine Warnung in das Log geschrieben. |
|
Es wird ein Error in das Log geschrieben (Standardwert). |
|
Der zugehörige Job bricht mit einem Fehler ab. |
Zur Kenntlichmachung von Bilddateien, die in einer E-Mail referenziert sind, aber nicht konvertiert wurden, werden anstelle dieser folgende Platzhalter eingefügt:
Wert | Bedeutung |
---|---|
Das Bild wurde aufgrund der Einstellung
|
|
Die Bilddatei konnte nicht geladen werden. |
Standardmäßig werden Bilder im HTML Markup einer E-Mail, die für das aktuell ausgewählte PDF Seitenformat zu breit oder zu hoch sind, auf mehrere Seiten aufgeteilt.
Grundsätzlich ist bei komplexer HTML-Formatierung zu beachten, dass diese nicht originär für ein Medium gedacht ist, das seitenweise arbeitet und deshalb oft nur verlustbehaftet konvertiert und insbesondere auf Seiten umgebrochen werden muss. Darüber hinaus lässt sich HTML auch nicht immer auf eine gewisse Breite "zusammenzwängen", weshalb ggf. auf größere Seitenbreiten ausgewichen werden muss. Sind zum Beispiel Tabellen oder komplexe Layouts im HTML Markup vorhanden, so ist es nicht möglich verlustfrei eine Skalierung dieser Inhalte auf ein bestimmtes Seitenformat zu erreichen.
Um dennoch das PDF-Seitenformat, im Falle eines zu breiten oder zu hohen
Bildes im HTML-Markup, möglichst einzuhalten, können nun für die
MailBodyCreatorNode
, durch den Aufruf von setHtmlProcessingMode(),
folgende Konfigurations-Optionen gewählt werden:
Konfigurationswert | Beschreibung |
---|---|
|
Standardverhalten, bei dem zu breite und zu hohe Bilder auf mehrere Seiten aufgeteilt werden. |
|
Größenabhängige Verlagerung von Bildern in den Anhang. Diese Einstellung bewirkt, dass Bilder, die zu groß für eine Seite sind, in den Anhang des Resultats der E-Mail-Konvertierung verschoben werden. |
|
Größenabhängige Verlagerung von Bildern in den Anhang. Diese Einstellung bewirkt, dass Bilder, die breiter oder höher sind als eine vorgegebene Größe, in den Anhang des Resultats der E-Mail-Konvertierung verschoben werden. |
|
Verlagerung aller Bilder in den Anhang. |
|
Durch diese Einstellung werden Bilder, soweit möglich, auf das aktuelle PDF-Seitenformat angepasst. Dies kann nicht in allen Fällen garantiert werden, da bei komplexer HTML-Formatierung sonst ein Informationsverlust entstehen kann. |
Auflösungsreduktion von Bildern in PDF-Dokumentention
Die Dateigröße eines PDF-Dokuments kann durch darin eingebettete Bilder
stark zunehmen. Mit dem PDFImageOptimizationNode
ist es möglich, ein
bereits existierendes PDF-Dokument dahingehend zu manipulieren, dass die
Auflösung der eingebetteten Bilder auf einen DPI-Schwellenwert reduziert
wird, um eine Reduktion der Dateigröße des Dokuments zu erreichen.
Hierbei wird so vorgegangen, dass für jedes enthaltene Bild geprüft
wird, ob dessen Auflösung den als Parameter übergebbaren Schwellenwert
übersteigt. Ist dies der Fall, wird es durch ein JPEG-Bild ersetzt,
dessen Auflösung dem Schwellenwert entspricht. Die bei der
JPEG-Generierung zu verwendende Bildqualität (es handelt sich um einen
Prozentwert) kann ebenfalls als Parameter mitgegeben werden.
Bei der Berechnung der Auflösung eines Bildes spielt die Größe der
Seite, auf der sich das Bild befindet, eine wichtige Rolle. Im
Normalfall werden alle Seiten eines PDF-Dokuments die gleiche Größe
haben (z. B. A4). Da aber in PDF vorgesehen ist, dass für jede Seite
eine individuelle Größe gesetzt werden kann, kann es durchaus vorkommen,
dass ein PDF-Dokument unterschiedlich große Seiten enthält. Aufgrund
dieses Sachverhalts erlaubt der PDFImageOptimizationNode
das Setzen
einer Zielseitengröße (target page size). Wird dieser optionale
Parameter nicht gesetzt, bezieht sich die berechnete Auflösung auf die
im PDF-Dokument hinterlegte Seitengröße. Das Setzen der Zielseitengröße
ist z. B. dann sinnvoll, wenn es vor allem auf die Bildqualität im
ausgedruckten Dokument ankommt. Durch das Setzen der Zielseitengröße
kann dann möglicherweise eine wesentlich stärkere Reduktion der
Dateigröße erreicht werden.
try (Job job = createServerJob()) {
job.setType("optimize images");
// Instantiate nodes:
// 1. data input node
StreamInputNode siNode = new StreamInputNode();
// 2. optimize embedded images
PDFImageOptimizationNode imgOptimizationNode = new PDFImageOptimizationNode();
// 3. set the image resolution threshold to 150 DPI (default: 300)
imgOptimizationNode.setMaxResolution(150);
// 4. set the JPEG image quality to 80 percent (default: 75)
imgOptimizationNode.setJPEGQuality(0.8f);
// 5. set the page size of the output device (optional)
imgOptimizationNode.setTargetPageSize(PDFImageOptimizationNode.PageSize.A4);
// 6. output node
StreamOutputNode soNode = new StreamOutputNode();
// Assemble the workflow and perform the job
job.attach(siNode.appendSuccessor(imgOptimizationNode).appendSuccessor(soNode));
job.submit();
// Send PDF document
siNode.addStream(…);
// Signalise the end of input data
siNode.complete();
// Wait for server reply
for (Stream stream : soNode.getStreamBundle()) {
// Reading the data
InputStream is = stream.getInputStream();
// Work with this data (not shown)
…
}
}
Beispiel 6.18. Verwendung des PDFImageOptimizationNode
Ansteuerung externer Programme
Durch den ExternalProcessCallNode
ist die Ansteuerung externer
Programme sehr einfach möglich. Dabei kümmert sich der jadice server
darum, dass ein- und ausgehende Datenströme automatisch zu temporären
Dateien umgewandelt und diese nach der Verarbeitung durch das externe
Programm gelöscht werden.
Einzige Voraussetzung ist, dass das Programm auf dem Server über Kommandozeile angesprochen werden kann.
try (Job job = createServerJob()) {
job.setType("external my converter");
// Instantiate nodes:
// 1. data input node
StreamInputNode siNode = new StreamInputNode();
// 2. start an external process
ExternalProcessCallNode epcNode = new ExternalProcessCallNode();
// Configuration:
// - Program name (back slashes must be escaped!)
epcNode.setProgramName("C:\\Programme\\MyConverter\\MyConverter.exe");
// - Command line parameters (jadice server will substitute ${infile} / ${outfile:pdf})
epcNode.setArguments("-s -a ${infile} /convert=${outfile:pdf}");
// 3. output node
StreamOutputNode soNode = new StreamOutputNode();
// Assemble the workflow
job.attach(siNode.appendSuccessor(epcNode).appendSuccessor(soNode));
// Submit job and send data
job.submit();
StreamDescriptor sd = new StreamDescriptor();
// jadice server will use the name when it stores this file and passes it to the external program
sd.setFileName("myfile.dat");
siNode.addStream(new BundledStream(…, sd));
siNode.complete();
// Wait for server reply
for (Stream stream : soNode.getStreamBundle()) {
// Work with this data (not shown)
InputStream is = stream.getInputStream();
…
}
}
Beispiel 6.19. Verwendung des ExternalProcessCallNode
Ausfiltern von Dateien bei der Extraktion von Archiv-Dateiformaten (ZIP, RAR, 7ZIP, TAR)
Eingehende Mails und Archiv-Dateiformate, die konvertiert werden sollen, enthalten zum Teil aus fachlicher Sicht irrelevante Dateiformate. Um diese Dateien auszufiltern, besteht die Möglichkeit, regelbasiert eine Filterung der Datenströme anhand des Dateinamen vorzunehmen.
Dabei können diese Regeln in einer Konfigurationsdatei auf Serverseite definiert werden. Beispielhaft ist hier ein Auszug möglicher Regeldefinitionen aufgeführt. Die Regeldatei muss in UTF-8-Kodierung vorliegen.
Beispiel | Beschreibung |
---|---|
**/CVS/* |
Trifft auf alle Dateien in "CVS" Ordnern zu. Folgende Dateien werden hiermit beispielsweise ausgefiltert:
Nicht ausgefiltert werden:
Hierbei treffen die Unterordner von CVS "foo/" und "bar/" nicht auf den Regelausdruck zu. |
org/apache/jakarta/** |
Trifft auf alle Dateien im Datei-Pfad "org/apache/jakarta" zu. Folgende Dateien werden hiermit beispielsweise ausgefiltert:
Nicht ausgefiltert wird:
Da hier der Unterordner "jakarta" des Ordners "org/apache" fehlt. |
org/apache/**/CVS/* |
Trifft auf alle Dateien in "CVS" Ordnern zu, die in Unterordnern des Ordners "org/apache" liegen. Folgende Dateien werden hiermit beispielsweise ausgefiltert:
Nicht ausgefiltert werden:
Hier treffen wieder die Unterordner "foo/" und "bar/" nicht auf den Regelausdruck zu. |
**/test/** |
Trifft auf alle Dateien in "test" Ordnern zu, inklusive Dateien in deren Unterordnern. |
Beispiele für Regeln zur Ausfilterung von Dateien aus Archiv-Dateiformaten.
Mehr Informationen zum Thema und die Quelle dieser Regelbeschreibungen finden Sie unter https://ant.apache.org/manual/dirtasks.html#patterns.
Um Regelsätze bei einem Extraktionsvorgang der Archiv-Datenformate
anzuwenden, ist eine entsprechende Konfiguration der zugehörigen Worker
notwendig. Diese Funktionalität steht für folgende Worker zur Verfügung:
UnZIPWorker, UnRARWorker, UnSevenZIPWorker sowie UnTARWorker. Um eine
Regeldatei einem Worker zuzuordnen, fügen Sie eine Konfiguration für den
entsprechden Worker in die Datei server-config/application/workers.xml
mit ein.
<bean id="unzipFilterRulesBean" class="com.levigo.jadice.server.archive.worker.filter.AntPatternArchiveEntryFilter">
<!-- Die Regeldatei unzipFilterRules.txt muss für diese Beispielkonfiguration auf Serverseite im Ordner <jadice-server>/server-config/custom/ vorliegen -->
<property name="antPatternFilterRulesURI" value="resource://custom/unzipFilterRules.txt" />
</bean>
<workers:worker class="com.levigo.jadice.server.archive.worker.UnZIPWorker">
<property name="filters">
<util:list>
<bean class="com.levigo.jadice.server.archive.worker.filter.OSXFilter" />
<ref bean="unzipFilterRulesBean"/>
</util:list>
</property>
</workers:worker>
Beispiel 6.20. Beispiel einer Worker-Konfiguration mit Ausfilterung von Dateien aus Archiv-Dateiformaten (workers.xml)
Der Namespace xmlns:util="http://www.springframework.org/schema/util"
und die SchemaLocations http://www.springframework.org/schema/util
sowie http://www.springframework.org/schema/util/spring-util-2.5.xsd
müssen in der Deklaration der workers.xml mit eingebunden werden.
Implementierung eigener Nodes / Worker
In diesem Kapitel soll anhand eines einfachen Beispiels gezeigt werden,
wie der jadice server um eigene Node
s und NodeWorker
erweitert
werden kann, um damit neue Verarbeitungsschritte realisieren zu können.
Dazu sind im Wesentlichen zwei Schritte notwendig: Zunächst muss eine
Node
-Klasse implementiert werden, die sowohl auf Client- als auch auf
der Serverseite vorhanden ist (siehe Abschnitt
Node-Klasse). Danach muss
die korrespondierende NodeWorker
-Klasse implementiert werden (siehe
Abschnitt
Worker-Klasse). Diese
muss nur auf der Serverseite vorhanden sein.
Node-Klasse
Die neu zu erstellende Nodeklasse muss von der abstrakten Superklasse
Node
erben. Sie muss einen parameterlosen Konstruktor ("default
constructor") besitzen.
Außerdem kann die Methode getWorkerClassName() überschrieben werden. Als
standardmäßigen Rückgabewert liefert diese als Rückgabewert den voll
qualifizierten Klassenname des Node
, wobei Node
durch Worker
ersetzt wird sowie worker
als zusätzliche Namespace-Ebene eingefügt
wird (Bsp: com.acme.jadiceserver.ExampleNode.getWorkerClassName()
liefert com.acme.jadiceserver.worker.ExampleWorker
).
Haben Sie eine andere Paketstruktur gewählt, so kann diese Methode überschrieben werden, um den voll qualifizierten Klassennamen der korrespondierenden Workerklasse zu liefern:
package com.mycompany.jadice.client;
import com.levigo.jadice.server.Node;
public class DemoNode extends Node {
public String getWorkerClassName() {
// Full qualified class name of the worker class
return "com.mycompany.jadice.worker.DemoWorker";
}
Beispiel 6.21. Implementierung eines Nodes
Soll es möglich sein, dass dem NodeWorker
zur Laufzeit Parameter
übermittelt werden, so kann dies durch weitere Methoden in der
Node
-Implementierung erfolgen. Dabei ist zu beachten, dass alle
Objekt- und statischen Attribute das Interface Serializable
implementieren müssen, da diese über JMS serialisiert und transportiert
werden (siehe Abschnitt Konfiguration des eingebetteten Messagebrokers).
public String getMyParameter() {
// Should be modifiable via a setter method
return "a Parameter";
}
Beispiel 6.22. Erweiterung des Nodes aus Beispiel 6.21, „Implementierung eines Nodes“ um einen Parameter
Der selbst implementierte Node
muss sowohl client- als auch
serverseitig im Klassenpfad eingebunden werden und kann genau wie die im
Abschnitt Anwendungsszenarien samt
Code-Beispielen gezeigten Node
s in
eigene Workflows eingebettet werden.
Worker-Klasse
Die Workerklasse, in der die Konvertierung durchführt wird, erbt von der
abstrakten, generischen Superklasse NodeWorker
<N>
, wobei der
Typ-Parameter <N>
für die zugehörige Node
-Klasse steht.
Hier ist die abstrakte Methode work() zu implementieren, in der die serverseitige Konvertierung durchführt wird.
package com.mycompany.jadice.server;
import java.io.InputStream;
import com.levigo.jadice.server.core.NodeWorker;
import com.levigo.jadice.server.shared.types.BundledStream;
import com.levigo.jadice.server.shared.types.Stream;
import com.levigo.jadice.server.shared.types.StreamDescriptor;
import com.mycompany.jadice.client.DemoNode;
public class DemoWorker extends NodeWorker<DemoNode> {
protected void work() throws Throwable {
// Parameter as defined in the example above
String myParam = getNode().getMyParameter();
// Retrive input data
for (Stream stream : getInputBundle()) {
InputStream unprocessedIS = stream.getInputStream();
// Meta data of the received stream
StreamDescriptor unprocessedSD = stream.getDescriptor();
// Method to process the data (not shown here)
InputStream processedIS = process(unprocessedIS, myParam);
// Meta data of the processed data
// unprocessedSD is set as "parent"
StreamDescriptor processedSD = new StreamDescriptor(unprocessedSD);
processedSD.setDescription("<Beschreibung>");
processedSD.setMimeType("<MIME Type>");
processedSD.setFileName("<Dateiname>");
// Link result result with its meta data
Stream result = new BundledStream(processedIS, processedSD);
// pass the result
getOutputBundle().addStream(result);
}
}
}
Beispiel 6.23. Implementierung eines NodeWorkers
Der auf diese Weise implementierte NodeWorker
muss nur im Klassenpfad
des jadice server eingebunden werden und wird bei Verwendung des
zugehörigen Node
s aus Beispiel 6.21, „Implementierung eines Nodes“
automatisch aufgerufen.