aboutsummaryrefslogtreecommitdiff
path: root/src/wasm/java
diff options
context:
space:
mode:
Diffstat (limited to 'src/wasm/java')
-rw-r--r--src/wasm/java/eu/mulk/aendggner/wasm/BrowserMain.java155
-rw-r--r--src/wasm/java/eu/mulk/aendggner/wasm/InflaterErsatz.java146
2 files changed, 301 insertions, 0 deletions
diff --git a/src/wasm/java/eu/mulk/aendggner/wasm/BrowserMain.java b/src/wasm/java/eu/mulk/aendggner/wasm/BrowserMain.java
new file mode 100644
index 0000000..c4f5a9a
--- /dev/null
+++ b/src/wasm/java/eu/mulk/aendggner/wasm/BrowserMain.java
@@ -0,0 +1,155 @@
+package eu.mulk.aendggner.wasm;
+
+import eu.mulk.aendggner.Pipeline;
+import eu.mulk.aendggner.Quelle;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+import java.util.function.Function;
+import java.util.logging.LogManager;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.graalvm.webimage.api.JS;
+import org.graalvm.webimage.api.JSNumber;
+import org.graalvm.webimage.api.JSObject;
+import org.graalvm.webimage.api.JSString;
+import org.graalvm.webimage.api.JSValue;
+
+/**
+ * Einstiegspunkt der Browserfassung: stellt {@code globalThis.aendggnerSynopse} bereit und ruft
+ * damit dieselbe {@link Pipeline} auf wie Befehlszeile und Tests.
+ *
+ * <p>Erwartet ein JS-Objekt <code>{stamm: {name, base64}, patches: [{name, base64}], artikel,
+ * vollstaendig}</code> und liefert <code>{html, angewandt, manuell, normen}</code> oder
+ * <code>{fehler}</code> zurück. Geworfen wird nichts: Eine Ausnahme im Wasm hinterlässt auf der
+ * JS-Seite nur einen unlesbaren Stapel, also wird jeder Fehler als Text zurückgereicht.
+ *
+ * <p>Der Dateiinhalt wandert als Base64-Text über die Grenze, nicht als {@code Uint8Array}: Die
+ * Umsetzung typisierter Felder nach {@code byte[]} ist in Web Image derzeit defekt
+ * („byteArrayHub is not defined“). Zeichenketten überqueren die Grenze zuverlässig, und die
+ * Base64-Dekodierung ist reines Java.
+ */
+public final class BrowserMain {
+
+ private BrowserMain() {}
+
+ public static void main(String... args) {
+ // JULs Standardformatter ermittelt den Aufrufer über StackWalker, den Web Image nicht kennt;
+ // im Browser gibt es ohnehin kein Logdatei-Ziel.
+ LogManager.getLogManager().reset();
+ Logger.getLogger("").setLevel(Level.OFF);
+
+ exportiere(BrowserMain::synopse);
+
+ // Die Erreichbarkeitsanalyse sieht nur Aufrufe aus Java; dass JavaScript die exportierte
+ // Funktion aufruft, weiß sie nicht — ohne diesen (nie durchlaufenen) Zweig bliebe die
+ // gesamte Pipeline aus dem Image heraus und der erste Aufruf endete in einem
+ // NoClassDefFoundError.
+ melde();
+ }
+
+ @JS(args = "fn", value = "globalThis.aendggnerSynopse = fn;")
+ private static native void exportiere(Function<JSObject, JSObject> fn);
+
+ @JS(
+ value =
+ "if (typeof globalThis.aendggnerBereit === 'function') { globalThis.aendggnerBereit(); }")
+ private static native void melde();
+
+ private static JSObject synopse(JSObject eingabe) {
+ var antwort = JSObject.create();
+ try {
+ var stamm = quelle(eingabe.get("stamm"));
+ var patches = new ArrayList<Quelle>();
+ var patchListe = eingabe.get("patches");
+ for (int i = 0; i < anzahl(patchListe); i++) {
+ patches.add(quelle(element(patchListe, i)));
+ }
+
+ var artikel = text(eingabe.get("artikel"));
+ var vollstaendig = Boolean.TRUE.equals(wahrheitswert(eingabe.get("vollstaendig")));
+
+ var ergebnis =
+ Pipeline.erzeugeSynopse(
+ stamm, List.copyOf(patches), artikel == null || artikel.isBlank() ? null : artikel,
+ vollstaendig);
+
+ // Java-Werte kämen auf der JS-Seite als undurchsichtige Proxys an; JSString/JSNumber
+ // erzeugen echte JS-Werte.
+ antwort.set("html", JSString.of(ergebnis.html()));
+ antwort.set("angewandt", JSNumber.of(ergebnis.anzahlAngewandt()));
+ antwort.set("manuell", JSNumber.of(ergebnis.anzahlManuell()));
+ antwort.set("normen", JSNumber.of(ergebnis.anzahlGeaenderteNormen()));
+ } catch (Throwable e) {
+ e.printStackTrace();
+ var meldung = e.getMessage();
+ antwort.set(
+ "fehler",
+ JSString.of(
+ e.getClass().getSimpleName()
+ + (meldung == null || meldung.isBlank() ? "" : ": " + meldung)));
+ }
+ return antwort;
+ }
+
+ private static Quelle quelle(Object datei) {
+ if (!(datei instanceof JSObject objekt)) {
+ throw new IllegalArgumentException("Datei fehlt oder ist kein Objekt.");
+ }
+ var name = text(objekt.get("name"));
+ var base64 = text(objekt.get("base64"));
+ if (base64 == null || base64.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Datei „" + (name == null ? "?" : name) + "“ enthält keine Daten.");
+ }
+ return new Quelle(name == null ? "Datei" : name, Base64.getDecoder().decode(base64));
+ }
+
+ /**
+ * Die Interop reicht JS-Werte je nach Typ als {@link JSValue} oder als bereits umgesetztes
+ * Java-Objekt herüber; die folgenden Helfer nehmen beides an, damit sich die Browserfassung nicht
+ * an einer Fassung der experimentellen Web-Image-API festmacht.
+ */
+ private static String text(Object wert) {
+ if (wert == null) {
+ return null;
+ }
+ if (wert instanceof String s) {
+ return s;
+ }
+ if (wert instanceof JSValue v) {
+ return "undefined".equals(v.typeof()) ? null : v.asString();
+ }
+ return wert.toString();
+ }
+
+ private static Boolean wahrheitswert(Object wert) {
+ if (wert instanceof Boolean b) {
+ return b;
+ }
+ if (wert instanceof JSValue v) {
+ return v.asBoolean();
+ }
+ return Boolean.FALSE;
+ }
+
+ private static int anzahl(Object liste) {
+ if (!(liste instanceof JSObject objekt)) {
+ return 0;
+ }
+ var laenge = objekt.get("length");
+ if (laenge instanceof Number n) {
+ return n.intValue();
+ }
+ if (laenge instanceof JSValue v) {
+ return v.asInt();
+ }
+ return 0;
+ }
+
+ private static Object element(Object liste, int index) {
+ var objekt = (JSObject) liste;
+ var wert = objekt.get(Integer.valueOf(index));
+ return wert != null ? wert : objekt.get(String.valueOf(index));
+ }
+}
diff --git a/src/wasm/java/eu/mulk/aendggner/wasm/InflaterErsatz.java b/src/wasm/java/eu/mulk/aendggner/wasm/InflaterErsatz.java
new file mode 100644
index 0000000..8817e4d
--- /dev/null
+++ b/src/wasm/java/eu/mulk/aendggner/wasm/InflaterErsatz.java
@@ -0,0 +1,146 @@
+package eu.mulk.aendggner.wasm;
+
+import com.jcraft.jzlib.JZlib;
+import com.oracle.svm.core.annotate.Inject;
+import com.oracle.svm.core.annotate.RecomputeFieldValue;
+import com.oracle.svm.core.annotate.Substitute;
+import com.oracle.svm.core.annotate.TargetClass;
+import java.util.zip.DataFormatException;
+
+/**
+ * Ersetzt java.util.zip.Inflater durch die reine Java-Umsetzung von jzlib.
+ *
+ * <p>Web Image kennt die nativen zlib-Bindungen des JDK nicht (GR-65205); ohne Inflate ist kein
+ * PDF lesbar, denn nahezu jeder Inhaltsstrom ist FlateDecode-komprimiert.
+ */
+@TargetClass(java.util.zip.Inflater.class)
+final class Target_java_util_zip_Inflater {
+
+ @Inject @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Reset)
+ com.jcraft.jzlib.Inflater impl;
+
+ @Inject @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Reset)
+ boolean nowrap;
+
+ @Inject @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Reset)
+ boolean fertig;
+
+ @Inject @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Reset)
+ boolean braucheWoerterbuch;
+
+ @Substitute
+ Target_java_util_zip_Inflater(boolean nowrap) {
+ this.nowrap = nowrap;
+ this.impl = new com.jcraft.jzlib.Inflater();
+ this.impl.init(nowrap);
+ }
+
+ @Substitute
+ Target_java_util_zip_Inflater() {
+ this(false);
+ }
+
+ @Substitute
+ public void setInput(byte[] input, int off, int len) {
+ impl.next_in = input;
+ impl.next_in_index = off;
+ impl.avail_in = len;
+ }
+
+ @Substitute
+ public void setInput(byte[] input) {
+ setInput(input, 0, input.length);
+ }
+
+ @Substitute
+ public int inflate(byte[] output, int off, int len) throws DataFormatException {
+ impl.next_out = output;
+ impl.next_out_index = off;
+ impl.avail_out = len;
+ int err = impl.inflate(JZlib.Z_NO_FLUSH);
+ int erzeugt = len - impl.avail_out;
+ switch (err) {
+ case JZlib.Z_STREAM_END:
+ fertig = true;
+ return erzeugt;
+ case JZlib.Z_NEED_DICT:
+ braucheWoerterbuch = true;
+ return erzeugt;
+ case JZlib.Z_OK:
+ case JZlib.Z_BUF_ERROR:
+ return erzeugt;
+ default:
+ throw new DataFormatException(impl.msg == null ? "Inflate-Fehler " + err : impl.msg);
+ }
+ }
+
+ @Substitute
+ public int inflate(byte[] output) throws DataFormatException {
+ return inflate(output, 0, output.length);
+ }
+
+ @Substitute
+ public boolean needsInput() {
+ return impl.avail_in <= 0;
+ }
+
+ @Substitute
+ public boolean needsDictionary() {
+ return braucheWoerterbuch;
+ }
+
+ @Substitute
+ public boolean finished() {
+ return fertig;
+ }
+
+ @Substitute
+ public int getRemaining() {
+ return Math.max(impl.avail_in, 0);
+ }
+
+ @Substitute
+ public long getBytesRead() {
+ return impl.total_in;
+ }
+
+ @Substitute
+ public long getBytesWritten() {
+ return impl.total_out;
+ }
+
+ @Substitute
+ public int getTotalIn() {
+ return (int) impl.total_in;
+ }
+
+ @Substitute
+ public int getTotalOut() {
+ return (int) impl.total_out;
+ }
+
+ @Substitute
+ public void setDictionary(byte[] dictionary, int off, int len) {
+ var kopie = new byte[len];
+ System.arraycopy(dictionary, off, kopie, 0, len);
+ impl.setDictionary(kopie, len);
+ braucheWoerterbuch = false;
+ }
+
+ @Substitute
+ public void setDictionary(byte[] dictionary) {
+ setDictionary(dictionary, 0, dictionary.length);
+ }
+
+ @Substitute
+ public void reset() {
+ impl.init(nowrap);
+ fertig = false;
+ braucheWoerterbuch = false;
+ }
+
+ @Substitute
+ public void end() {
+ impl.end();
+ }
+}