diff options
Diffstat (limited to 'src/main/resources')
7 files changed, 545 insertions, 20 deletions
diff --git a/src/main/resources/META-INF/native-image/eu.mulk/aendggner/reachability-metadata.json b/src/main/resources/META-INF/native-image/eu.mulk/aendggner/reachability-metadata.json new file mode 100644 index 0000000..4f85735 --- /dev/null +++ b/src/main/resources/META-INF/native-image/eu.mulk/aendggner/reachability-metadata.json @@ -0,0 +1,361 @@ +{ + "reflection": [ + { + "type": "ch.qos.logback.classic.Logger" + }, + { + "type": "com.sun.crypto.provider.AESCipher$General", + "methods": [ + { + "name": "<init>", + "parameterTypes": [] + } + ] + }, + { + "type": "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl", + "methods": [ + { + "name": "<init>", + "parameterTypes": [] + } + ] + }, + { + "type": "java.awt.image.BufferedImage", + "jniAccessible": true, + "fields": [ + { + "name": "colorModel" + }, + { + "name": "imageType" + }, + { + "name": "raster" + } + ], + "methods": [ + { + "name": "getRGB", + "parameterTypes": [ + "int", + "int", + "int", + "int", + "int[]", + "int", + "int" + ] + }, + { + "name": "setRGB", + "parameterTypes": [ + "int", + "int", + "int", + "int", + "int[]", + "int", + "int" + ] + } + ] + }, + { + "type": "java.awt.image.ColorModel", + "jniAccessible": true, + "fields": [ + { + "name": "colorSpace" + }, + { + "name": "colorSpaceType" + }, + { + "name": "isAlphaPremultiplied" + }, + { + "name": "is_sRGB" + }, + { + "name": "nBits" + }, + { + "name": "numComponents" + }, + { + "name": "supportsAlpha" + }, + { + "name": "transparency" + } + ], + "methods": [ + { + "name": "getRGBdefault", + "parameterTypes": [] + } + ] + }, + { + "type": "java.awt.image.Raster", + "jniAccessible": true, + "fields": [ + { + "name": "dataBuffer" + }, + { + "name": "height" + }, + { + "name": "minX" + }, + { + "name": "minY" + }, + { + "name": "numBands" + }, + { + "name": "numDataElements" + }, + { + "name": "sampleModel" + }, + { + "name": "sampleModelTranslateX" + }, + { + "name": "sampleModelTranslateY" + }, + { + "name": "width" + } + ] + }, + { + "type": "java.awt.image.SampleModel", + "jniAccessible": true, + "fields": [ + { + "name": "height" + }, + { + "name": "width" + } + ], + "methods": [ + { + "name": "getPixels", + "parameterTypes": [ + "int", + "int", + "int", + "int", + "int[]", + "java.awt.image.DataBuffer" + ] + }, + { + "name": "setPixels", + "parameterTypes": [ + "int", + "int", + "int", + "int", + "int[]", + "java.awt.image.DataBuffer" + ] + } + ] + }, + { + "type": "java.awt.image.SinglePixelPackedSampleModel", + "jniAccessible": true, + "fields": [ + { + "name": "bitMasks" + }, + { + "name": "bitOffsets" + }, + { + "name": "bitSizes" + }, + { + "name": "maxBitSize" + } + ] + }, + { + "type": "java.lang.Boolean", + "jniAccessible": true, + "methods": [ + { + "name": "getBoolean", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, + { + "type": "java.lang.System", + "jniAccessible": true, + "methods": [ + { + "name": "load", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, + { + "type": "org.apache.commons.logging.LogFactory" + }, + { + "type": "org.apache.commons.logging.impl.SLF4JLogFactory" + }, + { + "type": "org.apache.commons.logging.impl.WeakHashtable", + "methods": [ + { + "name": "<init>", + "parameterTypes": [] + } + ] + }, + { + "type": "org.apache.log4j.LogManager" + }, + { + "type": "org.apache.logging.log4j.Logger" + }, + { + "type": "org.apache.pdfbox.pdmodel.encryption.StandardSecurityHandler", + "methods": [ + { + "name": "<init>", + "parameterTypes": [] + } + ] + }, + { + "type": "org.jboss.logmanager.LogManager" + }, + { + "type": "org.slf4j.jul.JULServiceProvider" + }, + { + "type": "sun.awt.image.IntegerComponentRaster", + "jniAccessible": true, + "fields": [ + { + "name": "data" + }, + { + "name": "dataOffsets" + }, + { + "name": "pixelStride" + }, + { + "name": "scanlineStride" + }, + { + "name": "type" + } + ] + }, + { + "type": "sun.misc.Unsafe", + "fields": [ + { + "name": "theUnsafe" + } + ], + "methods": [ + { + "name": "invokeCleaner", + "parameterTypes": [ + "java.nio.ByteBuffer" + ] + } + ] + }, + { + "type": "sun.security.provider.MD5", + "methods": [ + { + "name": "<init>", + "parameterTypes": [] + } + ] + }, + { + "type": "sun.security.provider.NativePRNG", + "methods": [ + { + "name": "<init>", + "parameterTypes": [ + "java.security.SecureRandomParameters" + ] + } + ] + }, + { + "type": "sun.security.provider.SHA2$SHA256", + "methods": [ + { + "name": "<init>", + "parameterTypes": [] + } + ] + }, + { + "type": "sun.text.resources.cldr.FormatData" + }, + { + "type": "sun.util.resources.cldr.CalendarData" + } + ], + "resources": [ + { + "glob": "META-INF/services/java.net.spi.URLStreamHandlerProvider" + }, + { + "glob": "META-INF/services/java.time.zone.ZoneRulesProvider" + }, + { + "glob": "META-INF/services/javax.xml.parsers.DocumentBuilderFactory" + }, + { + "glob": "META-INF/services/org.apache.commons.logging.LogFactory" + }, + { + "glob": "META-INF/services/org.jboss.logging.LoggerProvider" + }, + { + "glob": "META-INF/services/org.slf4j.spi.SLF4JServiceProvider" + }, + { + "glob": "commons-logging.properties" + }, + { + "module": "java.base", + "glob": "jdk/internal/icu/impl/data/icudt76b/nfc.nrm" + }, + { + "module": "java.base", + "glob": "jdk/internal/icu/impl/data/icudt76b/ubidi.icu" + }, + { + "glob": "org/apache/fontbox/**" + }, + { + "glob": "org/apache/pdfbox/resources/**" + } + ] +}
\ No newline at end of file diff --git a/src/main/resources/eu/mulk/aendggner/web/app.js b/src/main/resources/eu/mulk/aendggner/web/app.js new file mode 100644 index 0000000..c59f6e3 --- /dev/null +++ b/src/main/resources/eu/mulk/aendggner/web/app.js @@ -0,0 +1,90 @@ +// Nimmt die Dateien aus dem Formular entgegen, reicht sie an den Worker und zeigt die Synopse. +// Es gibt keinen Server: Alles läuft im Browser. + +const formular = document.querySelector("#synopse-formular"); +const knopf = formular.querySelector("button"); +const meldung = document.querySelector("#meldung"); + +let worker = null; + +function zeige(text, art) { + meldung.textContent = text; + meldung.className = art ? "meldung " + art : "meldung"; + meldung.hidden = !text; +} + +/** + * Der Dateiinhalt geht als Base64 an das Wasm-Modul: Die Umsetzung typisierter Felder nach + * byte[] ist in Web Image derzeit defekt, Zeichenketten überqueren die Grenze zuverlässig. + */ +async function alsBase64(datei) { + const bytes = new Uint8Array(await datei.arrayBuffer()); + let roh = ""; + const block = 0x8000; // btoa verträgt keine beliebig langen Argumentlisten. + for (let i = 0; i < bytes.length; i += block) { + roh += String.fromCharCode.apply(null, bytes.subarray(i, i + block)); + } + return { name: datei.name, base64: btoa(roh) }; +} + +formular.addEventListener("submit", async (ereignis) => { + ereignis.preventDefault(); + + const stammDatei = document.querySelector("#stamm").files[0]; + const patchDateien = Array.from(document.querySelector("#aenderung").files); + if (!stammDatei || patchDateien.length === 0) { + zeige("Bitte ein Stammgesetz und mindestens ein Änderungsdokument wählen.", "fehler"); + return; + } + + knopf.disabled = true; + zeige("Lade das Rechenwerk (einmalig einige Megabyte) und werte aus …", "arbeit"); + + try { + const [stamm, patches] = await Promise.all([ + alsBase64(stammDatei), + Promise.all(patchDateien.map(alsBase64)), + ]); + + if (!worker) { + worker = new Worker("worker.js"); + } + + const ergebnis = await new Promise((aufloesen, ablehnen) => { + worker.onmessage = (nachricht) => aufloesen(nachricht.data); + worker.onerror = (fehler) => ablehnen(new Error(fehler.message || "Worker-Fehler")); + worker.postMessage({ + stamm, + patches, + vollstaendig: document.querySelector("#vollstaendig").checked, + artikel: null, + }); + }); + + if (ergebnis.fehler) { + zeige("Verarbeitung fehlgeschlagen: " + ergebnis.fehler, "fehler"); + return; + } + + const blob = new Blob([ergebnis.html], { type: "text/html;charset=utf-8" }); + const adresse = URL.createObjectURL(blob); + + // Kein window.open: Der Aufruf käme nach dem Warten auf den Worker und gälte dem Browser + // nicht mehr als Nutzerhandlung — er würde als Popup blockiert. Stattdessen ein Link, der + // sich öffnen und ebenso gut speichern lässt. + zeige( + `${ergebnis.angewandt} Befehle angewandt, ${ergebnis.manuell} manuell zu prüfen, ` + + `${ergebnis.normen} geänderte Normen. `, + "fertig", + ); + const verweis = document.createElement("a"); + verweis.href = adresse; + verweis.target = "_blank"; + verweis.textContent = "Synopse öffnen"; + meldung.append(verweis); + } catch (e) { + zeige("Verarbeitung fehlgeschlagen: " + (e && e.message ? e.message : e), "fehler"); + } finally { + knopf.disabled = false; + } +}); diff --git a/src/main/resources/eu/mulk/aendggner/web/datenschutz.html b/src/main/resources/eu/mulk/aendggner/web/datenschutz.html index 65ec0c9..5b9bbac 100644 --- a/src/main/resources/eu/mulk/aendggner/web/datenschutz.html +++ b/src/main/resources/eu/mulk/aendggner/web/datenschutz.html @@ -4,7 +4,7 @@ <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Datenschutz — ÄndGgner</title> - <link rel="stylesheet" href="/style.css"> + <link rel="stylesheet" href="style.css"> </head> <body> <main> @@ -15,19 +15,22 @@ Zugriffslogs, Hosting-Anbieter). </p> - <h2>Hochgeladene Dateien</h2> + <h2>Ihre Dateien</h2> <p> - Stammgesetz und Änderungsgesetz(e) werden ausschließlich im Arbeitsspeicher bzw. in - temporären Dateien verarbeitet, um die Synopse zu erzeugen. Nach Auslieferung der Antwort - werden diese temporären Dateien sofort gelöscht. Es findet keine dauerhafte Speicherung des - Inhalts statt. + Die gewählten Dateien werden <strong>nicht übertragen</strong>. Die gesamte Auswertung — + PDF-Textgewinnung, Anwendung der Änderungsbefehle, Erzeugung der Synopse — läuft als + WebAssembly-Programm in Ihrem Browser. Der Server sieht die Dateien nicht, kann sie nicht + sehen und speichert folglich auch nichts davon. Wer das prüfen möchte, öffne die + Netzwerkanzeige der Entwicklerwerkzeuge: Nach dem Laden der Seite und des Rechenwerks + entsteht kein weiterer Netzverkehr. </p> <h2>Zugriffsprotokolle</h2> <p> - Wie bei jedem Webserver werden beim Aufruf technische Zugriffsdaten (u. a. IP-Adresse, - Zeitpunkt, aufgerufene Adresse) für einen begrenzten Zeitraum zur Absicherung des Betriebs - protokolliert. + Beim Abruf der Seite selbst und der zugehörigen Dateien werden wie bei jedem Webserver + technische Zugriffsdaten (u. a. IP-Adresse, Zeitpunkt, aufgerufene Adresse) für einen + begrenzten Zeitraum zur Absicherung des Betriebs protokolliert. Diese Protokolle enthalten + keine Angaben über die von Ihnen ausgewerteten Dokumente. </p> <h2>Keine Cookies, keine Konten, kein Tracking</h2> @@ -36,6 +39,6 @@ Analyse- oder Tracking-Dienste Dritter ein. </p> </main> - <footer><a href="/">Zurück zum Formular</a></footer> + <footer><a href="index.html">Zurück zum Formular</a></footer> </body> </html> diff --git a/src/main/resources/eu/mulk/aendggner/web/impressum.html b/src/main/resources/eu/mulk/aendggner/web/impressum.html index 303db8e..525ef77 100644 --- a/src/main/resources/eu/mulk/aendggner/web/impressum.html +++ b/src/main/resources/eu/mulk/aendggner/web/impressum.html @@ -4,7 +4,7 @@ <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Impressum — ÄndGgner</title> - <link rel="stylesheet" href="/style.css"> + <link rel="stylesheet" href="style.css"> </head> <body> <main> @@ -21,6 +21,6 @@ [E-Mail-Adresse] </address> </main> - <footer><a href="/">Zurück zum Formular</a></footer> + <footer><a href="index.html">Zurück zum Formular</a></footer> </body> </html> diff --git a/src/main/resources/eu/mulk/aendggner/web/index.html b/src/main/resources/eu/mulk/aendggner/web/index.html index 02f8f75..9f26889 100644 --- a/src/main/resources/eu/mulk/aendggner/web/index.html +++ b/src/main/resources/eu/mulk/aendggner/web/index.html @@ -4,7 +4,8 @@ <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>ÄndGgner — Änderungsgesetze konsolidieren</title> - <link rel="stylesheet" href="/style.css"> + <link rel="stylesheet" href="style.css"> + <script src="app.js" defer></script> </head> <body> <header> @@ -15,7 +16,7 @@ </header> <main> - <form method="post" action="/synopse" enctype="multipart/form-data"> + <form id="synopse-formular"> <div class="field"> <label for="stamm">Stammgesetz</label> <input type="file" id="stamm" name="stamm" required> @@ -39,7 +40,7 @@ <div class="field checkbox"> <label> - <input type="checkbox" name="vollstaendig"> + <input type="checkbox" id="vollstaendig" name="vollstaendig"> Unveränderte Vorschriften mit in die Synopse aufnehmen </label> </div> @@ -47,18 +48,20 @@ <button type="submit">Synopse erzeugen</button> </form> + <p id="meldung" class="meldung" hidden></p> + <p class="disclaimer"> Die Synopse wird automatisch erstellt und ist <strong>ohne Gewähr</strong> für Vollständigkeit und Richtigkeit; sie ersetzt keine Rechtsberatung. Uneindeutige Änderungsbefehle werden zur - manuellen Prüfung markiert statt stillschweigend verworfen. Hochgeladene Dateien werden - ausschließlich zur Erzeugung der Synopse verarbeitet und danach sofort wieder gelöscht — es - findet keine dauerhafte Speicherung statt. Bitte nur Rechtstexte hochladen. + manuellen Prüfung markiert statt stillschweigend verworfen. Die gewählten Dateien + <strong>verlassen Ihren Rechner nicht</strong>: Die Auswertung läuft vollständig im Browser, + es wird nichts hochgeladen und nichts gespeichert. </p> </main> <footer> - <a href="/impressum">Impressum</a> · - <a href="/datenschutz">Datenschutz</a> · + <a href="impressum.html">Impressum</a> · + <a href="datenschutz.html">Datenschutz</a> · <a href="https://TODO-QUELLCODE-URL-EINTRAGEN">Quellcode (AGPLv3)</a> </footer> </body> diff --git a/src/main/resources/eu/mulk/aendggner/web/style.css b/src/main/resources/eu/mulk/aendggner/web/style.css index 6e2ec70..cdea20e 100644 --- a/src/main/resources/eu/mulk/aendggner/web/style.css +++ b/src/main/resources/eu/mulk/aendggner/web/style.css @@ -112,3 +112,29 @@ footer { footer a { color: var(--accent); } + +.meldung { + margin-top: 1.25rem; + padding: 0.75rem 1rem; + border: 1px solid var(--border); + border-radius: 6px; + font-size: 0.9375rem; +} + +.meldung.arbeit { + color: var(--muted); +} + +.meldung.fertig { + border-color: var(--accent); +} + +.meldung.fehler { + border-color: #a33; + color: #a33; +} + +button[disabled] { + filter: grayscale(0.6); + cursor: progress; +} diff --git a/src/main/resources/eu/mulk/aendggner/web/worker.js b/src/main/resources/eu/mulk/aendggner/web/worker.js new file mode 100644 index 0000000..12ba400 --- /dev/null +++ b/src/main/resources/eu/mulk/aendggner/web/worker.js @@ -0,0 +1,42 @@ +// Führt das Wasm-Modul in einem eigenen Thread aus: Web Image ist einthreadig, und ein Lauf über +// mehrere Sekunden würde die Oberfläche sonst einfrieren. + +const bereit = new Promise((aufloesen) => { + self.aendggnerBereit = aufloesen; +}); + +// Die Laufzeit von aendggner.js sucht das Wasm-Modul neben ihrer eigenen Datei und findet die +// über `document.currentScript` — den es im Worker nicht gibt, wo sie stattdessen auf +// `location.href` (also diese Datei) zurückfällt und ins Leere greift. Der Selbststart beim +// Import scheitert deshalb; danach starten wir die VM mit ausdrücklichem Pfad noch einmal. +self.importScripts("aendggner.js"); + +const gestartet = GraalVM.run([], Object.assign(new GraalVM.Config(), { + wasm_path: new URL("aendggner.js.wasm", self.location.href).href, +})); + +self.onmessage = async (nachricht) => { + const { stamm, patches, vollstaendig, artikel } = nachricht.data; + try { + await gestartet; + await bereit; + const ergebnis = self.aendggnerSynopse({ + stamm, + patches, + artikel: artikel ?? null, + vollstaendig: Boolean(vollstaendig), + }); + if (ergebnis.fehler) { + self.postMessage({ fehler: String(ergebnis.fehler) }); + } else { + self.postMessage({ + html: String(ergebnis.html), + angewandt: Number(ergebnis.angewandt), + manuell: Number(ergebnis.manuell), + normen: Number(ergebnis.normen), + }); + } + } catch (e) { + self.postMessage({ fehler: e && e.message ? e.message : String(e) }); + } +}; |
