Das ist tatsächlich nicht so trivial, aber der Reihe nach:
Wer danach googelt, findet erstaunlich (?) häufig den Ansatz, die URL auf endsWith(“.pdf”) zu prüfen 😳 Das sagt natürlich nichts darüber, ob das PDF auch dargestellt wird (oder ob es das richtige PDF ist), aber als erste Näherung habe ich das übernommen. Allerdings prüfe ich die URL etwas genauer, und vergleiche sie mit der URL, deren PDF-Ausgabe backendseitig bereits inhaltlich geprüft wird:
1
2
Stringexpected=getBaseUrl()+"item/[0-9]+/print"
url.matches(expected)
Kurz hatte ich dann überlegt, auf den Content Type zu prüfen, aber da kann natürlich alles drin stehen. Die o.g. Bedingungen stellt das ebenfalls nicht sicher.
Dann gibt es Ansätze, STRG+A/STRG+C zu nutzen (Beispiel), um den Inhalt des PDF zu verifizieren – funktioniert nicht mit jedem OS/Browser.
Dann, ebenfalls häufig genannt: PDFBox. Finde ich gut, so kommt man an den Inhalt des PDFs und kann sicherstellen, dass es das richtige PDF ist:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
importorg.apache.commons.codec.binary.Base64
importorg.apache.pdfbox.pdmodel.PDDocument
importorg.apache.pdfbox.text.PDFTextStripper
// test if there's a PDF at this URI:
PDDocument pdf=getAsPDF(url)
noExceptionThrown()
pdf!=null
// and if the PDF seems to be valid:
Stringtext=extractTextFromPDF(pdf)
text.contains("foo bar")
// ... mit:
PDDocument getAsPDF(StringtheUrl)throwsException{
// bonus points: The end point needs authentification :-)
Nachteil: Dass die URL [ein|das richtige] PDF ausliefert, sagt natürlich nichts darüber, dass das im Browser dargestellt wird 🙃
Also: Screenshot machen. Allerdings nicht die Selenium-Methode, denn aus Browser-Sicht ist die Seite “leer”, das PDF wird von einem Plugin gerendert. Die Lösung dazu: Robot.
ACHTUNG: Das geht nicht in einer “headless”-Umgebung, wie man sie bsplw. auf vielen CI-Servern findet:
java.awt.AWTException: headless environment
Mit einem waitFor, bis das PDF tatsächlich gerendert wurde (denn das ist ungleich “die Seite wurde geladen”):
Vorab: Ja, es gibt @RequestBody. Und es ist hässlich. Ich habe es nicht mal eingebaut. Aber ich musste es googlen und will das nicht noch mal tun müssen:
Googelt man “open file” etc., geht es meistens darum, eine Datei einzulesen. In meinem konkreten Fall wollte ich sie aber öffnen, also bsplw. ein PDF im Reader. Lösung:
Ein Envers Timestamp sieht in der Datenbank bsplw so aus: 1441090155727 (und ist ein Bigint). Das wäre der 30.04.47636, 06:02:07, und ich bezweifel, dass der Datenbankeintrag aus der Zukunft kommt. Die Lösung ist so naheliegend wie einfach: Die letzten drei Stellen stehen für die Millisekunden, der “korrekte” Timestamp lautet 1441090155, und das ist der 01.09.2015, 08:49:15.
Eine Anmerkung dazu: Das “Early Access” Repo ist meiner Einschätzung nach eigentlich nicht nötig; alle letztlich verwendeten Dependencies kommen aus dem “Releases” Repo. Aber: Offenbar gibt es irgendwelche transitiven Dependencies, die dort zu finden sind. Sie werden “später” durch andere transitive Dependecies “überschrieben”, aber sie müssen trotzdem aufgelöst werden können! Andernfalls funktioniert zwar auch alles, aber es gibt unschöne Warnings. Und ich habe einige Zeit darauf verwendet, das “Early Access” Repo durch force und exclude wegzukonfigurieren – ein Fass ohne Boden 🙂
Verschachtelte Strukturen in Elasticsearch zu mappen, ist strukturell gesehen eigentlich gut dokumentiert, siehe Object Type (Achtung: Nicht zu verwechseln mit Nested Type!). In Java kann dabei trotzdem einiges schiefgehen – zumindest in v0.90.x, über die aktuellen Versionen kann ich nichts sagen.
Zuerst ein Klassiker, wenn man sich die zahlreichen Tipps dazu im Internet ansieht:
Merge failed with failures {[mapper [field.innerField] has different index values, mapper [field.innerField] has different tokenize values, mapper [field.innerField] has different index_analyzer]}
Das liegt gerne mal daran, dass der Index vor dem Mapping aufgebaut wurde. Was aber nicht bedeutet, dass man das selber explizit so macht! Es kann ganz simpel daran liegen, dass die früher erzeugten (und jetzt veralteten) Dateien auf der Platte nicht mehr passen. Alle Dateien löschen und neu erzeugen lassen, dann kann der Fehler schon verschwunden sein m(
Dann, auch sehr verwirrend: Der XContentBuilder hat eine string()-Funktion (nicht zu verwechseln mit toString()). Die hilft beim Debuggen insofern, als dass man das erzeugte Mapping/JSON ausgeben kann. Allerdings “tut” string() intern irgendwas, keine Ahnung, den Stream schließen oder so. Mit einem string() im Code bekommt man eine NullPointerException in UTF8JsonGenerator.writeRaw(UTF8JsonGenerator.java:670). Tollerweise (trollerweise?) liegt die Klasse zwar unter org.elasticsearch.common.jackson.core.json.UTF8JsonGenerator, die Sourcen sind aber nicht Teil der ES-Sourcen… und der UTF8JsonGenerator hat sehr viele writeRaw()-Methoden.
Bis man darauf kommt, dass es ausschließlich an string() liegt, kann schon mal einige Zeit vergehen: Mit einem string() im Code kann man zwar das erzeugte JSON ausgeben, dieses ist aber völlig irrelevant. Es kann sogar ungültig sein, etwa durch ein überzähliges endObject() – durch string() sieht man statt des eigentlichen
JsonGenerationException: Current context not an object but ROOT
nur den NullPointer von oben.
Trotzdem ist string() nötig, denn drölf verschachtelte endObject() und field() werden schnell unübersichtlich. Meine Empfehlung: Das JSON unter Einsatz von string() gemäß der Doku aufbauen, string() dann auskommentieren, und erst dann die auftretenden Fehlermeldungen lesen.