diff --git a/README.md b/README.md index 0a2bb8a..15a450a 100644 --- a/README.md +++ b/README.md @@ -98,4 +98,12 @@ Options: Die Anwendung kann auch die Conditions in der CSV-Datei mit der Onkostar-Datenbank direkt vergleichen. Hierzu kann der Befehl `compare` genutzt werden. Dieser verwendet alle Optionen für die Datenbank und die -Option `--file` für die CSV-Datei und gibt eine Übersicht auf der Konsole aus. \ No newline at end of file +Option `--file` für die CSV-Datei und gibt eine Übersicht auf der Konsole aus. + +## Vergleich der XML-basierten LKR-Export-Protokolldatei mit der Datenbank + +Mithilfe dieser Anwendung kann auch der aktuelle Inhalt der Datenbank gegen die LKR-Export-Protokolldatei für einen +Export verglichen werden. + +Der Befehl `check-export` kann zusammen mit der Angabe der Protokolldatei (`--file`) und der Angabe des +Exports (`--export-package=...`) und den Optionen für den Datenbankzugriff ausgeführt werden. \ No newline at end of file diff --git a/src/cli.rs b/src/cli.rs index 34b9783..98c0d1e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -148,6 +148,32 @@ pub enum SubCommand { )] include_histo_zyto: bool, }, + #[command(about = "Abgleich zwischen LKR-Export-Protokoll und Onkostar-Datenbank")] + CheckExport { + #[arg(short = 'D', long, help = "Datenbank-Name", default_value = "onkostar")] + database: String, + #[arg( + short = 'h', + long, + help = "Datenbank-Host", + default_value = "localhost" + )] + host: String, + #[arg(short = 'P', long, help = "Datenbank-Host", default_value = "3306")] + port: u16, + #[arg( + short = 'p', + long, + help = "Passwort. Wenn nicht angegeben, wird danach gefragt" + )] + password: Option, + #[arg(short = 'u', long, help = "Benutzername")] + user: String, + #[arg(short, long, help = "LKR-Export-Protokoll-Datei")] + file: PathBuf, + #[arg(long, help = "Exportpaketnummer", default_value = "0")] + export_package: u16, + }, } fn value_is_date(value: &str) -> Result { diff --git a/src/database.rs b/src/database.rs index 6ab88e0..595d55d 100644 --- a/src/database.rs +++ b/src/database.rs @@ -24,7 +24,7 @@ use mysql::prelude::Queryable; use mysql::{params, Pool}; use crate::common::{ExportData, Icd10GroupSize}; -use crate::resources::{EXPORT_QUERY, SQL_QUERY}; +use crate::resources::{EXPORTED_TO_LKR, EXPORT_QUERY, SQL_QUERY}; pub struct DatabaseSource(String); @@ -111,4 +111,30 @@ impl DatabaseSource { Err(()) } + + pub fn exported(&self, export_id: u16) -> Result, ()> { + match Pool::new(self.0.as_str()) { + Ok(pool) => { + if let Ok(mut connection) = pool.try_get_conn(Duration::from_secs(3)) { + return match connection.exec_map( + EXPORTED_TO_LKR, + params! { + "export_id" => export_id, + }, + |(id, xml_data)| (id, xml_data), + ) { + Ok(result) => Ok(result), + Err(_) => { + return Err(()); + } + }; + } + } + Err(_) => { + return Err(()); + } + } + + Err(()) + } } diff --git a/src/lkrexport.rs b/src/lkrexport.rs new file mode 100644 index 0000000..ee3e25c --- /dev/null +++ b/src/lkrexport.rs @@ -0,0 +1,224 @@ +/* + * This file is part of bzkf-rwdp-check + * + * Copyright (C) 2024 Comprehensive Cancer Center Mainfranken and contributors. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +use std::fs; +use std::path::Path; +use std::str::FromStr; + +use itertools::Itertools; +use regex::Regex; + +pub struct LkrExportProtocolFile { + pub patients: Vec, +} + +impl LkrExportProtocolFile { + pub fn parse_file(path: &Path) -> Result { + let xml_file_content = fs::read_to_string(path).map_err(|_| ())?; + Self::parse(&xml_file_content) + } + + pub fn parse(content: &str) -> Result { + let re = Regex::new(r"(?s)(?(.*?))").unwrap(); + + if re.is_match(content) { + let patients = re + .find_iter(content) + .map(|m| Patient { + raw_value: m.as_str().to_string(), + }) + .collect_vec(); + return Ok(LkrExportProtocolFile { patients }); + } + + Err(()) + } + + pub fn meldungen(&self) -> Vec { + self.patients + .iter() + .flat_map(|patient| patient.meldungen()) + .collect_vec() + } +} + +pub struct Patient { + pub raw_value: String, +} + +impl Patient { + pub fn meldungen(&self) -> Vec { + let re = Regex::new(r"(?s)(?)").unwrap(); + + if re.is_match(&self.raw_value) { + return re + .find_iter(&self.raw_value) + .map(|m| Meldung { + raw_value: m.as_str().to_string(), + }) + .collect_vec(); + } + vec![] + } +} + +pub struct Meldung { + pub raw_value: String, +} + +impl FromStr for Meldung { + type Err = (); + + fn from_str(s: &str) -> Result { + Ok(Meldung { + raw_value: s.to_string(), + }) + } +} + +#[allow(unused)] +impl Meldung { + pub fn id(&self) -> Option { + let re = Regex::new(r#"Meldung_ID="(?(.*?))""#).unwrap(); + + if re.is_match(&self.raw_value) { + let caps = re.captures(&self.raw_value).unwrap(); + return Some(caps["meldung_id"].to_string()); + } + + None + } + + pub fn icd10(&self) -> Option { + let re = Regex::new(r"(?s)(?(.*?))") + .unwrap(); + + if re.is_match(&self.raw_value) { + let caps = re.captures(&self.raw_value).unwrap(); + return Some(caps["icd10"].to_string()); + } + + None + } + + pub fn database_id(&self) -> Option { + match self.id() { + Some(id) => to_database_id(&id), + _ => None, + } + } + + pub fn no_linebreak(&self) -> String { + let re = Regex::new(r"\n\s*").unwrap(); + re.replace_all(&self.raw_value, "").trim().to_string() + } +} + +pub fn to_database_id(id: &str) -> Option { + let re1 = Regex::new(r"^(?[0-9A-F]+)").unwrap(); + let re2 = Regex::new(r"(?[0-9]+)$").unwrap(); + + if re1.is_match(id) { + match re1.find(id).map(|m| m.as_str().to_string()) { + Some(val) => match u64::from_str_radix(&val, 16) { + Ok(val) => Some(val.to_string()), + _ => None, + }, + _ => None, + } + } else if re2.is_match(id) { + re2.find(id).map(|m| m.as_str().to_string()) + } else { + None + } +} + +#[cfg(test)] +mod tests { + use crate::lkrexport::{LkrExportProtocolFile, Meldung}; + + #[test] + fn should_read_xml_file_content() { + let actual = LkrExportProtocolFile::parse(include_str!("../testdaten/testdaten_1.xml")); + + assert!(actual.is_ok()); + assert_eq!(actual.unwrap().patients.len(), 2); + } + + #[test] + fn should_get_meldungen() { + let actual = LkrExportProtocolFile::parse(include_str!("../testdaten/testdaten_1.xml")); + + assert!(actual.is_ok()); + + let patients = actual.unwrap().patients; + + assert_eq!(patients[0].meldungen().len(), 1); + assert_eq!(patients[1].meldungen().len(), 1); + } + + #[test] + fn should_get_meldung_database_id() { + let actual = LkrExportProtocolFile::parse(include_str!("../testdaten/testdaten_1.xml")); + + assert!(actual.is_ok()); + + let patients = actual.unwrap().patients; + + assert_eq!( + patients[0].meldungen()[0].database_id(), + Some("1727528".to_string()) + ); + assert_eq!( + patients[1].meldungen()[0].database_id(), + Some("1727824".to_string()) + ); + } + + #[test] + fn should_get_meldung_icd10() { + let actual = LkrExportProtocolFile::parse(include_str!("../testdaten/testdaten_1.xml")); + + assert!(actual.is_ok()); + + let patients = actual.unwrap().patients; + + assert_eq!( + patients[0].meldungen()[0].icd10(), + Some("C17.1".to_string()) + ); + assert_eq!( + patients[1].meldungen()[0].icd10(), + Some("C17.2".to_string()) + ); + } + + #[test] + fn should_get_meldung_with_trimmed_margin() { + let meldung = Meldung { + raw_value: " \n TestInhalt 3\n\n".into(), + }; + + assert_eq!( + meldung.no_linebreak(), + "TestInhalt 3".to_string() + ); + } +} diff --git a/src/main.rs b/src/main.rs index 50d368f..bb8ab9a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +use std::collections::HashMap; use std::error::Error; use clap::Parser; @@ -28,10 +29,12 @@ use itertools::Itertools; use crate::cli::{Cli, SubCommand}; use crate::common::{Check, DiffRecord, Icd10GroupSize}; use crate::database::DatabaseSource; +use crate::lkrexport::{to_database_id, LkrExportProtocolFile, Meldung}; mod cli; mod common; mod database; +mod lkrexport; mod opal; mod resources; @@ -421,6 +424,179 @@ fn main() -> Result<(), Box> { )); }); } + SubCommand::CheckExport { + database, + host, + password, + port, + user, + file, + export_package, + } => { + let password = request_password_if_none(password); + + let _ = term.write_line( + &style(format!( + "Warte auf Daten für den LKR-Export '{}'...", + export_package + )) + .blue() + .to_string(), + ); + + let db = DatabaseSource::new(&database, &host, &password, port, &user); + + let db_entries = db + .exported(export_package) + .map_err(|_e| "Fehler bei Zugriff auf die Datenbank")?; + + let db_meldungen = db_entries + .iter() + .map(|entry| LkrExportProtocolFile::parse(&entry.1)) + .filter(|entry| entry.is_ok()) + .flat_map(|entry| entry.unwrap().meldungen()) + .filter(|meldung| meldung.id().is_some()) + .map(|meldung| (meldung.id().unwrap(), meldung)) + .collect::>(); + + let xml_meldungen = LkrExportProtocolFile::parse_file(file.as_path()) + .map_err(|_e| "Fehler bei Zugriff auf die Protokolldatei")? + .meldungen() + .into_iter() + .filter(|meldung| meldung.id().is_some()) + .map(|meldung| (meldung.id().unwrap(), meldung)) + .collect::>(); + + let missing_xml_ids = db_meldungen + .keys() + .filter(|&key| !xml_meldungen.contains_key(key)) + .collect_vec(); + + let _ = term.clear_last_lines(1); + + let _ = term.write_line( + &style(format!( + "{} Datenbankeinträge mit {} Meldungen abgerufen", + db_entries.len(), + db_meldungen.len() + )) + .green() + .to_string(), + ); + + if db_meldungen.len() != xml_meldungen.len() { + let _ = term.write_line( + &style("\nNicht übereinstimmende Anzahl an Meldungen:") + .yellow() + .to_string(), + ); + let _ = term.write_line(&format!( + "Datenbank: {:>10}\nProtokolldatei: {:>10}", + db_meldungen.len(), + xml_meldungen.len() + )); + + let missing_db_ids = xml_meldungen + .keys() + .filter(|&key| !db_meldungen.contains_key(key)) + .collect_vec(); + + if !missing_db_ids.is_empty() { + let _ = term.write_line( + &style("\nIn der Datenbank fehlende Meldungen:") + .yellow() + .to_string(), + ); + + missing_db_ids.iter().sorted().for_each(|&item| { + let _ = term.write_line(&format!( + "{} ({})", + item, + to_database_id(item).unwrap_or("?".into()) + )); + }); + } + + if !missing_xml_ids.is_empty() { + let _ = term.write_line( + &style("\nIn der Protokolldatei fehlende Meldungen:") + .yellow() + .to_string(), + ); + + missing_xml_ids.iter().sorted().for_each(|&item| { + let _ = term.write_line(&format!( + "{} ({})", + item, + to_database_id(item).unwrap_or("?".into()) + )); + }); + } + } + + let multiple_meldung_entries = db_entries + .iter() + .map(|(lkr_meldung, meldung)| (lkr_meldung, LkrExportProtocolFile::parse(meldung))) + .filter_map(|(lkr_meldung, meldung)| { + if meldung.unwrap().meldungen().len() > 1 { + Some(lkr_meldung) + } else { + None + } + }) + .sorted() + .collect_vec(); + + if !multiple_meldung_entries.is_empty() { + let _ = term.write_line( + &style("\nFolgende Einträge in `lkr_meldung_export` haben mehrere Meldungsinhalte in `xml_daten`:") + .yellow() + .to_string(), + ); + + multiple_meldung_entries.iter().for_each(|item| { + let _ = term.write_line(&item.to_string()); + }); + } + + let different_content = db_meldungen + .iter() + .filter(|(id, _)| !missing_xml_ids.contains(id)) + .filter(|(id, meldung)| { + xml_meldungen + .get(&id.to_string()) + .unwrap_or(&Meldung { + raw_value: String::new(), + }) + .no_linebreak() + != meldung.no_linebreak() + }) + .map(|(_, meldung)| meldung.id().unwrap_or("?".into())) + .collect_vec(); + + if !different_content.is_empty() { + let _ = term.write_line( + &style(&format!( + "\nFolgende {} Meldungen unterscheiden sich in der Datenbank und der Protokolldatei:", + different_content.len() + )) + .yellow() + .to_string(), + ); + + let _ = term.write_line( + "Dies kann auch aufgrund der verwendeten XML-Encodierung auftreten und bedeutet nicht immer eine inhaltliche Abweichung." + ); + + different_content.iter().sorted().for_each(|id| { + let _ = term.write_line(&format!( + "{} ({})", + id, + to_database_id(id).unwrap_or("?".into()) + )); + }); + } + } } Ok(()) diff --git a/src/resources/exported-to-lkr.sql b/src/resources/exported-to-lkr.sql new file mode 100644 index 0000000..ace8d03 --- /dev/null +++ b/src/resources/exported-to-lkr.sql @@ -0,0 +1,26 @@ +/* + * This file is part of bzkf-rwdp-check + * + * Copyright (C) 2024 Comprehensive Cancer Center Mainfranken and contributors. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +SELECT + CONVERT(id,char) AS id, + xml_daten +FROM lkr_meldung_export +WHERE typ <> -1 + AND (lkr_export = :export_id OR (0 = :export_id AND lkr_export IN (SELECT MAX(lkr_export) FROM lkr_meldung_export))); \ No newline at end of file diff --git a/src/resources/mod.rs b/src/resources/mod.rs index 78c2878..5df086a 100644 --- a/src/resources/mod.rs +++ b/src/resources/mod.rs @@ -21,3 +21,5 @@ pub const SQL_QUERY: &str = include_str!("query.sql"); pub const EXPORT_QUERY: &str = include_str!("export.sql"); + +pub const EXPORTED_TO_LKR: &str = include_str!("exported-to-lkr.sql"); diff --git a/testdaten/testdaten_1.xml b/testdaten/testdaten_1.xml new file mode 100644 index 0000000..3b01820 --- /dev/null +++ b/testdaten/testdaten_1.xml @@ -0,0 +1,89 @@ + + + + TEST + Musterstraße 1, 012345 Musterhausen + + + + + E123456789 + 123456789 + Tester + + Patrick + Tester + M + 01.01.1980 + + + Testweg + 1 + DE + 01234 + Musterhausen + + + + + + 11.06.2024 + I + statusaenderung + + C17.1 + 10 2015 GM + 10.06.2024 + T + + + + 11.06.2024 + praeth + + + + + + + + E123456789 + 123456789 + Tester + + Patricia + Tester + W + 01.01.1980 + + + Testweg + 1 + DE + 01234 + Musterhausen + + + + + + 11.06.2024 + I + statusaenderung + + C17.2 + 10 2015 GM + 01.01.2024 + T + + + + 10.01.2024 + praeth + + + + + + +