blob: 0fdfa91d5e3f60e0c923365a2704f21a079eb7aa [file] [log] [blame]
// SPDX-FileCopyrightText: 2026 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
// SPDX-License-Identifier: AGPL-3.0-or-later
package eu.mulk.aendggner;
import eu.mulk.aendggner.aenderung.DokumentArt;
import eu.mulk.aendggner.aenderung.DokumentKopf;
import eu.mulk.aendggner.aenderung.Inkrafttreten;
import eu.mulk.aendggner.aenderung.parse.AenderungsantragParser;
import eu.mulk.aendggner.aenderung.parse.AenderungsgesetzParser;
import eu.mulk.aendggner.aenderung.parse.DeutschesDatum;
import eu.mulk.aendggner.aenderung.parse.DokumentErkenner;
import eu.mulk.aendggner.aenderung.parse.EntwurfsPatcher;
import eu.mulk.aendggner.aenderung.parse.PatchTextExtraktor;
import eu.mulk.aendggner.aenderung.parse.SuperskriptModus;
import eu.mulk.aendggner.aenderung.parse.TextBereiniger;
import eu.mulk.aendggner.anwendung.BefehlAnwender;
import eu.mulk.aendggner.anwendung.Grund;
import eu.mulk.aendggner.anwendung.InhaltsuebersichtsProbe;
import eu.mulk.aendggner.anwendung.Nachfassungsabgleich;
import eu.mulk.aendggner.gesetz.Fortschreibung;
import eu.mulk.aendggner.gesetz.Gesetz;
import eu.mulk.aendggner.gesetz.Superskript;
import eu.mulk.aendggner.gesetz.gii.GiiXmlLoader;
import eu.mulk.aendggner.gesetz.land.LandesRechtLoader;
import eu.mulk.aendggner.gesetz.land.LandesRechtTextAusgeber;
import eu.mulk.aendggner.synopse.HtmlRenderer;
import eu.mulk.aendggner.synopse.SynopseBuilder;
import java.io.IOException;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.jspecify.annotations.Nullable;
/**
* Kernpipeline: Stammgesetz laden → Änderungsgesetze parsen und anwenden → Synopse rendern.
*
* <p>Wird sowohl von der Befehlszeile ({@link AendGgner}) als auch von der Browserfassung ({@code
* eu.mulk.aendggner.wasm.BrowserMain}) verwendet, damit die Anwendungslogik nur an einer Stelle
* existiert. Sie kennt kein Dateisystem: Eingaben kommen als {@link Quelle} (Name und Bytes).
*/
public final class Pipeline {
private static final org.jboss.logging.Logger log =
org.jboss.logging.Logger.getLogger(Pipeline.class);
private Pipeline() {}
/**
* Was ein Auftrag zu erledigen ist. Der Record tritt an die Stelle einer Überladungsleiter: Mit
* jedem weiteren Wahlrecht wuchs die Zahl der Signaturen, und die Aufrufer mussten Angaben
* durchreichen, die sie nichts angehen. Pflicht sind das Stammgesetz und wenigstens ein
* Änderungsdokument; alles Übrige tritt über die {@code mit…}-Methoden hinzu.
*
* @param artikel nur diesen Artikel des Änderungsgesetzes anwenden; {@code null} = alle, deren
* Einleitung das Stammgesetz nennt.
* @param vollstaendig auch unveränderte Normen in die Synopse aufnehmen.
* @param stichtag die Fassung dieses Tages erzeugen: Befehle, die an ihm noch nicht in Kraft
* waren, bleiben unangewandt und werden gesondert ausgewiesen. {@code null} = alle Befehle
* anwenden (dann warnt die Synopse, wenn das Gesetz gestaffelt in Kraft tritt).
* @param nachfassung die amtliche Nachfassung, gegen die das Ergebnis normweise zu halten ist;
* {@code null} = kein Abgleich.
*/
public record Auftrag(
Quelle stammgesetz,
List<Quelle> aenderungsdokumente,
@Nullable String artikel,
boolean vollstaendig,
@Nullable LocalDate stichtag,
@Nullable Quelle nachfassung) {
public Auftrag {
aenderungsdokumente = List.copyOf(aenderungsdokumente);
}
public static Auftrag von(Quelle stammgesetz, List<Quelle> aenderungsdokumente) {
return new Auftrag(stammgesetz, aenderungsdokumente, null, false, null, null);
}
/** Bequemlichkeit für Befehlszeile und Tests; im Browser gibt es keine {@link Path}e. */
public static Auftrag von(Path stammgesetz, List<Path> aenderungsdokumente) throws IOException {
var quellen = new ArrayList<Quelle>();
for (var datei : aenderungsdokumente) {
quellen.add(Quelle.lies(datei));
}
return von(Quelle.lies(stammgesetz), quellen);
}
public Auftrag mitArtikel(@Nullable String neuerArtikel) {
return new Auftrag(
stammgesetz, aenderungsdokumente, neuerArtikel, vollstaendig, stichtag, nachfassung);
}
public Auftrag mitVollstaendig(boolean neuVollstaendig) {
return new Auftrag(
stammgesetz, aenderungsdokumente, artikel, neuVollstaendig, stichtag, nachfassung);
}
public Auftrag mitStichtag(@Nullable LocalDate neuerStichtag) {
return new Auftrag(
stammgesetz, aenderungsdokumente, artikel, vollstaendig, neuerStichtag, nachfassung);
}
public Auftrag mitNachfassung(@Nullable Quelle neueNachfassung) {
return new Auftrag(
stammgesetz, aenderungsdokumente, artikel, vollstaendig, stichtag, neueNachfassung);
}
}
/**
* @param neufassung die fortgeschriebene Fassung als kanonischer Klartext. Sie fällt ohnehin an
* und wird deshalb stets mitgegeben: An ihr hängen die Kette (Heft auf Heft) und jede Prüfung
* von außen.
* @param abgleich der normweise Vergleich mit der amtlichen Nachfassung; {@code null}, wenn der
* Auftrag keine nannte.
*/
public record Ergebnis(
String html,
long anzahlAngewandt,
long anzahlManuell,
int anzahlGeaenderteNormen,
int anzahlProtokollEintraege,
String neufassung,
@Nullable Nachfassungsabgleich abgleich) {}
/**
* Ein eingespeistes Änderungsdokument samt erkannter Art und aufbereitetem Text.
*
* @param eingearbeitet die Dokumente, die vor der Anwendung in {@code text} eingearbeitet wurden
* — bei einem Entwurf die Änderungsanträge, die ihn geändert haben. Sie gehen in die
* Quellenzeile ein, denn die gezeigte Fassung ist ohne sie nicht nachvollziehbar.
* @param fassung welche von mehreren Fassungen des Dokuments gilt, sofern es mehrere trägt — die
* Beschlussempfehlung stellt Entwurf und Ausschussfassung nebeneinander. Sonst {@code null}.
*/
record Quelldokument(
Quelle quelle,
DokumentKopf kopf,
String text,
List<String> eingearbeitet,
@Nullable String fassung) {
Quelldokument(Quelle quelle, DokumentKopf kopf, String text) {
this(quelle, kopf, text, List.of(), null);
}
Quelldokument(Quelle quelle, DokumentKopf kopf, String text, List<String> eingearbeitet) {
this(quelle, kopf, text, eingearbeitet, null);
}
String quellenAngabe(List<String> artikel) {
var sb = new StringBuilder(quelle.name());
sb.append(" [").append(kopf.anzeigeName());
if (fassung != null) {
sb.append(", ").append(fassung);
}
sb.append("]");
for (var zusatz : eingearbeitet) {
sb.append(" + ").append(zusatz);
}
return sb.append(" (Artikel ").append(String.join(", ", artikel)).append(")").toString();
}
}
public static Ergebnis erzeugeSynopse(Auftrag auftrag) throws Exception {
var baseFile = auftrag.stammgesetz();
var patches = auftrag.aenderungsdokumente();
var artikel = auftrag.artikel();
var stichtag = auftrag.stichtag();
var altesGesetz = ladeStammgesetz(baseFile);
var extraktor = new PatchTextExtraktor(superskriptModus(altesGesetz));
var parser = new AenderungsgesetzParser();
var warnungen = new ArrayList<String>();
var dokumente =
ordneNachAusfertigung(
wendeAntraegeAn(leseDokumente(patches, extraktor, warnungen), warnungen), warnungen);
var gesetz = altesGesetz;
var protokoll = new ArrayList<BefehlAnwender.AngewandteAenderung>();
var quellen = new ArrayList<String>();
boolean entwurfsfassung = false;
Inkrafttreten inkrafttreten = null;
for (var dokument : dokumente) {
var heft = new Fortschreibung(dokument.kopf().anzeigeName(), dokument.kopf().ausfertigung());
if (gesetz.traegt(heft)) {
warnungen.add(doppelanwendungsRuege(dokument, heft));
}
var parseErgebnis = parser.parse(dokument.text(), gesetz, artikel, entwurfsGrenzen(dokument));
if (parseErgebnis.befehle().isEmpty()) {
warnungen.add(
"In %s (%s) wurde kein auf %s anwendbarer Artikel gefunden."
.formatted(
dokument.quelle().name(), dokument.kopf().anzeigeName(), gesetz.jurabk()));
}
if (parseErgebnis.inkrafttreten() != null) {
inkrafttreten = parseErgebnis.inkrafttreten();
}
var geteilt = teileNachStichtag(parseErgebnis, stichtag, warnungen);
var anwendung = BefehlAnwender.anwenden(gesetz, geteilt.anzuwenden());
// Vermerkt wird nur, was auch gewirkt hat: Ein Heft, dessen Befehle sämtlich liegenblieben,
// hat den Wortlaut nicht fortgeschrieben und darf ihn deshalb nicht als fortgeschrieben
// ausweisen.
gesetz =
anwendung.anzahlAngewandt() > 0 && !gesetz.traegt(heft)
? anwendung.neu().mitFortschreibung(heft)
: anwendung.neu();
protokoll.addAll(anwendung.protokoll());
protokoll.addAll(geteilt.zurueckgestellt());
warnungen.addAll(parseErgebnis.warnungen());
entwurfsfassung |= dokument.kopf().art().istEntwurfsfassung();
quellen.add(dokument.quellenAngabe(parseErgebnis.artikel()));
}
if (stichtag == null && inkrafttreten != null && inkrafttreten.gestaffelt()) {
warnungen.add(staffelungsWarnung(inkrafttreten));
}
// Erst nach allen Heften: Ob die Inhaltsübersicht dem Text folgt, steht am Ende fest und nicht
// nach jedem einzelnen Schritt.
warnungen.addAll(InhaltsuebersichtsProbe.pruefe(altesGesetz, gesetz));
var gesamtErgebnis = new BefehlAnwender.AnwendungsErgebnis(gesetz, protokoll);
var synopse =
SynopseBuilder.baue(
altesGesetz,
gesamtErgebnis,
warnungen,
auftrag.vollstaendig(),
inkrafttreten,
stichtag);
// Der Abgleich setzt die fertige neue Fassung voraus und tritt deshalb erst hier hinzu.
Nachfassungsabgleich abgleich = null;
if (auftrag.nachfassung() != null) {
abgleich = Nachfassungsabgleich.vergleiche(ladeStammgesetz(auftrag.nachfassung()), gesetz);
synopse = synopse.mitAbgleich(abgleich);
}
var quellenZeile = baseFile.name() + " + " + String.join(" + ", quellen);
var html = HtmlRenderer.rendere(synopse, quellenZeile, entwurfsfassung);
return new Ergebnis(
html,
gesamtErgebnis.anzahlAngewandt(),
gesamtErgebnis.anzahlManuell(),
synopse.eintraege().size(),
protokoll.size(),
LandesRechtTextAusgeber.ausgeben(gesetz),
abgleich);
}
/**
* Die Rüge, wenn ein Heft ein zweites Mal auf denselben Wortlaut trifft.
*
* <p>Ein Änderungsbefehl ist keine Zustandsbeschreibung, sondern eine Anordnung: Wer zweimal
* anfügt, fügt zweimal an. Am Wortlaut allein ist das nicht zu erkennen — die Befehle greifen ein
* zweites Mal anstandslos —, wohl aber an dem, was die Fassung über sich selbst mitführt.
* Abgebrochen wird gleichwohl nicht: Es kann Gründe geben, dasselbe Heft erneut anzuwenden, und
* verworfen wird hier nichts (§ 1 Absatz 4 des Handbuchs). Gesagt wird es.
*/
private static String doppelanwendungsRuege(Quelldokument dokument, Fortschreibung heft) {
return ("Die Fassung trägt „%s“ bereits; %s wird damit ein zweites Mal angewandt. Ein Befehl"
+ " ist eine Anordnung und keine Zustandsbeschreibung: Was angefügt wird, wird dann"
+ " zweimal angefügt. Gemeint ist die Kette wohl mit einem anderen Heft.")
.formatted(heft.bezeichnung(), dokument.quelle().name());
}
/**
* Bringt die Hefte in die Reihenfolge ihrer Ausfertigung.
*
* <p>Die Kette wendet Heft auf Heft an; jedes setzt den Stand voraus, den das vorige hinterlassen
* hat. Die Reihenfolge der Aufrufargumente ist dafür kein Maßstab, das Ausfertigungsdatum schon.
* Umgestellt wird nur, wenn <em>jedes</em> Dokument ein Datum trägt — sonst fehlt der gemeinsame
* Maßstab, und geraten wird nicht. Und umgestellt wird nicht stillschweigend.
*/
private static List<Quelldokument> ordneNachAusfertigung(
List<Quelldokument> dokumente, List<String> warnungen) {
if (dokumente.size() < 2 || dokumente.stream().anyMatch(d -> d.kopf().ausfertigung() == null)) {
return dokumente;
}
var geordnet = new ArrayList<>(dokumente);
geordnet.sort(java.util.Comparator.comparing(d -> d.kopf().ausfertigung()));
if (geordnet.equals(dokumente)) {
return dokumente;
}
warnungen.add(
"Die Änderungsdokumente wurden nach ihrem Ausfertigungsdatum geordnet und in dieser Folge"
+ " angewandt: "
+ geordnet.stream()
.map(d -> d.quelle().name())
.collect(java.util.stream.Collectors.joining(", "))
+ ". Jedes Heft setzt den Stand voraus, den das vorige hinterlässt.");
return List.copyOf(geordnet);
}
/** Die Befehle eines Dokuments, geschieden nach dem, was am Stichtag schon galt. */
private record Geteilt(
List<eu.mulk.aendggner.aenderung.Aenderungsbefehl> anzuwenden,
List<BefehlAnwender.AngewandteAenderung> zurueckgestellt) {}
/**
* Scheidet die Befehle nach dem Stichtag. Ohne Stichtag bleibt alles beisammen — dann ist die
* Fassung die des vollständigen Inkrafttretens, worauf die Staffelungswarnung hinweist.
*
* <p>Eine Anordnung ohne bestimmbares Datum („am Tag nach der Verkündung“ — der Verkündungstag
* steht nicht im Gesetzestext) gilt als am Stichtag bereits wirksam; geraten wird nicht, aber es
* wird gesagt.
*/
private static Geteilt teileNachStichtag(
AenderungsgesetzParser.ParseErgebnis parseErgebnis,
@Nullable LocalDate stichtag,
List<String> warnungen) {
var befehle = parseErgebnis.befehle();
var inkrafttreten = parseErgebnis.inkrafttreten();
if (stichtag == null || befehle.isEmpty()) {
return new Geteilt(befehle, List.of());
}
if (inkrafttreten == null) {
warnungen.add(
"Es wurde ein Stichtag gewählt, das Änderungsdokument trägt aber keine lesbare"
+ " Inkrafttretens-Vorschrift; angewandt wurden deshalb alle Befehle.");
return new Geteilt(befehle, List.of());
}
var anzuwenden = new ArrayList<eu.mulk.aendggner.aenderung.Aenderungsbefehl>();
var zurueckgestellt = new ArrayList<BefehlAnwender.AngewandteAenderung>();
boolean unbestimmt = false;
for (var befehl : befehle) {
var regel = inkrafttreten.fuer(befehl.provenienz()).orElse(null);
if (regel == null || regel.datum() == null) {
unbestimmt |= regel != null;
anzuwenden.add(befehl);
continue;
}
if (regel.datum().isAfter(stichtag)) {
zurueckgestellt.add(
new BefehlAnwender.AngewandteAenderung(
befehl,
BefehlAnwender.Status.NICHT_IN_KRAFT,
"Tritt erst am "
+ DeutschesDatum.schreibe(regel.datum())
+ " in Kraft („"
+ regel.wortlaut()
+ "“).",
Set.of(),
Grund.NOCH_NICHT_IN_KRAFT));
continue;
}
anzuwenden.add(befehl);
}
if (unbestimmt) {
warnungen.add(
"Eine Inkrafttretens-Anordnung nennt kein bestimmtes Datum, sondern knüpft an die"
+ " Verkündung an; der Verkündungstag steht nicht im Gesetzestext. Die betroffenen"
+ " Befehle wurden als am Stichtag bereits geltend behandelt.");
}
return new Geteilt(List.copyOf(anzuwenden), List.copyOf(zurueckgestellt));
}
/**
* Wird ein Änderungsgesetz gestaffelt wirksam, so ist die auf einen Schlag gerechnete Fassung
* eine, die an keinem Tag gegolten hat. Das darf nicht unausgesprochen bleiben.
*/
private static String staffelungsWarnung(Inkrafttreten inkrafttreten) {
var sb = new StringBuilder("Das Änderungsgesetz tritt gestaffelt in Kraft");
inkrafttreten
.grundregel()
.ifPresent(r -> sb.append(" — im Grundsatz: „").append(r.wortlaut()).append("“"));
for (var sonder : inkrafttreten.sonderregeln()) {
sb.append("; abweichend: „").append(sonder.wortlaut()).append("“");
}
return sb.append(
". Die gezeigte Fassung wendet alle Befehle an und gilt daher erst, wenn das Gesetz"
+ " vollständig in Kraft getreten ist; die Fassung eines bestimmten Tages ergibt"
+ " sich nur mit einem Stichtag.")
.toString();
}
/**
* Liest die Änderungsdokumente ein, bestimmt ihre Art und sortiert die aus, aus denen keine
* Synopse zu gewinnen ist. Verworfen wird nichts stillschweigend: Jedes ausgesonderte Dokument
* hinterlässt eine Warnung, die in der Synopse erscheint.
*/
private static List<Quelldokument> leseDokumente(
List<Quelle> patches, PatchTextExtraktor extraktor, List<String> warnungen) throws Exception {
var dokumente = new ArrayList<Quelldokument>();
for (var datei : patches) {
var rohText = extraktor.extrahiere(datei);
// Die Erkennung arbeitet auf dem Rohtext: Der Bereiniger entfernt genau die
// Drucksachenköpfe, aus denen Art und Nummer hervorgehen.
var kopf = DokumentErkenner.erkenne(rohText);
log.infof("Datei %s erkannt als %s.", datei.name(), kopf.anzeigeName());
if (kopf.art() == DokumentArt.OHNE_BEFEHLE) {
warnungen.add(
"%s ist ein %s und enthält keine Änderungsbefehle; die Datei wurde übergangen."
.formatted(datei.name(), kopf.art().anzeigeName()));
continue;
}
if (kopf.art() == DokumentArt.BESCHLUSSEMPFEHLUNG) {
// Die maßgebliche Fassung steht nicht im Fließtext, sondern in der zweispaltigen
// Zusammenstellung: links der Entwurf, rechts die Beschlüsse des Ausschusses.
var fassung = extraktor.leseZusammenstellung(datei);
warnungen.addAll(fassung.warnungen());
if (fassung.text() == null) {
// Eine halb aufgelöste Fassung auszugeben wäre schlimmer als keine.
warnungen.add(
("%s ist eine Beschlussempfehlung, deren Zusammenstellung sich nicht auflösen ließ;"
+ " die Datei wurde übergangen. Für eine Synopse eignet sich der zugrunde"
+ " liegende Gesetzentwurf%s.")
.formatted(
datei.name(),
kopf.bezugsDrucksachen().isEmpty()
? ""
: " (Drs. " + kopf.bezugsDrucksachen().get(0) + ")"));
continue;
}
dokumente.add(
new Quelldokument(
datei,
kopf,
TextBereiniger.bereinige(fassung.text()),
List.of(),
"Ausschussfassung"));
continue;
}
dokumente.add(new Quelldokument(datei, kopf, TextBereiniger.bereinige(rohText)));
}
return dokumente;
}
/**
* Wendet jeden Änderungsantrag auf den Entwurf an, den er ändern will, und nimmt ihn aus der
* Liste: Was danach bleibt, sind lauter Dokumente, die unmittelbar das Stammgesetz ändern.
*
* <p>Zugeordnet wird über die Drucksachennummer, die der Antrag selbst nennt („(Drs. 19/9707)“).
* Nur wo die fehlt, entscheidet die Reihenfolge der Argumente — der zuletzt genannte Entwurf
* davor. Findet sich gar kein Entwurf, bleibt der Antrag unangewandt und wird gemeldet; ihn
* ersatzweise auf das Stammgesetz loszulassen wäre falsch, denn seine Stellenangaben zielen auf
* die Drucksache.
*/
private static List<Quelldokument> wendeAntraegeAn(
List<Quelldokument> dokumente, List<String> warnungen) {
if (dokumente.stream().noneMatch(d -> d.kopf().art() == DokumentArt.AENDERUNGSANTRAG)) {
return dokumente;
}
var ergebnis = new ArrayList<>(dokumente);
for (var antrag : dokumente) {
if (antrag.kopf().art() != DokumentArt.AENDERUNGSANTRAG) {
continue;
}
ergebnis.remove(antrag);
int zielIndex = findeEntwurf(ergebnis, antrag, dokumente.indexOf(antrag));
if (zielIndex < 0) {
warnungen.add(
("%s ist ein Änderungsantrag zu %s; der zugehörige Gesetzentwurf wurde nicht"
+ " mitgegeben, der Antrag blieb daher unberücksichtigt.")
.formatted(
antrag.quelle().name(),
antrag.kopf().bezugsDrucksachen().isEmpty()
? "einer Drucksache"
: "Drs. " + String.join(", ", antrag.kopf().bezugsDrucksachen())));
continue;
}
var ziel = ergebnis.get(zielIndex);
var parseErgebnis = AenderungsantragParser.parse(antrag.text());
warnungen.addAll(parseErgebnis.warnungen());
var patch = EntwurfsPatcher.wendeAn(ziel.text(), parseErgebnis.befehle());
warnungen.addAll(patch.warnungen());
log.infof(
"%s: %d von %d Antragsbefehlen auf %s angewandt.",
antrag.quelle().name(),
patch.angewandt(),
parseErgebnis.befehle().size(),
ziel.quelle().name());
var eingearbeitet = new ArrayList<>(ziel.eingearbeitet());
eingearbeitet.add(antrag.quelle().name() + " [" + antrag.kopf().anzeigeName() + "]");
ergebnis.set(
zielIndex,
new Quelldokument(
ziel.quelle(),
ziel.kopf(),
patch.text(),
List.copyOf(eingearbeitet),
ziel.fassung()));
}
return ergebnis;
}
/** Der Index des Entwurfs, den {@code antrag} ändert; {@code -1}, wenn keiner dabei ist. */
private static int findeEntwurf(
List<Quelldokument> dokumente, Quelldokument antrag, int antragsPosition) {
for (int i = 0; i < dokumente.size(); i++) {
var kopf = dokumente.get(i).kopf();
if (kopf.eigeneDrucksache() != null
&& antrag.kopf().bezugsDrucksachen().contains(kopf.eigeneDrucksache())) {
return i;
}
}
int letzter = -1;
for (int i = 0; i < dokumente.size() && i < antragsPosition; i++) {
if (dokumente.get(i).kopf().art() == DokumentArt.GESETZENTWURF) {
letzter = i;
}
}
return letzter;
}
/**
* Entwürfe tragen hinter dem Regelungstext einen Begründungsteil, dessen Freitext keine Befehle
* enthält und den letzten Artikel nicht verunreinigen darf. Verkündete Gesetze haben ihn nicht —
* dort bliebe die Suche nach Begründungsmarken folgenlos, aber sie unterbleibt trotzdem, damit
* ein Gesetzblatt nicht an einem gleichlautenden Wort abbricht.
*/
private static boolean entwurfsGrenzen(Quelldokument dokument) {
return dokument.kopf().art().istEntwurfsfassung();
}
/**
* Der Text der Änderungsdokumente, so wie das Erzeugnis ihn liest — der Notausgang des § 6 Absatz
* 2 des Handbuchs.
*
* <p>Bleibt die PDF-Aufbereitung im Einzelfall fehlerhaft, so ist nicht das Ergebnis zu
* beargwöhnen, sondern der Text nachzusehen, von Hand zu berichtigen und als Klartextdatei wieder
* einzuspeisen. Dass dieser Weg beiden Fassungen offensteht, ist kein Zierat: Wer im Browser
* arbeitet, hat sonst keine Möglichkeit, einem unerklärlichen Rest auf den Grund zu gehen.
*
* <p>Das Stammgesetz wird auch hier gebraucht, denn seine Schreibweise bestimmt den
* Superskriptmodus der Textgewinnung ({@link #superskriptModus}).
*
* @param roh die Bereinigung übergehen; nur zur Fehlersuche an der Textgewinnung selbst.
*/
public static String extrahiereText(Quelle baseFile, List<Quelle> patches, boolean roh)
throws Exception {
var extraktor = new PatchTextExtraktor(superskriptModus(ladeStammgesetz(baseFile)));
var text = new StringBuilder();
for (var patch : patches) {
var roher = extraktor.extrahiere(patch);
text.append(roh ? roher : TextBereiniger.bereinige(roher)).append('\n');
}
return text.toString();
}
/** Bequemlichkeit für Befehlszeile und Tests; im Browser gibt es keine {@link Path}e. */
static Gesetz ladeStammgesetz(Path baseFile) throws Exception {
return ladeStammgesetz(Quelle.lies(baseFile));
}
/**
* Gii-XML → {@link GiiXmlLoader}; PDF/Klartext (Landesrecht) → {@link LandesRechtLoader}.
*
* <p>Vorweg wird ausgepackt: gesetze-im-internet.de gibt das Norm-XML nur als {@code xml.zip}
* aus, und das soll unentpackt taugen (siehe {@link ZipAuspacker}). Die Änderungsdokumente
* bleiben davon unberührt — Gesetzblätter und Drucksachen kommen nirgends als Archiv.
*/
static Gesetz ladeStammgesetz(Quelle baseFile) throws Exception {
var quelle = ZipAuspacker.auspacken(baseFile);
return DateiTyp.erkenne(quelle.inhalt()) == DateiTyp.XML
? new GiiXmlLoader().load(quelle)
: new LandesRechtLoader().load(quelle);
}
/**
* Der Superskriptmodus folgt der Schreibweise des Stammgesetzes: Trägt es amtliche Satznummern
* (bayerisches Landesrecht, Niedersachsen u.a.), werden auch die Änderungsgesetze mit
* Superskript-Erhalt extrahiert, damit Zitate und Stammtext dieselbe Schreibweise tragen; sonst
* (Bundesrecht, Länder ohne amtliche Satzzählung) sind hochgestellte Ziffern bloße Fußnotenmarker
* und werden verworfen.
*/
static SuperskriptModus superskriptModus(Gesetz gesetz) {
for (var norm : gesetz.normen()) {
for (var absatz : norm.absaetze()) {
if (Superskript.traegtSatznummern(absatz.text())) {
return SuperskriptModus.BEHALTEN;
}
}
}
return SuperskriptModus.ENTFERNEN;
}
}