| package eu.mulk.aendggner; |
| |
| import eu.mulk.aendggner.aenderung.parse.AenderungsgesetzParser; |
| 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.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.synopse.HtmlRenderer; |
| import eu.mulk.aendggner.synopse.SynopseBuilder; |
| import java.io.IOException; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.concurrent.Callable; |
| import java.util.logging.LogManager; |
| import org.jboss.logging.Logger; |
| import picocli.CommandLine; |
| import picocli.CommandLine.Command; |
| import picocli.CommandLine.Option; |
| import picocli.CommandLine.Parameters; |
| |
| @Command( |
| name = "ÄndGgner", |
| mixinStandardHelpOptions = true, |
| version = "ÄndGgner 0.1", |
| description = "Displays German amendment acts in a user-friendly, consolidated way.") |
| public class AendGgner implements Callable<Integer> { |
| |
| private static final Logger log = Logger.getLogger(AendGgner.class); |
| |
| @Parameters( |
| index = "0", |
| description = |
| "The base law: gii-norm XML from gesetze-im-internet.de, or — for Bavarian state law —" |
| + " the consolidated version from gesetze-bayern.de (PDF or canonical plain text).") |
| private Path baseFile; |
| |
| @Parameters( |
| index = "1..*", |
| arity = "*", |
| description = "The amendment act(s) to apply (BGBl PDF or plain text).") |
| private List<Path> patches; |
| |
| @Option( |
| names = {"-o", "--output"}, |
| paramLabel = "<file>", |
| description = |
| "Output file for the HTML synopsis (default: ${DEFAULT-VALUE}; \"-\" = stdout).", |
| defaultValue = "synopse.html") |
| private String output; |
| |
| @Option(names = "--vollstaendig", description = "Include unchanged provisions in the synopsis.") |
| private boolean vollstaendig; |
| |
| @Option( |
| names = "--format", |
| paramLabel = "<format>", |
| description = "Output format (currently only: html).", |
| defaultValue = "html") |
| private String format; |
| |
| @Option( |
| names = "--dump-gesetz", |
| hidden = true, |
| description = "Debug: dump the parsed base law structure and exit.") |
| private boolean dumpGesetz; |
| |
| @Option( |
| names = "--extract-only", |
| description = |
| "Print the cleaned linear text of the amendment act and exit. " |
| + "Useful for inspecting and hand-correcting PDF extraction; " |
| + "the corrected text can be fed back in as a plain-text patch file.") |
| private boolean extractOnly; |
| |
| @Option( |
| names = "--raw", |
| hidden = true, |
| description = "Debug: with --extract-only, skip text cleanup.") |
| private boolean raw; |
| |
| @Option( |
| names = "--dump-befehle", |
| hidden = true, |
| description = "Debug: dump the parsed amendment commands and exit.") |
| private boolean dumpBefehle; |
| |
| @Option( |
| names = "--artikel", |
| paramLabel = "<n>", |
| description = |
| "Only apply this article of the amendment act " |
| + "(default: all articles whose introduction names the base law).") |
| private String artikel; |
| |
| public static void main(String... args) { |
| int exitCode = new CommandLine(new AendGgner()).execute(args); |
| System.exit(exitCode); |
| } |
| |
| @Override |
| public final Integer call() throws Exception { |
| setupLogging(); |
| |
| log.debugf("Logging configured."); |
| |
| if (dumpGesetz) { |
| var gesetz = ladeStammgesetz(); |
| System.out.printf( |
| "%s — %s (%d Normen)%n", gesetz.jurabk(), gesetz.langue(), gesetz.normen().size()); |
| for (var norm : gesetz.normen()) { |
| System.out.printf( |
| " %s%s — %d Absätze%s%n", |
| norm.enbez(), |
| norm.titel() == null ? "" : " (" + norm.titel() + ")", |
| norm.absaetze().size(), |
| norm.weggefallen() ? " [weggefallen]" : ""); |
| } |
| return 0; |
| } |
| |
| if (extractOnly) { |
| var gesetz = ladeStammgesetz(); |
| var extraktor = new PatchTextExtraktor(superskriptModus(gesetz)); |
| for (var file : patches) { |
| var text = extraktor.extrahiere(file); |
| System.out.println(raw ? text : TextBereiniger.bereinige(text)); |
| } |
| return 0; |
| } |
| |
| if (dumpBefehle) { |
| var gesetz = ladeStammgesetz(); |
| var extraktor = new PatchTextExtraktor(superskriptModus(gesetz)); |
| var parser = new AenderungsgesetzParser(); |
| for (var file : patches) { |
| var text = TextBereiniger.bereinige(extraktor.extrahiere(file)); |
| var ergebnis = parser.parse(text, gesetz, artikel); |
| System.out.printf( |
| "%s: %d Befehle aus Artikel %s%n", |
| file.getFileName(), ergebnis.befehle().size(), ergebnis.artikel()); |
| for (var warnung : ergebnis.warnungen()) { |
| System.err.println("WARNUNG: " + warnung); |
| } |
| var anwendung = BefehlAnwender.anwenden(gesetz, ergebnis.befehle()); |
| for (var eintrag : anwendung.protokoll()) { |
| var befehl = eintrag.befehl(); |
| System.out.printf( |
| " [%s] %s @ %s → %s%s%n %s%n", |
| befehl.getClass().getSimpleName(), |
| befehl.provenienz().anzeigeText(), |
| befehl.stelle().anzeigeText(), |
| eintrag.status(), |
| eintrag.begruendung().isEmpty() ? "" : " (" + eintrag.begruendung() + ")", |
| kuerze(befehl.provenienz().originalText())); |
| } |
| System.out.printf( |
| "Angewandt: %d, manuell prüfen: %d%n", |
| anwendung.anzahlAngewandt(), anwendung.anzahlManuell()); |
| gesetz = anwendung.neu(); |
| } |
| return 0; |
| } |
| |
| if (patches == null || patches.isEmpty()) { |
| System.err.println("Fehler: mindestens ein Änderungsgesetz muss angegeben werden."); |
| return 1; |
| } |
| if (!format.equals("html")) { |
| System.err.println("Fehler: unbekanntes Ausgabeformat „" + format + "“ (unterstützt: html)."); |
| return 1; |
| } |
| |
| // Pipeline: Stammgesetz laden → Befehle parsen → anwenden → Synopse rendern. |
| var altesGesetz = ladeStammgesetz(); |
| var extraktor = new PatchTextExtraktor(superskriptModus(altesGesetz)); |
| var parser = new AenderungsgesetzParser(); |
| |
| var gesetz = altesGesetz; |
| var protokoll = new ArrayList<BefehlAnwender.AngewandteAenderung>(); |
| var warnungen = new ArrayList<String>(); |
| var quellen = new ArrayList<String>(); |
| |
| for (var file : patches) { |
| var text = TextBereiniger.bereinige(extraktor.extrahiere(file)); |
| var parseErgebnis = parser.parse(text, gesetz, artikel); |
| if (parseErgebnis.befehle().isEmpty()) { |
| System.err.printf( |
| "Warnung: in %s wurde kein auf %s anwendbarer Artikel gefunden.%n", |
| file, gesetz.jurabk()); |
| } |
| var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle()); |
| gesetz = anwendung.neu(); |
| protokoll.addAll(anwendung.protokoll()); |
| warnungen.addAll(parseErgebnis.warnungen()); |
| quellen.add( |
| file.getFileName() + " (Artikel " + String.join(", ", parseErgebnis.artikel()) + ")"); |
| } |
| |
| var gesamtErgebnis = new BefehlAnwender.AnwendungsErgebnis(gesetz, protokoll); |
| var synopse = SynopseBuilder.baue(altesGesetz, gesamtErgebnis, warnungen, vollstaendig); |
| var quelle = baseFile.getFileName() + " + " + String.join(" + ", quellen); |
| var html = HtmlRenderer.rendere(synopse, quelle); |
| |
| if (output.equals("-")) { |
| System.out.println(html); |
| } else { |
| Files.writeString(Path.of(output), html, StandardCharsets.UTF_8); |
| log.infof("Synopse nach %s geschrieben.", output); |
| } |
| |
| System.err.printf( |
| "%d Befehle angewandt, %d manuell zu prüfen, %d geänderte Normen.%n", |
| gesamtErgebnis.anzahlAngewandt(), |
| gesamtErgebnis.anzahlManuell(), |
| synopse.eintraege().size()); |
| |
| return gesamtErgebnis.anzahlAngewandt() == 0 && !protokoll.isEmpty() ? 2 : 0; |
| } |
| |
| /** Gii-XML → {@link GiiXmlLoader}; PDF/Klartext (Landesrecht) → {@link LandesRechtLoader}. */ |
| private Gesetz ladeStammgesetz() throws Exception { |
| return istGiiXml() ? new GiiXmlLoader().load(baseFile) : new LandesRechtLoader().load(baseFile); |
| } |
| |
| /** |
| * 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. |
| */ |
| private 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; |
| } |
| |
| private boolean istGiiXml() throws IOException { |
| var mimeType = new org.apache.tika.Tika().detect(baseFile); |
| return mimeType.equals("application/xml") || mimeType.equals("text/xml"); |
| } |
| |
| private static String kuerze(String text) { |
| var einzeilig = text.replaceAll("\\s+", " "); |
| return einzeilig.length() <= 160 ? einzeilig : einzeilig.substring(0, 157) + "…"; |
| } |
| |
| private static void setupLogging() throws IOException { |
| try (var loggingProperties = |
| AendGgner.class.getResourceAsStream("/eu/mulk/aendggner/logging.properties")) { |
| LogManager.getLogManager().readConfiguration(loggingProperties); |
| } |
| } |
| } |