diff options
Diffstat (limited to 'src/main')
16 files changed, 228 insertions, 182 deletions
diff --git a/src/main/java/eu/mulk/aendggner/aenderung/Aenderungsbefehl.java b/src/main/java/eu/mulk/aendggner/aenderung/Aenderungsbefehl.java index 606383f..4117922 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/Aenderungsbefehl.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/Aenderungsbefehl.java @@ -73,9 +73,9 @@ public sealed interface Aenderungsbefehl { * @param vorher {@code true} bei „Vor …“, {@code false} bei „Nach …“. * @param bezeichnung die Bezeichnung des neuen Elements (z.B. „28a“, „5a“); {@code null}, wenn * der Befehl keine nennt (z.B. „folgender Satz“). - * @param anker bei „Vor den Wörtern „Aus dem Bereich Verkehr:“ wird folgender Absatz 5 - * eingefügt“ der Wortanker, der die Position innerhalb von {@code stelle} bestimmt; - * {@code null}, wenn die Position wie üblich aus der Struktur folgt. + * @param anker bei „Vor den Wörtern „Aus dem Bereich Verkehr:“ wird folgender Absatz 5 eingefügt“ + * der Wortanker, der die Position innerhalb von {@code stelle} bestimmt; {@code null}, wenn + * die Position wie üblich aus der Struktur folgt. */ record StrukturEinfuegung( Stelle stelle, diff --git a/src/main/java/eu/mulk/aendggner/aenderung/Stelle.java b/src/main/java/eu/mulk/aendggner/aenderung/Stelle.java index 1c63a5f..69e6837 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/Stelle.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/Stelle.java @@ -29,8 +29,8 @@ public record Stelle(List<Komponente> komponenten) { Absatzbezeichnung {} /** - * „§ 5“, „§ 28a“, im bayerischen Landesrecht „Art. 5“ — die Normnummer ohne Sigel, dazu das - * Sigel selbst („§“ oder „Art.“). + * „§ 5“, „§ 28a“, im bayerischen Landesrecht „Art. 5“ — die Normnummer ohne Sigel, dazu das Sigel + * selbst („§“ oder „Art.“). */ public record Paragraph(String nummer, String sigel) implements Komponente { public Paragraph(String nummer) { diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/ZitatExtraktor.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/ZitatExtraktor.java index d4df176..4b9d511 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/parse/ZitatExtraktor.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/ZitatExtraktor.java @@ -170,8 +170,8 @@ public final class ZitatExtraktor { Pattern.compile("^(?:\\d+[a-z]?\\.|[a-z]{1,3}\\))[ \\t]"); /** - * Wahr, wenn das öffnende Anführungszeichen an Position {@code i} ein Fortführungszeichen ist: - * Es steht am Zeilenanfang innerhalb eines offenen Zitats und ihm folgt unmittelbar ein + * Wahr, wenn das öffnende Anführungszeichen an Position {@code i} ein Fortführungszeichen ist: Es + * steht am Zeilenanfang innerhalb eines offenen Zitats und ihm folgt unmittelbar ein * Aufzählungsmarker. */ private static boolean istFortfuehrungszeichen(String text, int i) { @@ -191,7 +191,8 @@ public final class ZitatExtraktor { * Stammgesetze zitieren ihre Normköpfe als „Art. N“, nicht als „Artikel N“ — zitierter Inhalt * läuft hier also nicht versehentlich auf. */ - private static final Pattern ARTIKEL_GRENZE = Pattern.compile("^Artikel[ \\t]+\\d+[a-z]?[ \\t]*$"); + private static final Pattern ARTIKEL_GRENZE = + Pattern.compile("^Artikel[ \\t]+\\d+[a-z]?[ \\t]*$"); /** Wahr, wenn die bei {@code von} beginnende Zeile eine Artikel-Überschrift ist. */ private static boolean beginntArtikelUeberschrift(String text, int von) { diff --git a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java index d8e710c..b8540c5 100644 --- a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java +++ b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java @@ -2,14 +2,14 @@ package eu.mulk.aendggner.anwendung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Anfuegung; -import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Ebene; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Aufhebung; +import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Ebene; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Ersetzung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.FussnotenAufhebung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.GliederungsUeberschriften; -import eu.mulk.aendggner.aenderung.Aenderungsbefehl.SatznummerierungStreichung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Neufassung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Sammelbefehl; +import eu.mulk.aendggner.aenderung.Aenderungsbefehl.SatznummerierungStreichung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Streichung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.StrukturEinfuegung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.StrukturErsetzung; @@ -298,7 +298,8 @@ public final class BefehlAnwender { private static List<StrukturErsetzung> ersetzungen(Aenderungsbefehl befehl) { return switch (befehl) { case StrukturErsetzung e -> List.of(e); - case Sammelbefehl s -> s.teilbefehle().stream().flatMap(t -> ersetzungen(t).stream()).toList(); + case Sammelbefehl s -> + s.teilbefehle().stream().flatMap(t -> ersetzungen(t).stream()).toList(); default -> List.of(); }; } @@ -374,8 +375,7 @@ public final class BefehlAnwender { // Führende Eigenbezeichnung („Abschnitt 2 …“ oder „2. Abschnitt …“) aus dem Zitat entfernen. var label = Pattern.compile("^(\\d+[a-z]?\\.\\s+\\S+|\\S+\\s+\\d+[a-z]?)\\s+").matcher(titel); if (label.find() - && kanonischeBezeichnung(label.group(1)) - .equals(kanonischeBezeichnung(alt.bezeichnung()))) { + && kanonischeBezeichnung(label.group(1)).equals(kanonischeBezeichnung(alt.bezeichnung()))) { titel = titel.substring(label.end()).strip(); } else if (titel.startsWith(alt.bezeichnung())) { titel = titel.substring(alt.bezeichnung().length()).strip(); @@ -395,7 +395,9 @@ public final class BefehlAnwender { return angewandt(befehl, alt.bezeichnung()); } - /** „Der bisherige Abschnitt 2 wird zu Abschnitt 3.“ — Bezeichnung der Gliederungseinheit umsetzen. */ + /** + * „Der bisherige Abschnitt 2 wird zu Abschnitt 3.“ — Bezeichnung der Gliederungseinheit umsetzen. + */ private static AngewandteAenderung wendeGliederungUmnummerierungAn( List<Gliederung> gliederungen, Umnummerierung befehl) { int idx = findeGliederung(gliederungen, befehl.stelle().gliederungsPfad()); @@ -451,12 +453,12 @@ public final class BefehlAnwender { if (idx < 0) { return manuell( befehl, - "Gliederungseinheit nicht gefunden: " - + pfad.get(pfad.size() - 1).bezeichnung()); + "Gliederungseinheit nicht gefunden: " + pfad.get(pfad.size() - 1).bezeichnung()); } indizes.add(idx); } - var alte = indizes.stream().map(gliederungen::get).collect(java.util.stream.Collectors.toSet()); + var alte = + indizes.stream().map(gliederungen::get).collect(java.util.stream.Collectors.toSet()); int einfuegePos = java.util.Collections.min(indizes); indizes.sort(java.util.Comparator.reverseOrder()); for (int idx : indizes) { @@ -479,7 +481,9 @@ public final class BefehlAnwender { } var anker = normen.get(aufloesung.normIndex()); int gliederungsPos = - anker.gliederung() != null ? gliederungen.indexOf(anker.gliederung()) + 1 : gliederungen.size(); + anker.gliederung() != null + ? gliederungen.indexOf(anker.gliederung()) + 1 + : gliederungen.size(); if (gliederungsPos == 0) { gliederungsPos = gliederungen.size(); } @@ -513,7 +517,8 @@ public final class BefehlAnwender { gefunden = -1; for (int i = 0; i < gliederungen.size(); i++) { var g = gliederungen.get(i); - if (kanonischeBezeichnung(g.bezeichnung()).equals(kanonischeBezeichnung(einheit.bezeichnung())) + if (kanonischeBezeichnung(g.bezeichnung()) + .equals(kanonischeBezeichnung(einheit.bezeichnung())) && (g.kennzahl() == null || g.kennzahl().startsWith(praefix))) { gefunden = i; break; @@ -542,34 +547,39 @@ public final class BefehlAnwender { normen, befehl, ohneFussnoten( - text -> { - if (befehl.amEnde()) { - var gestutzt = text.stripTrailing(); - if (!gestutzt.endsWith(befehl.alt())) { - return TextErgebnis.fehler("Der Text endet nicht mit „" + befehl.alt() + "“."); - } - var rumpf = gestutzt.substring(0, gestutzt.length() - befehl.alt().length()); - // Tritt an die Stelle des Satzzeichens ein Klammerzusatz („Der Punkt am Ende wird - // durch die Angabe „(Gesellschaftsdialog).“ ersetzt“), gehört davor ein Leerzeichen — - // so setzt es auch die amtliche Nachfassung. - var fuge = - befehl.neu().startsWith("(") - && !rumpf.isEmpty() - && Character.isLetterOrDigit(rumpf.charAt(rumpf.length() - 1)) - ? " " - : ""; - return TextErgebnis.ok(rumpf + fuge + befehl.neu()); - } - int anzahl = zaehleVorkommen(text, befehl.alt()); - if (anzahl == 0) { - return TextErgebnis.fehler("„" + befehl.alt() + "“ kommt im Zieltext nicht vor."); - } - if (anzahl > 1 && !befehl.jeweils()) { - return TextErgebnis.fehler( - "„" + befehl.alt() + "“ kommt " + anzahl + "-mal vor (ohne „jeweils“ mehrdeutig)."); - } - return TextErgebnis.ok(text.replace(befehl.alt(), befehl.neu())); - })); + text -> { + if (befehl.amEnde()) { + var gestutzt = text.stripTrailing(); + if (!gestutzt.endsWith(befehl.alt())) { + return TextErgebnis.fehler("Der Text endet nicht mit „" + befehl.alt() + "“."); + } + var rumpf = gestutzt.substring(0, gestutzt.length() - befehl.alt().length()); + // Tritt an die Stelle des Satzzeichens ein Klammerzusatz („Der Punkt am Ende wird + // durch die Angabe „(Gesellschaftsdialog).“ ersetzt“), gehört davor ein Leerzeichen + // — + // so setzt es auch die amtliche Nachfassung. + var fuge = + befehl.neu().startsWith("(") + && !rumpf.isEmpty() + && Character.isLetterOrDigit(rumpf.charAt(rumpf.length() - 1)) + ? " " + : ""; + return TextErgebnis.ok(rumpf + fuge + befehl.neu()); + } + int anzahl = zaehleVorkommen(text, befehl.alt()); + if (anzahl == 0) { + return TextErgebnis.fehler("„" + befehl.alt() + "“ kommt im Zieltext nicht vor."); + } + if (anzahl > 1 && !befehl.jeweils()) { + return TextErgebnis.fehler( + "„" + + befehl.alt() + + "“ kommt " + + anzahl + + "-mal vor (ohne „jeweils“ mehrdeutig)."); + } + return TextErgebnis.ok(text.replace(befehl.alt(), befehl.neu())); + })); } private static AngewandteAenderung wendeStreichungAn(List<Norm> normen, Streichung befehl) { @@ -577,19 +587,21 @@ public final class BefehlAnwender { normen, befehl, ohneFussnoten( - text -> { - int anzahl = zaehleVorkommen(text, befehl.woerter()); - if (anzahl == 0) { - return TextErgebnis.fehler("„" + befehl.woerter() + "“ kommt im Zieltext nicht vor."); - } - if (anzahl > 1) { - return TextErgebnis.fehler("„" + befehl.woerter() + "“ kommt " + anzahl + "-mal vor."); - } - return TextErgebnis.ok( - text.replace(befehl.woerter(), "") - .replaceAll(" +", " ") - .replaceAll(" ([,;.])", "$1")); - })); + text -> { + int anzahl = zaehleVorkommen(text, befehl.woerter()); + if (anzahl == 0) { + return TextErgebnis.fehler( + "„" + befehl.woerter() + "“ kommt im Zieltext nicht vor."); + } + if (anzahl > 1) { + return TextErgebnis.fehler( + "„" + befehl.woerter() + "“ kommt " + anzahl + "-mal vor."); + } + return TextErgebnis.ok( + text.replace(befehl.woerter(), "") + .replaceAll(" +", " ") + .replaceAll(" ([,;.])", "$1")); + })); } private static AngewandteAenderung wendeWoerterEinfuegungAn( @@ -598,50 +610,57 @@ public final class BefehlAnwender { normen, befehl, ohneFussnoten( - text -> - switch (befehl.anker()) { - case WortAnker.NachWoertern nach -> { - var pruefung = eindeutigeFundstelle(text, nach.woerter()); - if (pruefung.fehler() != null) { - yield TextErgebnis.fehler(pruefung.fehler()); - } - int ende = pruefung.index() + nach.woerter().length(); - yield TextErgebnis.ok( - text.substring(0, ende) + fuge(befehl.woerter()) + befehl.woerter() - + text.substring(ende)); - } - case WortAnker.VorWoertern vor -> { - var pruefung = eindeutigeFundstelle(text, vor.woerter()); - if (pruefung.fehler() != null) { - yield TextErgebnis.fehler(pruefung.fehler()); - } - yield TextErgebnis.ok( - text.substring(0, pruefung.index()) - + befehl.woerter() - + " " - + text.substring(pruefung.index())); - } - case WortAnker.VorKommaAmEnde ignoriert -> { - var gestutzt = text.stripTrailing(); - if (!gestutzt.endsWith(",")) { - yield TextErgebnis.fehler("Der Zieltext endet nicht mit einem Komma."); - } - yield TextErgebnis.ok( - gestutzt.substring(0, gestutzt.length() - 1) + " " + befehl.woerter() + ","); - } - case WortAnker.AmEnde ignoriert -> { - var gestutzt = text.stripTrailing(); - if (gestutzt.endsWith(".") || gestutzt.endsWith(",") || gestutzt.endsWith(";")) { - var satzzeichen = gestutzt.charAt(gestutzt.length() - 1); - yield TextErgebnis.ok( - gestutzt.substring(0, gestutzt.length() - 1) - + " " - + befehl.woerter() - + satzzeichen); - } - yield TextErgebnis.ok(gestutzt + " " + befehl.woerter()); - } - })); + text -> + switch (befehl.anker()) { + case WortAnker.NachWoertern nach -> { + var pruefung = eindeutigeFundstelle(text, nach.woerter()); + if (pruefung.fehler() != null) { + yield TextErgebnis.fehler(pruefung.fehler()); + } + int ende = pruefung.index() + nach.woerter().length(); + yield TextErgebnis.ok( + text.substring(0, ende) + + fuge(befehl.woerter()) + + befehl.woerter() + + text.substring(ende)); + } + case WortAnker.VorWoertern vor -> { + var pruefung = eindeutigeFundstelle(text, vor.woerter()); + if (pruefung.fehler() != null) { + yield TextErgebnis.fehler(pruefung.fehler()); + } + yield TextErgebnis.ok( + text.substring(0, pruefung.index()) + + befehl.woerter() + + " " + + text.substring(pruefung.index())); + } + case WortAnker.VorKommaAmEnde ignoriert -> { + var gestutzt = text.stripTrailing(); + if (!gestutzt.endsWith(",")) { + yield TextErgebnis.fehler("Der Zieltext endet nicht mit einem Komma."); + } + yield TextErgebnis.ok( + gestutzt.substring(0, gestutzt.length() - 1) + + " " + + befehl.woerter() + + ","); + } + case WortAnker.AmEnde ignoriert -> { + var gestutzt = text.stripTrailing(); + if (gestutzt.endsWith(".") + || gestutzt.endsWith(",") + || gestutzt.endsWith(";")) { + var satzzeichen = gestutzt.charAt(gestutzt.length() - 1); + yield TextErgebnis.ok( + gestutzt.substring(0, gestutzt.length() - 1) + + " " + + befehl.woerter() + + satzzeichen); + } + yield TextErgebnis.ok(gestutzt + " " + befehl.woerter()); + } + })); } // --- Strukturoperationen ------------------------------------------------------------------- @@ -876,7 +895,8 @@ public final class BefehlAnwender { var text = absatz.text(); var einrueckung = einrueckungVon(text, von); var ersatz = rueckeZitatEin(normalisiereZitatText(befehl.text()), einrueckung); - absaetze.set(f1.absatzIndex(), absatz.mitText(text.substring(0, von) + ersatz + text.substring(bis))); + absaetze.set( + f1.absatzIndex(), absatz.mitText(text.substring(0, von) + ersatz + text.substring(bis))); normen.set(f1.normIndex(), norm.mitAbsaetzen(absaetze)); return angewandt(befehl, norm.enbez()); } @@ -911,8 +931,7 @@ public final class BefehlAnwender { yield angewandt(befehl, neue.stream().map(Norm::enbez).toList()); } - var sigelNeu = - befehl.stelle().paragraph().map(Stelle.Paragraph::sigel).orElse("§"); + var sigelNeu = befehl.stelle().paragraph().map(Stelle.Paragraph::sigel).orElse("§"); var enbezNeu = sigelNeu + " " + befehl.bezeichnung(); if (StellenAufloeser.normIndex(gesetzAus(normen), enbezNeu) >= 0) { yield manuell(befehl, enbezNeu + " existiert bereits im Stammgesetz."); @@ -1016,7 +1035,10 @@ public final class BefehlAnwender { normalisiereZitatText(befehl.text()), einrueckungVon(text, zeilenAnfang)); return TextErgebnis.ok( befehl.vorher() - ? text.substring(0, zeilenAnfang) + block + "\n" + text.substring(zeilenAnfang) + ? text.substring(0, zeilenAnfang) + + block + + "\n" + + text.substring(zeilenAnfang) : text.substring(0, zeilenEnde) + "\n" + block + text.substring(zeilenEnde)); })); } @@ -1176,8 +1198,10 @@ public final class BefehlAnwender { var absaetze = new ArrayList<>(norm.absaetze()); int quelleIdx = fundstelle.absatzIndex(); var neueNummer = neuAbsatz.get().nummer(); - // Ein leerer Platzhalter-Absatz mit der Zielnummer (weggefallen/gegenstandslos) wird durch die - // Umnummerierung überschrieben (analog zur Norm-Umnummerierung auf eine weggefallene Zielnorm). + // Ein leerer Platzhalter-Absatz mit der Zielnummer (weggefallen/gegenstandslos) wird durch + // die + // Umnummerierung überschrieben (analog zur Norm-Umnummerierung auf eine weggefallene + // Zielnorm). for (int i = absaetze.size() - 1; i >= 0; i--) { if (i != quelleIdx && neueNummer.equals(absaetze.get(i).nummer()) @@ -1208,8 +1232,11 @@ public final class BefehlAnwender { .matcher(text.substring(bereich.von(), bereich.bis())); if (!marke.find()) { return TextErgebnis.fehler( - "„" + alteMarke + "“ steht nicht am Anfang von " - + befehl.stelle().anzeigeText() + "."); + "„" + + alteMarke + + "“ steht nicht am Anfang von " + + befehl.stelle().anzeigeText() + + "."); } var umbenannt = text.substring(0, bereich.von()) @@ -1235,9 +1262,11 @@ public final class BefehlAnwender { var absaetze = new ArrayList<>(norm.absaetze()); var absatz = absaetze.get(fundstelle.absatzIndex()); var text = absatz.text(); - var marker = Superskript.LAUF.matcher(text).region(fundstelle.bereich().von(), fundstelle.bereich().bis()); - if (marker.lookingAt() - && Superskript.istSatzanfang(text, marker.start(), marker.end())) { + var marker = + Superskript.LAUF + .matcher(text) + .region(fundstelle.bereich().von(), fundstelle.bereich().bis()); + if (marker.lookingAt() && Superskript.istSatzanfang(text, marker.start(), marker.end())) { var neuerText = text.substring(0, marker.start()) + Superskript.zuSuperskript(neuSatz.nummer()) @@ -1279,7 +1308,9 @@ public final class BefehlAnwender { */ private static String entferneWeggefallenenPlatzhalter(String text, String marke) { return text.replaceFirst( - "(?m)^[ \\t]*" + Pattern.quote(marke) + "[ \\t]+\\((?:weggefallen|gegenstandslos|aufgehoben)\\)\\n?", + "(?m)^[ \\t]*" + + Pattern.quote(marke) + + "[ \\t]+\\((?:weggefallen|gegenstandslos|aufgehoben)\\)\\n?", ""); } @@ -1310,8 +1341,7 @@ public final class BefehlAnwender { // Steht genau ein unnummerierter Absatz zwischen nummerierten (bayerische Folge „Dem Wortlaut // werden … Abs. 1 bis 4 vorangestellt“ → „Der bisherige Wortlaut wird Abs. 5“), erhält nur // dieser die Nummer. Sind alle Absätze unnummeriert, wird der Gesamtwortlaut zu einem Absatz. - var unnummerierte = - norm.absaetze().stream().filter(a -> a.nummer() == null).toList(); + var unnummerierte = norm.absaetze().stream().filter(a -> a.nummer() == null).toList(); if (unnummerierte.size() == 1 && norm.absaetze().size() > 1) { var absaetze = new ArrayList<>(norm.absaetze()); int idx = absaetze.indexOf(unnummerierte.get(0)); @@ -1427,11 +1457,7 @@ public final class BefehlAnwender { if (!fehlend.isEmpty()) { return manuell( befehl, - "Fußnote " - + String.join(", ", fehlend) - + " kommt in " - + norm.enbez() - + " nicht vor."); + "Fußnote " + String.join(", ", fehlend) + " kommt in " + norm.enbez() + " nicht vor."); } normen.set(aufloesung.normIndex(), norm.mitAbsaetzen(absaetze)); return angewandt(befehl, norm.enbez()); @@ -1475,7 +1501,11 @@ public final class BefehlAnwender { betroffene.addAll(ergebnis.betroffeneEnbez()); if (ergebnis.status() != Status.ANGEWANDT) { meldungen[index] = - "Teil " + (index + 1) + " (" + teil.stelle().anzeigeText() + "): " + "Teil " + + (index + 1) + + " (" + + teil.stelle().anzeigeText() + + "): " + ergebnis.begruendung(); } } @@ -1742,7 +1772,8 @@ public final class BefehlAnwender { /** * Zerlegt einen Zitatblock mehrerer Paragraphen an den §-Überschriften (nicht an Querverweisen) - * und parst jeden Abschnitt zu einer {@link Norm}. Die Gliederung wird von der Vorlage übernommen. + * und parst jeden Abschnitt zu einer {@link Norm}. Die Gliederung wird von der Vorlage + * übernommen. */ private static List<Norm> parseNormenBlock(String block, @Nullable Gliederung gliederung) { var normen = new ArrayList<Norm>(); @@ -1868,10 +1899,10 @@ public final class BefehlAnwender { } /** - * Rückt die kanonischen Zitatzeilen auf die Ziel-Einrückung um: Aufzählungszeilen (und die - * erste Zeile) auf {@code einrueckung}, Fortsetzungszeilen — etwa der Definitionstext unter - * einer Kurzüberschrift — zwei Zeichen tiefer, damit sie Kindzeilen der Einheit bleiben - * (dieselbe Form, die der ContentFlattener aus dem Stammgesetz-XML erzeugt). + * Rückt die kanonischen Zitatzeilen auf die Ziel-Einrückung um: Aufzählungszeilen (und die erste + * Zeile) auf {@code einrueckung}, Fortsetzungszeilen — etwa der Definitionstext unter einer + * Kurzüberschrift — zwei Zeichen tiefer, damit sie Kindzeilen der Einheit bleiben (dieselbe Form, + * die der ContentFlattener aus dem Stammgesetz-XML erzeugt). */ private static String rueckeZitatEin(String zitat, String einrueckung) { var sb = new StringBuilder(); diff --git a/src/main/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtAnwender.java index e9f84b6..acd42d7 100644 --- a/src/main/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtAnwender.java +++ b/src/main/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtAnwender.java @@ -74,12 +74,13 @@ final class InhaltsuebersichtAnwender { /** Die adressierte Angabe: die §-/Gliederungs-Komponenten hinter der Inhaltsübersichts-Marke. */ private static List<Stelle.Komponente> zielKette(Stelle stelle) { return stelle.komponenten().stream() - .filter( - k -> k instanceof Stelle.Paragraph || k instanceof Stelle.Gliederungseinheit) + .filter(k -> k instanceof Stelle.Paragraph || k instanceof Stelle.Gliederungseinheit) .toList(); } - /** Ersetzt die Zeilen von {@code von} bis {@code bis} durch die Angaben des Zitats (oder nichts). */ + /** + * Ersetzt die Zeilen von {@code von} bis {@code bis} durch die Angaben des Zitats (oder nichts). + */ private static AngewandteAenderung ersetzeZeilen( List<Norm> normen, Aenderungsbefehl befehl, @@ -139,8 +140,7 @@ final class InhaltsuebersichtAnwender { // --- Zeilenmodell ---------------------------------------------------------------------------- - private record Zeilenfund( - int absatzIndex, int vonZeile, int bisZeile, @Nullable String fehler) { + private record Zeilenfund(int absatzIndex, int vonZeile, int bisZeile, @Nullable String fehler) { static Zeilenfund fehlgeschlagen(String begruendung) { return new Zeilenfund(-1, -1, -1, begruendung); } @@ -148,8 +148,8 @@ final class InhaltsuebersichtAnwender { /** * Findet die (norm-weit eindeutige) Zeile der adressierten Angabe. Die Kette wird verschachtelt - * aufgelöst: „Teil 2 Abschnitt 4“ sucht die Abschnitt-Zeile erst hinter der Teil-2-Zeile (und - * vor dem nächsten Teil), sodass gleichnamige Abschnitte anderer Teile nicht stören. + * aufgelöst: „Teil 2 Abschnitt 4“ sucht die Abschnitt-Zeile erst hinter der Teil-2-Zeile (und vor + * dem nächsten Teil), sodass gleichnamige Abschnitte anderer Teile nicht stören. */ private static Zeilenfund findeZeile(Norm norm, List<Stelle.Komponente> kette) { var ziel = kette.get(kette.size() - 1); diff --git a/src/main/java/eu/mulk/aendggner/anwendung/SatzTeiler.java b/src/main/java/eu/mulk/aendggner/anwendung/SatzTeiler.java index b457cf2..3360bc6 100644 --- a/src/main/java/eu/mulk/aendggner/anwendung/SatzTeiler.java +++ b/src/main/java/eu/mulk/aendggner/anwendung/SatzTeiler.java @@ -11,8 +11,8 @@ import org.jspecify.annotations.Nullable; * Zerlegt einen Absatztext in Sätze im Rechtssinne, mit einer Stoppliste für juristische * Abkürzungen und Datumsangaben („Abs.“, „31. März 2021“). * - * <p>Trägt der Text amtliche Satznummern als Unicode-Superskripte (bayerisches Landesrecht, - * „¹Die freilebende Tierwelt …“), wird exakt an diesen geteilt statt heuristisch. + * <p>Trägt der Text amtliche Satznummern als Unicode-Superskripte (bayerisches Landesrecht, „¹Die + * freilebende Tierwelt …“), wird exakt an diesen geteilt statt heuristisch. */ public final class SatzTeiler { @@ -182,8 +182,8 @@ public final class SatzTeiler { /** * Punkt einer Aufzählungsmarke am Zeilenanfang („… folgende Aufgaben ⏎ 1. Erlaß von Satzungen * …“). Er beendet keinen Satz: die Glieder einer Aufzählung gehören zum tragenden Satz. Die - * Beschränkung auf den Zeilenanfang unterscheidet die Marke von einer Zahl am Satzende („… beträgt - * 30. Die Frist …“). + * Beschränkung auf den Zeilenanfang unterscheidet die Marke von einer Zahl am Satzende („… + * beträgt 30. Die Frist …“). */ private static boolean istAufzaehlungsMarke(String text, int punktPosition) { var wort = wortVor(text, punktPosition); diff --git a/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java b/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java index dd8409e..ff77279 100644 --- a/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java +++ b/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java @@ -164,7 +164,9 @@ final class StellenAufloeser { } bereich = zeilenBlock( - text, labelRegex, bereich != null ? bereich : new SatzTeiler.SatzBereich(0, text.length())); + text, + labelRegex, + bereich != null ? bereich : new SatzTeiler.SatzBereich(0, text.length())); if (bereich == null) { return null; } diff --git a/src/main/java/eu/mulk/aendggner/gesetz/Gliederung.java b/src/main/java/eu/mulk/aendggner/gesetz/Gliederung.java index 7a09093..00106d2 100644 --- a/src/main/java/eu/mulk/aendggner/gesetz/Gliederung.java +++ b/src/main/java/eu/mulk/aendggner/gesetz/Gliederung.java @@ -6,8 +6,8 @@ import org.jspecify.annotations.Nullable; * Eine Gliederungseinheit (z.B. „Teil 2 — Anforderungen an zu errichtende Gebäude“ oder „2. * Abschnitt — Koordinierung und Früherkennung“). * - * @param kennzahl die hierarchie-kodierende Gliederungskennzahl aus dem gii-XML (z.B. {@code 020020} - * für Abschnitt 2 in Teil 2); {@code null} bei anderweitig konstruierten Einheiten. + * @param kennzahl die hierarchie-kodierende Gliederungskennzahl aus dem gii-XML (z.B. {@code + * 020020} für Abschnitt 2 in Teil 2); {@code null} bei anderweitig konstruierten Einheiten. * @param bezeichnung die relative Bezeichnung („Teil 2“, „Abschnitt 2“). * @param titel die Überschrift der Einheit ({@code null} bei titellosen Einheiten). */ diff --git a/src/main/java/eu/mulk/aendggner/gesetz/Superskript.java b/src/main/java/eu/mulk/aendggner/gesetz/Superskript.java index 733c1f7..4ba8304 100644 --- a/src/main/java/eu/mulk/aendggner/gesetz/Superskript.java +++ b/src/main/java/eu/mulk/aendggner/gesetz/Superskript.java @@ -46,9 +46,9 @@ public final class Superskript { } /** - * Wahr, wenn {@code text} mindestens eine amtliche Satznummer am Satzanfang trägt. Dient dazu, die - * Schreibweise eines Stammgesetzes zu erkennen (Landesrecht mit amtlicher Satzzählung behält seine - * Superskripte, Bundesrecht ohne solche verwirft sie). + * Wahr, wenn {@code text} mindestens eine amtliche Satznummer am Satzanfang trägt. Dient dazu, + * die Schreibweise eines Stammgesetzes zu erkennen (Landesrecht mit amtlicher Satzzählung behält + * seine Superskripte, Bundesrecht ohne solche verwirft sie). */ public static boolean traegtSatznummern(String text) { var m = LAUF.matcher(text); diff --git a/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtLoader.java b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtLoader.java index ecb10f8..c64ca21 100644 --- a/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtLoader.java +++ b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtLoader.java @@ -6,17 +6,17 @@ import eu.mulk.aendggner.aenderung.parse.TextBereiniger; import eu.mulk.aendggner.gesetz.Gesetz; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.text.Normalizer; import java.nio.file.Files; import java.nio.file.Path; +import java.text.Normalizer; import org.apache.tika.Tika; import org.jboss.logging.Logger; /** * Liest ein Stammgesetz des Landesrechts aus der konsolidierten Fassung — als PDF oder als * kanonischer Klartext. Deckt bayerisches Landesrecht (Gliederung in {@code Art.}, amtliche - * Satznummern) ebenso ab wie die {@code §}-gegliederten Gesetze der übrigen Länder; das Sigel - * folgt je Norm aus dem Text (siehe {@link LandesRechtTextParser}). + * Satznummern) ebenso ab wie die {@code §}-gegliederten Gesetze der übrigen Länder; das Sigel folgt + * je Norm aus dem Text (siehe {@link LandesRechtTextParser}). * * <p>PDFs durchlaufen dieselbe Aufbereitung wie Änderungsgesetze (Fontgrößen-Filter mit * Superskript-Erhalt, Textbereinigung); der bereinigte Lineartext ist zugleich das dokumentierte @@ -56,7 +56,7 @@ public final class LandesRechtLoader { /** * Stellt einen vom Zeilen-Reflow an das Satzende der Vornorm geklebten Normkopf („… verlangen. - * Art. 17 Jagderlaubnis“) wieder auf eine eigene Zeile. Das doppelte Leerzeichen zwischen + * Art. 17 Jagderlaubnis“) wieder auf eine eigene Zeile. Das doppelte Leerzeichen zwischen * Norm-Nummer und Titel unterscheidet den Normkopf von gewöhnlichen Querverweisen. */ private static String nachSatzendeGetrennteNormkoepfe(String text) { diff --git a/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java index 6c58c49..a256243 100644 --- a/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java +++ b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java @@ -57,20 +57,22 @@ final class LandesRechtTextParser { // nachgestelltem Schlüsselwort („I. Abschnitt“, Muster GLIEDERUNG). Der Titel steht — nach der // kanonischen Aufbereitung — mit doppeltem Leerzeichen auf derselben Zeile. private static final Pattern GLIEDERUNG_ARABISCH = - Pattern.compile("^(Buch|Teil|Kapitel|Abschnitt|Unterabschnitt|Titel) (\\d+[a-z]?)\\s+(\\S.*)$"); + Pattern.compile( + "^(Buch|Teil|Kapitel|Abschnitt|Unterabschnitt|Titel) (\\d+[a-z]?)\\s+(\\S.*)$"); // Römische Gliederung ohne Schlüsselwort („I. Rechtsform und Aufgaben“, NRW). Sie ist nur an der // Stellung erkennbar, deshalb gilt dieselbe Absicherung wie für UNTER_GLIEDERUNG: kurzer, // großgeschriebener, satzzeichenfreier Titel und ein Normkopf oder eine weitere Überschrift als // nächste nicht-leere Zeile. - private static final Pattern GLIEDERUNG_ROEMISCH = Pattern.compile("^([IVXLCDM]+)\\.\\s+(\\S.*)$"); + private static final Pattern GLIEDERUNG_ROEMISCH = + Pattern.compile("^([IVXLCDM]+)\\.\\s+(\\S.*)$"); private static final Pattern UNTER_GLIEDERUNG = Pattern.compile("^(\\d+[a-z]?)\\.\\s+(\\S.*)$"); /** * Die Inhaltsübersicht ist eine Norm ohne Sigel; Angabe-Befehle adressieren sie unter genau - * diesem Namen (siehe {@code InhaltsuebersichtAnwender}). Ihre Zeilen tragen das - * Übersichtsformat „§ N | Titel“. + * diesem Namen (siehe {@code InhaltsuebersichtAnwender}). Ihre Zeilen tragen das Übersichtsformat + * „§ N | Titel“. */ private static final String INHALTSUEBERSICHT = "Inhaltsübersicht"; @@ -81,12 +83,14 @@ final class LandesRechtTextParser { Pattern.compile( "^(§|Art\\.)\\s+(\\d+[a-z]?)\\s+" // Querverweis-Schlüsselwörter nur als ganzes Wort ausschließen: „§ 4 Satz 2“ ist ein - // Verweis, „§ 4 Satzungen“ dagegen ein Normtitel. Der Schutz „(?![a-zäöüß])“ verhindert, + // Verweis, „§ 4 Satzungen“ dagegen ein Normtitel. Der Schutz „(?![a-zäöüß])“ + // verhindert, // dass „Satz“ auch „Satzungen“, „Nummer“ auch „Nummerierung“ trifft. + "(?!(?:Absatz|Abs\\.|Satz|Sätze|Nummer|Nr\\.|Buchstabe|Buchst\\." + "|und|bis|oder|sowie|des|der|dieses)(?![a-zäöüß]))" // Der Titel endet nicht auf einen Punkt: ein Normkopf trägt eine Überschrift, keinen - // ganzen Satz. So wird ein am Zeilenanfang stehender Querverweis-Satz („§ 7 GAPInVeKoSG + // ganzen Satz. So wird ein am Zeilenanfang stehender Querverweis-Satz („§ 7 + // GAPInVeKoSG // findet entsprechend Anwendung.“) nicht fälschlich als Normkopf „§ 7“ gelesen. + "((?:\\p{Lu}|\\().*[^.])\\s*$"); @@ -210,8 +214,7 @@ final class LandesRechtTextParser { gliederungsZaehler++; elternKennzahl = String.format("%03d", gliederungsZaehler); aktuelleGliederung = - new Gliederung( - elternKennzahl, roemisch.group(1) + ".", roemisch.group(2).strip()); + new Gliederung(elternKennzahl, roemisch.group(1) + ".", roemisch.group(2).strip()); gliederungen.add(aktuelleGliederung); } else { var kennzahl = @@ -278,8 +281,8 @@ final class LandesRechtTextParser { } /** - * Ein Normkopf eröffnet nur dann eine neue Norm, wenn seine Nummer hinter der letzten liegt — - * das fängt Querverweise ab, die die Negativliste nicht ausschließt. + * Ein Normkopf eröffnet nur dann eine neue Norm, wenn seine Nummer hinter der letzten liegt — das + * fängt Querverweise ab, die die Negativliste nicht ausschließt. */ private static boolean istNeuerNormKopf(String nummer, int letzteNormNummer) { return numerisch(nummer) >= letzteNormNummer; @@ -315,10 +318,7 @@ final class LandesRechtTextParser { } private static Norm baueNorm( - String enbez, - @Nullable String titel, - @Nullable Gliederung gliederung, - List<String> zeilen) { + String enbez, @Nullable String titel, @Nullable Gliederung gliederung, List<String> zeilen) { boolean weggefallen = titel != null && WEGGEFALLEN_TITEL.matcher(titel).matches(); var absaetze = new ArrayList<Absatz>(); diff --git a/src/main/java/eu/mulk/aendggner/synopse/SynopseBuilder.java b/src/main/java/eu/mulk/aendggner/synopse/SynopseBuilder.java index 1568aa9..ce4f1a1 100644 --- a/src/main/java/eu/mulk/aendggner/synopse/SynopseBuilder.java +++ b/src/main/java/eu/mulk/aendggner/synopse/SynopseBuilder.java @@ -50,13 +50,13 @@ public final class SynopseBuilder { var manuell = anwendung.protokoll().stream().filter(a -> a.status() == Status.MANUELL_PRUEFEN).toList(); - return new Synopse(alt, neu, eintraege, gliederungsAenderungen(alt, neu), manuell, parseWarnungen); + return new Synopse( + alt, neu, eintraege, gliederungsAenderungen(alt, neu), manuell, parseWarnungen); } /** - * Paart die Gliederungseinheiten nach Kennzahl und sammelt die mit geänderter Überschrift. - * Neu eingefügte Einheiten (ohne Kennzahl und ohne Alt-Pendant) erscheinen mit {@code alt == - * null}. + * Paart die Gliederungseinheiten nach Kennzahl und sammelt die mit geänderter Überschrift. Neu + * eingefügte Einheiten (ohne Kennzahl und ohne Alt-Pendant) erscheinen mit {@code alt == null}. */ private static List<Synopse.GliederungsAenderung> gliederungsAenderungen(Gesetz alt, Gesetz neu) { var aenderungen = new ArrayList<Synopse.GliederungsAenderung>(); diff --git a/src/main/java/eu/mulk/aendggner/web/Multipart.java b/src/main/java/eu/mulk/aendggner/web/Multipart.java index 3e5a059..745443b 100644 --- a/src/main/java/eu/mulk/aendggner/web/Multipart.java +++ b/src/main/java/eu/mulk/aendggner/web/Multipart.java @@ -17,7 +17,8 @@ final class Multipart { private static final Pattern BOUNDARY_PATTERN = Pattern.compile("boundary=\"?([^\";]+)\"?"); private static final Pattern NAME_PATTERN = Pattern.compile("name=\"([^\"]*)\""); private static final Pattern FILENAME_PATTERN = Pattern.compile("filename=\"([^\"]*)\""); - private static final byte[] HEADER_BODY_SEPARATOR = "\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1); + private static final byte[] HEADER_BODY_SEPARATOR = + "\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1); private Multipart() {} @@ -33,7 +34,9 @@ final class Multipart { var delimiter = ("--" + boundary).getBytes(StandardCharsets.ISO_8859_1); var positions = new ArrayList<Integer>(); - for (int i = indexOf(body, delimiter, 0); i >= 0; i = indexOf(body, delimiter, i + delimiter.length)) { + for (int i = indexOf(body, delimiter, 0); + i >= 0; + i = indexOf(body, delimiter, i + delimiter.length)) { positions.add(i); } diff --git a/src/main/java/eu/mulk/aendggner/web/StaticHandler.java b/src/main/java/eu/mulk/aendggner/web/StaticHandler.java index 8cdf6e5..24a6e41 100644 --- a/src/main/java/eu/mulk/aendggner/web/StaticHandler.java +++ b/src/main/java/eu/mulk/aendggner/web/StaticHandler.java @@ -7,7 +7,10 @@ import java.io.InputStream; import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; -/** Liefert eine einzelne, beim Start einmal geladene statische Ressource aus (Formular, Impressum, CSS, …). */ +/** + * Liefert eine einzelne, beim Start einmal geladene statische Ressource aus (Formular, Impressum, + * CSS, …). + */ final class StaticHandler implements HttpHandler { private final byte[] content; @@ -48,7 +51,8 @@ final class StaticHandler implements HttpHandler { } } - private static void sendText(HttpExchange exchange, int status, String message) throws IOException { + private static void sendText(HttpExchange exchange, int status, String message) + throws IOException { var body = message.getBytes(StandardCharsets.UTF_8); exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8"); exchange.sendResponseHeaders(status, body.length); diff --git a/src/main/java/eu/mulk/aendggner/web/UploadHandler.java b/src/main/java/eu/mulk/aendggner/web/UploadHandler.java index 8765ac3..efcb512 100644 --- a/src/main/java/eu/mulk/aendggner/web/UploadHandler.java +++ b/src/main/java/eu/mulk/aendggner/web/UploadHandler.java @@ -21,19 +21,20 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; /** - * {@code POST /synopse} — nimmt Stammgesetz- und Änderungsgesetz-Datei(en) per - * {@code multipart/form-data} entgegen, ruft {@link Pipeline#erzeugeSynopse} auf und liefert das - * erzeugte HTML zurück. + * {@code POST /synopse} — nimmt Stammgesetz- und Änderungsgesetz-Datei(en) per {@code + * multipart/form-data} entgegen, ruft {@link Pipeline#erzeugeSynopse} auf und liefert das erzeugte + * HTML zurück. * - * <p>Hochgeladene Dateien landen ausschließlich als temporäre Dateien für die Dauer der Anfrage - * und werden danach in jedem Fall gelöscht — es wird nichts dauerhaft gespeichert. Die eigentliche + * <p>Hochgeladene Dateien landen ausschließlich als temporäre Dateien für die Dauer der Anfrage und + * werden danach in jedem Fall gelöscht — es wird nichts dauerhaft gespeichert. Die eigentliche * Verarbeitung läuft auf einem auf die Kernzahl begrenzten Thread-Pool mit fester Warteschlange; * bei Überlast wird sofort mit {@code 503} abgelehnt, statt unbegrenzt Arbeit anzunehmen. */ final class UploadHandler implements HttpHandler { private static final long MAX_PART_BYTES = 15L * 1024 * 1024; // 15 MB je Datei - private static final long MAX_BODY_BYTES = 40L * 1024 * 1024; // Sicherheitsnetz für die gesamte Anfrage + private static final long MAX_BODY_BYTES = + 40L * 1024 * 1024; // Sicherheitsnetz für die gesamte Anfrage private static final long TIMEOUT_SECONDS = 30; private final ExecutorService pipelinePool; @@ -77,7 +78,8 @@ final class UploadHandler implements HttpHandler { try { Files.deleteIfExists(path); } catch (IOException ignored) { - // Aufräumen ist best effort; ein verwaistes Temp-File blockiert die Antwort nicht. + // Aufräumen ist best effort; ein verwaistes Temp-File blockiert die Antwort + // nicht. } }); } catch (IOException ignored) { @@ -169,7 +171,8 @@ final class UploadHandler implements HttpHandler { var future = pipelinePool.submit(job); ergebnis = future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); } catch (RejectedExecutionException e) { - sendText(exchange, 503, "Der Dienst ist gerade ausgelastet. Bitte in Kürze erneut versuchen."); + sendText( + exchange, 503, "Der Dienst ist gerade ausgelastet. Bitte in Kürze erneut versuchen."); return; } catch (TimeoutException e) { sendText(exchange, 504, "Die Verarbeitung hat zu lange gedauert und wurde abgebrochen."); @@ -215,7 +218,8 @@ final class UploadHandler implements HttpHandler { return filename.replaceAll("[^A-Za-z0-9._-]", "_"); } - private static void sendText(HttpExchange exchange, int status, String message) throws IOException { + private static void sendText(HttpExchange exchange, int status, String message) + throws IOException { var body = message.getBytes(StandardCharsets.UTF_8); exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8"); exchange.sendResponseHeaders(status, body.length); diff --git a/src/main/java/eu/mulk/aendggner/web/WebMain.java b/src/main/java/eu/mulk/aendggner/web/WebMain.java index 9d71558..815f0db 100644 --- a/src/main/java/eu/mulk/aendggner/web/WebMain.java +++ b/src/main/java/eu/mulk/aendggner/web/WebMain.java @@ -34,7 +34,8 @@ public final class WebMain { "/datenschutz", new StaticHandler("/eu/mulk/aendggner/web/datenschutz.html", "text/html; charset=utf-8")); server.createContext( - "/style.css", new StaticHandler("/eu/mulk/aendggner/web/style.css", "text/css; charset=utf-8")); + "/style.css", + new StaticHandler("/eu/mulk/aendggner/web/style.css", "text/css; charset=utf-8")); server.createContext("/synopse", new UploadHandler()); server.start(); |
