From 09843349d9596429d77cb62e8edd4917247ea4d7 Mon Sep 17 00:00:00 2001 From: Paul-Christian Volkmer Date: Thu, 21 Aug 2025 10:45:36 +0200 Subject: [PATCH] feat: add MSI mapping as far as possible --- README.md | 7 +- .../pcvolkmer/onco/datamapper/ResultSet.java | 22 +++ .../datacatalogues/DataCatalogueFactory.java | 2 + .../MolekulargenMsiCatalogue.java | 46 +++++++ .../KpaMolekulargenetikMsiDataMapper.java | 113 ++++++++++++++++ ... => KpaMolekulargenetikNgsDataMapper.java} | 4 +- .../onco/datamapper/mapper/MtbDataMapper.java | 15 ++- .../onco/datamapper/ResultSetTest.java | 7 + .../MolekulargenMsiCatalogueTest.java | 126 ++++++++++++++++++ 9 files changed, 336 insertions(+), 6 deletions(-) create mode 100644 src/main/java/dev/pcvolkmer/onco/datamapper/datacatalogues/MolekulargenMsiCatalogue.java create mode 100644 src/main/java/dev/pcvolkmer/onco/datamapper/mapper/KpaMolekulargenetikMsiDataMapper.java rename src/main/java/dev/pcvolkmer/onco/datamapper/mapper/{KpaMolekulargenetikDataMapper.java => KpaMolekulargenetikNgsDataMapper.java} (98%) create mode 100644 src/test/java/dev/pcvolkmer/onco/datamapper/datacatalogues/MolekulargenMsiCatalogueTest.java diff --git a/README.md b/README.md index 5eb406e..6c00b11 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Um Mithilfe wird gebeten. | vorherige Molekular-Diagnostik | ✅ | | | Histologie-Berichte | ✅ | | | IHC-Berichte | - | Aktuell nicht vorgesehen | -| MSI-Befunde | ⌛ | Aktuell in Arbeit, https://github.com/dnpm-dip/mtb-model/issues/10 ist behoben | +| MSI-Befunde | ⛅ | Best effort: Formular OS.Molekulargenetik erfüllt nicht alle Anforderungen (2) | | NGS-Berichte | ⛅ | Best effort: Formular OS.Molekulargenetik erfüllt nicht alle Anforderungen (2) | | MTB-Beschlüsse | ✅ | Stützende molekulare Alteration(en) für einfache Variante und CNV (3) | | Follow-Up Verlauf | - | Späterer Zeitpunkt | @@ -67,7 +67,10 @@ Um Mithilfe wird gebeten. 1. Nicht alle möglichen Ausprägungen in `OS.Molekulargenetik` vorhanden. 2. Aktuell nicht alle Angaben effektiv im Formular `OS.Molekulargenetik` wie gefordert angebbar. - Hinweis: Tumorzellgehalt-Methode problematisch, wenn auch im NGS-Bericht histologisch festgestellt. + Hinweise: + * Tumorzellgehalt-Methode problematisch, wenn auch im NGS-Bericht histologisch festgestellt. + * Angabe zu MSI-Interpretation fehlt in Formular, ist aber Pflichtangabe - Wird gefiltert. + * Datenbanktabelle für MSI lautet tatsächlich `dk_molekluargenmsi` [sic!] 3. Implementierung des Mappings von HGNC-Symbol (Gen-Name) zu HGNC-ID über enthaltene Gen-Liste. ## Enthaltene Liste mit Genen diff --git a/src/main/java/dev/pcvolkmer/onco/datamapper/ResultSet.java b/src/main/java/dev/pcvolkmer/onco/datamapper/ResultSet.java index 329a78b..9df5f56 100644 --- a/src/main/java/dev/pcvolkmer/onco/datamapper/ResultSet.java +++ b/src/main/java/dev/pcvolkmer/onco/datamapper/ResultSet.java @@ -140,6 +140,28 @@ public class ResultSet { throw new IllegalArgumentException("Cannot convert " + raw.getClass() + " to Integer"); } + /** + * Get column value as Double and cast value if possible + * + * @param columnName The name of the column + * @return The column value as Integer + */ + public Double getDouble(String columnName) { + var raw = this.rawData.get(columnName); + + if (raw == null) { + return null; + } else if (raw instanceof Integer) { + return ((Integer) raw).doubleValue(); + } else if (raw instanceof Long) { + return ((Long) raw).doubleValue(); + } else if (raw instanceof Double) { + return ((Double) raw); + } + + throw new IllegalArgumentException("Cannot convert " + raw.getClass() + " to Integer"); + } + /** * Get column value as Date and cast value if possible * diff --git a/src/main/java/dev/pcvolkmer/onco/datamapper/datacatalogues/DataCatalogueFactory.java b/src/main/java/dev/pcvolkmer/onco/datamapper/datacatalogues/DataCatalogueFactory.java index 460af8c..44cbf46 100644 --- a/src/main/java/dev/pcvolkmer/onco/datamapper/datacatalogues/DataCatalogueFactory.java +++ b/src/main/java/dev/pcvolkmer/onco/datamapper/datacatalogues/DataCatalogueFactory.java @@ -95,6 +95,8 @@ public class DataCatalogueFactory { return MolekulargenetikCatalogue.create(jdbcTemplate); } else if (c == MolekulargenuntersuchungCatalogue.class) { return MolekulargenuntersuchungCatalogue.create(jdbcTemplate); + } else if (c == MolekulargenMsiCatalogue.class) { + return MolekulargenMsiCatalogue.create(jdbcTemplate); } else if (c == RebiopsieCatalogue.class) { return RebiopsieCatalogue.create(jdbcTemplate); } else if (c == ReevaluationCatalogue.class) { diff --git a/src/main/java/dev/pcvolkmer/onco/datamapper/datacatalogues/MolekulargenMsiCatalogue.java b/src/main/java/dev/pcvolkmer/onco/datamapper/datacatalogues/MolekulargenMsiCatalogue.java new file mode 100644 index 0000000..44b4e56 --- /dev/null +++ b/src/main/java/dev/pcvolkmer/onco/datamapper/datacatalogues/MolekulargenMsiCatalogue.java @@ -0,0 +1,46 @@ +/* + * This file is part of mv64e-onkostar-data + * + * Copyright (C) 2025 Paul-Christian Volkmer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + */ + +package dev.pcvolkmer.onco.datamapper.datacatalogues; + +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * Load raw result sets from database table 'dk_molekluargenmsi' + * + * @author Paul-Christian Volkmer + * @since 0.1 + */ +public class MolekulargenMsiCatalogue extends AbstractSubformDataCatalogue { + + private MolekulargenMsiCatalogue(JdbcTemplate jdbcTemplate) { + super(jdbcTemplate); + } + + @Override + protected String getTableName() { + return "dk_molekluargenmsi"; + } + + public static MolekulargenMsiCatalogue create(JdbcTemplate jdbcTemplate) { + return new MolekulargenMsiCatalogue(jdbcTemplate); + } + +} diff --git a/src/main/java/dev/pcvolkmer/onco/datamapper/mapper/KpaMolekulargenetikMsiDataMapper.java b/src/main/java/dev/pcvolkmer/onco/datamapper/mapper/KpaMolekulargenetikMsiDataMapper.java new file mode 100644 index 0000000..3b9857e --- /dev/null +++ b/src/main/java/dev/pcvolkmer/onco/datamapper/mapper/KpaMolekulargenetikMsiDataMapper.java @@ -0,0 +1,113 @@ +/* + * This file is part of mv64e-onkostar-data + * + * Copyright (C) 2025 Paul-Christian Volkmer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + */ + +package dev.pcvolkmer.onco.datamapper.mapper; + +import dev.pcvolkmer.mv64e.mtb.Msi; +import dev.pcvolkmer.mv64e.mtb.MsiMethodCoding; +import dev.pcvolkmer.mv64e.mtb.MsiMethodCodingCode; +import dev.pcvolkmer.mv64e.mtb.Reference; +import dev.pcvolkmer.onco.datamapper.ResultSet; +import dev.pcvolkmer.onco.datamapper.datacatalogues.MolekulargenMsiCatalogue; + +/** + * Mapper class to load and map prozedur data from database table 'dk_molekluargenetik' + * + * @author Paul-Christian Volkmer + * @since 0.1 + */ +public class KpaMolekulargenetikMsiDataMapper extends AbstractSubformDataMapper { + + public KpaMolekulargenetikMsiDataMapper( + final MolekulargenMsiCatalogue molekulargenMsiCatalogue + ) { + super(molekulargenMsiCatalogue); + } + + /** + * Loads and maps Prozedur related by database id + * + * @param id The database id of the procedure data set + * @return The loaded Procedure + */ + @Override + public Msi getById(final int id) { + return this.map(catalogue.getById(id)); + } + + @Override + protected Msi map(ResultSet resultSet) { + var builder = Msi.builder(); + + if (!resultSet.getString("komplexerbiomarker").equals("MSI")) { + return null; + } + + builder + .id(resultSet.getString("id")) + .patient(resultSet.getPatientReference()) + .method(getMethodCode(resultSet)) + .specimen(Reference.builder().id(resultSet.getString("hauptprozedur_id")).type("Specimen").build()) + // Aktuell nicht in Onkostar vorhanden! + //.interpretation() + // In Onkostar nur für "Sequenzierung" bzw "BIOINFORMATIC" als Prozentwert angegeben => "0" als Fallback? + .value(getSeqProzentwert(resultSet)) + ; + + + return builder.build(); + } + + private MsiMethodCoding getMethodCode(final ResultSet resultSet) { + var builder = MsiMethodCoding.builder() + .system("dnpm-dip/mtb/msi/method"); + + var analysemethoden = resultSet.getMerkmalList("analysemethoden"); + + // Achtung: Immer nur eine Methode wird betrachtet! In Onkostar sind gleichzeitig mehrere Angaben möglich! + if (analysemethoden == null) { + return null; + } else if (analysemethoden.contains("S")) { + builder.code(MsiMethodCodingCode.BIOINFORMATIC); + builder.display(MsiMethodCodingCode.BIOINFORMATIC.toString()); + } else if (analysemethoden.contains("P")) { + builder.code(MsiMethodCodingCode.PCR); + builder.display(MsiMethodCodingCode.PCR.toString()); + } else if (analysemethoden.contains("I")) { + builder.code(MsiMethodCodingCode.IHC); + builder.display(MsiMethodCodingCode.IHC.toString()); + } else { + return null; + } + + return builder.build(); + } + + private double getSeqProzentwert(final ResultSet resultSet) { + var analysemethoden = resultSet.getMerkmalList("analysemethoden"); + + // Achtung: Immer nur eine Methode wird betrachtet! In Onkostar sind gleichzeitig mehrere Angaben möglich! + if (analysemethoden != null && analysemethoden.contains("S")) { + return resultSet.getDouble("seqprozentwert"); + } + + return 0; + } +} diff --git a/src/main/java/dev/pcvolkmer/onco/datamapper/mapper/KpaMolekulargenetikDataMapper.java b/src/main/java/dev/pcvolkmer/onco/datamapper/mapper/KpaMolekulargenetikNgsDataMapper.java similarity index 98% rename from src/main/java/dev/pcvolkmer/onco/datamapper/mapper/KpaMolekulargenetikDataMapper.java rename to src/main/java/dev/pcvolkmer/onco/datamapper/mapper/KpaMolekulargenetikNgsDataMapper.java index 6774805..82a4632 100644 --- a/src/main/java/dev/pcvolkmer/onco/datamapper/mapper/KpaMolekulargenetikDataMapper.java +++ b/src/main/java/dev/pcvolkmer/onco/datamapper/mapper/KpaMolekulargenetikNgsDataMapper.java @@ -38,12 +38,12 @@ import java.util.stream.Collectors; * @author Paul-Christian Volkmer * @since 0.1 */ -public class KpaMolekulargenetikDataMapper implements DataMapper { +public class KpaMolekulargenetikNgsDataMapper implements DataMapper { private final MolekulargenetikCatalogue catalogue; private final MolekulargenuntersuchungCatalogue untersuchungCatalogue; - public KpaMolekulargenetikDataMapper( + public KpaMolekulargenetikNgsDataMapper( final MolekulargenetikCatalogue catalogue, final MolekulargenuntersuchungCatalogue untersuchungCatalogue, final PropertyCatalogue propertyCatalogue diff --git a/src/main/java/dev/pcvolkmer/onco/datamapper/mapper/MtbDataMapper.java b/src/main/java/dev/pcvolkmer/onco/datamapper/mapper/MtbDataMapper.java index 01b9120..7eff4be 100644 --- a/src/main/java/dev/pcvolkmer/onco/datamapper/mapper/MtbDataMapper.java +++ b/src/main/java/dev/pcvolkmer/onco/datamapper/mapper/MtbDataMapper.java @@ -124,7 +124,9 @@ public class MtbDataMapper implements DataMapper { catalogueFactory.catalogue(HistologieCatalogue.class) ); - var kpaMolekulargenetikDataMapper = new KpaMolekulargenetikDataMapper(molekulargenetikCatalogue, catalogueFactory.catalogue(MolekulargenuntersuchungCatalogue.class), propertyCatalogue); + var kpaMolekulargenetikNgsDataMapper = new KpaMolekulargenetikNgsDataMapper(molekulargenetikCatalogue, catalogueFactory.catalogue(MolekulargenuntersuchungCatalogue.class), propertyCatalogue); + var kpaMolekulargenetikMsiDataMapper = new KpaMolekulargenetikMsiDataMapper(catalogueFactory.catalogue(MolekulargenMsiCatalogue.class)); + var kpaVorbefundeDataMapper = new KpaVorbefundeDataMapper( catalogueFactory.catalogue(VorbefundeCatalogue.class), @@ -196,7 +198,16 @@ public class MtbDataMapper implements DataMapper { ) // NGS Berichte .ngsReports( - kpaMolekulargenetikDataMapper.getAllByKpaId(kpaId) + kpaMolekulargenetikNgsDataMapper.getAllByKpaId(kpaId) + ) + // MSI Befunde + .msiFindings( + kpaMolekulargenetikNgsDataMapper.getAllByKpaId(kpaId).stream() + .map(ngs -> Integer.parseInt(ngs.getId())) + .flatMap(ngsId -> kpaMolekulargenetikMsiDataMapper.getByParentId(ngsId).stream()) + // Filtere alle MSI: Nur mit Angabe Interpretation! + .filter(msi -> msi.getInterpretation() != null) + .collect(Collectors.toList()) ) ; diff --git a/src/test/java/dev/pcvolkmer/onco/datamapper/ResultSetTest.java b/src/test/java/dev/pcvolkmer/onco/datamapper/ResultSetTest.java index 793dcc3..07baaca 100644 --- a/src/test/java/dev/pcvolkmer/onco/datamapper/ResultSetTest.java +++ b/src/test/java/dev/pcvolkmer/onco/datamapper/ResultSetTest.java @@ -55,6 +55,13 @@ class ResultSetTest { assertThat(data.getLong("int")).isEqualTo(42L); } + @Test + void shouldReturnDoubleValues() { + var data = getTestData(); + + assertThat(data.getDouble("int")).isEqualTo(42); + } + @Test void shouldReturnDateValues() { var data = getTestData(); diff --git a/src/test/java/dev/pcvolkmer/onco/datamapper/datacatalogues/MolekulargenMsiCatalogueTest.java b/src/test/java/dev/pcvolkmer/onco/datamapper/datacatalogues/MolekulargenMsiCatalogueTest.java new file mode 100644 index 0000000..baf420b --- /dev/null +++ b/src/test/java/dev/pcvolkmer/onco/datamapper/datacatalogues/MolekulargenMsiCatalogueTest.java @@ -0,0 +1,126 @@ +/* + * This file is part of mv64e-onkostar-data + * + * Copyright (C) 2025 Paul-Christian Volkmer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + */ + +package dev.pcvolkmer.onco.datamapper.datacatalogues; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class MolekulargenMsiCatalogueTest { + + JdbcTemplate jdbcTemplate; + MolekulargenMsiCatalogue catalogue; + + @BeforeEach + void setUp(@Mock JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + this.catalogue = MolekulargenMsiCatalogue.create(jdbcTemplate); + } + + @Test + void shouldUseCorrectQuery(@Mock Map resultSet) { + doAnswer(invocationOnMock -> List.of(resultSet)) + .when(jdbcTemplate) + .queryForList(anyString(), anyInt()); + + this.catalogue.getById(1); + + var captor = ArgumentCaptor.forClass(String.class); + verify(this.jdbcTemplate).queryForList(captor.capture(), anyInt()); + + assertThat(captor.getValue()) + .isEqualTo("SELECT patient.patienten_id, dk_molekluargenmsi.*, prozedur.patient_id, prozedur.hauptprozedur_id FROM dk_molekluargenmsi JOIN prozedur ON (prozedur.id = dk_molekluargenmsi.id) JOIN patient ON (patient.id = prozedur.patient_id) WHERE geloescht = 0 AND prozedur.id = ?"); + } + + @Test + void shouldUseCorrectSubformQuery(@Mock Map resultSet) { + doAnswer(invocationOnMock -> List.of(resultSet)) + .when(jdbcTemplate) + .queryForList(anyString(), anyInt()); + + this.catalogue.getAllByParentId(1); + + var captor = ArgumentCaptor.forClass(String.class); + verify(this.jdbcTemplate).queryForList(captor.capture(), anyInt()); + + assertThat(captor.getValue()) + .isEqualTo("SELECT patient.patienten_id, dk_molekluargenmsi.*, prozedur.patient_id, prozedur.hauptprozedur_id FROM dk_molekluargenmsi JOIN prozedur ON (prozedur.id = dk_molekluargenmsi.id) JOIN patient ON (patient.id = prozedur.patient_id) WHERE geloescht = 0 AND hauptprozedur_id = ?"); + } + + @Test + void shouldUseCorrectMerkmalQuery(@Mock Map resultSet) { + when(resultSet.get(anyString())) + .thenReturn(Map.of("feldname", "name", "feldwert", "wert")); + + doAnswer(invocationOnMock -> List.of(resultSet)) + .when(jdbcTemplate) + .queryForList(anyString(), anyInt()); + + this.catalogue.getMerkmaleById(1); + + var captor = ArgumentCaptor.forClass(String.class); + verify(this.jdbcTemplate).queryForList(captor.capture(), anyInt()); + + assertThat(captor.getValue()) + .isEqualTo("SELECT feldname, feldwert FROM dk_molekluargenmsi_merkmale WHERE eintrag_id = ?"); + } + + @Test + void shouldUseMerkmalList() { + doAnswer(invocationOnMock -> { + var sql = invocationOnMock.getArgument(0, String.class); + ArrayList> result = new ArrayList<>(); + if (sql.startsWith("SELECT feldname")) { + result.add(Map.of("feldname", "name", "feldwert", "wert1")); + result.add(Map.of("feldname", "name", "feldwert", "wert2")); + } else { + var map = new HashMap(); + map.put("id", 1); + map.put("name", "x"); + result.add(map); + } + return result; + }) + .when(jdbcTemplate) + .queryForList(anyString(), anyInt()); + + var result = this.catalogue.getById(1); + + assertThat(result.getInteger("id")).isEqualTo(1); + assertThat(result.getMerkmalList("name")).isEqualTo(List.of("wert1", "wert2")); + } + +}