From 433dbbc18851a3f84f676e9f906122f7ba278e26 Mon Sep 17 00:00:00 2001 From: Paul-Christian Volkmer Date: Sat, 5 Jul 2025 13:32:31 +0200 Subject: [PATCH] feat: initial support for NGS reports --- .../datacatalogues/DataCatalogueFactory.java | 2 + .../MolekulargenetikCatalogue.java | 57 +++++- .../MolekulargenuntersuchungCatalogue.java | 50 ++++++ .../onco/datamapper/genes/GeneUtils.java | 9 + .../mapper/JsonToMolAltVarianteMapper.java | 19 +- .../mapper/KpaMolekulargenetikDataMapper.java | 166 ++++++++++++++++++ .../onco/datamapper/mapper/MtbDataMapper.java | 7 + .../MolekulargenetikCatalogueTest.java | 15 -- ...MolekulargenuntersuchungCatalogueTest.java | 82 +++++++++ 9 files changed, 377 insertions(+), 30 deletions(-) create mode 100644 src/main/java/dev/pcvolkmer/onco/datamapper/datacatalogues/MolekulargenuntersuchungCatalogue.java create mode 100644 src/main/java/dev/pcvolkmer/onco/datamapper/mapper/KpaMolekulargenetikDataMapper.java create mode 100644 src/test/java/dev/pcvolkmer/onco/datamapper/datacatalogues/MolekulargenuntersuchungCatalogueTest.java 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 acfcea6..b2772ef 100644 --- a/src/main/java/dev/pcvolkmer/onco/datamapper/datacatalogues/DataCatalogueFactory.java +++ b/src/main/java/dev/pcvolkmer/onco/datamapper/datacatalogues/DataCatalogueFactory.java @@ -93,6 +93,8 @@ public class DataCatalogueFactory { return EinzelempfehlungCatalogue.create(jdbcTemplate); } else if (c == MolekulargenetikCatalogue.class) { return MolekulargenetikCatalogue.create(jdbcTemplate); + } else if (c == MolekulargenuntersuchungCatalogue.class) { + return MolekulargenuntersuchungCatalogue.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/MolekulargenetikCatalogue.java b/src/main/java/dev/pcvolkmer/onco/datamapper/datacatalogues/MolekulargenetikCatalogue.java index 465d0c9..3694d10 100644 --- a/src/main/java/dev/pcvolkmer/onco/datamapper/datacatalogues/MolekulargenetikCatalogue.java +++ b/src/main/java/dev/pcvolkmer/onco/datamapper/datacatalogues/MolekulargenetikCatalogue.java @@ -20,15 +20,19 @@ package dev.pcvolkmer.onco.datamapper.datacatalogues; +import dev.pcvolkmer.onco.datamapper.ResultSet; import org.springframework.jdbc.core.JdbcTemplate; +import java.util.List; +import java.util.stream.Collectors; + /** * Load raw result sets from database table 'dk_molekulargenetik' * * @author Paul-Christian Volkmer * @since 0.1 */ -public class MolekulargenetikCatalogue extends AbstractSubformDataCatalogue { +public class MolekulargenetikCatalogue extends AbstractDataCatalogue { private MolekulargenetikCatalogue(JdbcTemplate jdbcTemplate) { super(jdbcTemplate); @@ -43,4 +47,55 @@ public class MolekulargenetikCatalogue extends AbstractSubformDataCatalogue { return new MolekulargenetikCatalogue(jdbcTemplate); } + /** + * Get procedure IDs by related Therapieplan procedure id + * Related form references in Einzelempfehlung, Rebiopsie, Reevaluation + * + * @param therapieplanId The procedure id + * @return The procedure ids + */ + public List getByTherapieplanId(int therapieplanId) { + return this.jdbcTemplate.queryForList( + "SELECT DISTINCT ref_molekulargenetik FROM dk_dnpm_uf_einzelempfehlung JOIN prozedur ON (prozedur.id = dk_dnpm_uf_einzelempfehlung.id) " + + " WHERE ref_molekulargenetik IS NOT NULL AND hauptprozedur_id = ? " + + " UNION SELECT ref_molekulargenetik FROM dk_dnpm_uf_rebiopsie JOIN prozedur ON (prozedur.id = dk_dnpm_uf_rebiopsie.id) " + + " WHERE ref_molekulargenetik IS NOT NULL AND hauptprozedur_id = ? " + + " UNION SELECT ref_molekulargenetik FROM dk_dnpm_uf_reevaluation JOIN prozedur ON (prozedur.id = dk_dnpm_uf_reevaluation.id) " + + " WHERE ref_molekulargenetik IS NOT NULL AND hauptprozedur_id = ?;" + , + therapieplanId, + therapieplanId, + therapieplanId) + .stream() + .map(ResultSet::from) + .map(rs -> rs.getInteger("ref_molekulargenetik")) + .collect(Collectors.toList()); + } + + /** + * Get procedure IDs used in related KPA/Therapieplan procedures + * Related form references in Einzelempfehlung, Rebiopsie, Reevaluation + * + * @param kpaId The procedure id + * @return The procedure ids + */ + public List getIdsByKpaId(int kpaId) { + return this.jdbcTemplate.queryForList( + "SELECT DISTINCT ref_molekulargenetik FROM dk_dnpm_uf_einzelempfehlung JOIN prozedur ON (prozedur.id = dk_dnpm_uf_einzelempfehlung.id) " + + " WHERE ref_molekulargenetik IS NOT NULL AND hauptprozedur_id IN (SELECT id FROM dk_dnpm_therapieplan WHERE ref_dnpm_klinikanamnese = ?) " + + " UNION SELECT ref_molekulargenetik FROM dk_dnpm_uf_rebiopsie JOIN prozedur ON (prozedur.id = dk_dnpm_uf_rebiopsie.id) " + + " WHERE ref_molekulargenetik IS NOT NULL AND hauptprozedur_id IN (SELECT id FROM dk_dnpm_therapieplan WHERE ref_dnpm_klinikanamnese = ?) " + + " UNION SELECT ref_molekulargenetik FROM dk_dnpm_uf_reevaluation JOIN prozedur ON (prozedur.id = dk_dnpm_uf_reevaluation.id) " + + " WHERE ref_molekulargenetik IS NOT NULL AND hauptprozedur_id IN (SELECT id FROM dk_dnpm_therapieplan WHERE ref_dnpm_klinikanamnese = ?);" + , + kpaId, + kpaId, + kpaId) + .stream() + .map(ResultSet::from) + .map(rs -> rs.getInteger("ref_molekulargenetik")) + .distinct() + .collect(Collectors.toList()); + } + } diff --git a/src/main/java/dev/pcvolkmer/onco/datamapper/datacatalogues/MolekulargenuntersuchungCatalogue.java b/src/main/java/dev/pcvolkmer/onco/datamapper/datacatalogues/MolekulargenuntersuchungCatalogue.java new file mode 100644 index 0000000..ebd722a --- /dev/null +++ b/src/main/java/dev/pcvolkmer/onco/datamapper/datacatalogues/MolekulargenuntersuchungCatalogue.java @@ -0,0 +1,50 @@ +/* + * 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 dev.pcvolkmer.onco.datamapper.ResultSet; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Load raw result sets from database table 'dk_molekulargenuntersuchung' + * + * @author Paul-Christian Volkmer + * @since 0.1 + */ +public class MolekulargenuntersuchungCatalogue extends AbstractSubformDataCatalogue { + + private MolekulargenuntersuchungCatalogue(JdbcTemplate jdbcTemplate) { + super(jdbcTemplate); + } + + @Override + protected String getTableName() { + return "dk_molekulargenuntersuchung"; + } + + public static MolekulargenuntersuchungCatalogue create(JdbcTemplate jdbcTemplate) { + return new MolekulargenuntersuchungCatalogue(jdbcTemplate); + } + +} diff --git a/src/main/java/dev/pcvolkmer/onco/datamapper/genes/GeneUtils.java b/src/main/java/dev/pcvolkmer/onco/datamapper/genes/GeneUtils.java index 4295815..fb63fc2 100644 --- a/src/main/java/dev/pcvolkmer/onco/datamapper/genes/GeneUtils.java +++ b/src/main/java/dev/pcvolkmer/onco/datamapper/genes/GeneUtils.java @@ -20,6 +20,7 @@ package dev.pcvolkmer.onco.datamapper.genes; +import dev.pcvolkmer.mv64e.mtb.Coding; import org.apache.commons.csv.CSVFormat; import java.io.IOException; @@ -48,6 +49,14 @@ public class GeneUtils { return genes().stream().filter(gene -> gene.getSymbol().equalsIgnoreCase(symbol)).findFirst(); } + public static Coding toCoding(Gene gene) { + return Coding.builder() + .code(gene.getHgncId()) + .display(gene.getSymbol()) + .system("https://www.genenames.org/") + .build(); + } + private static List genes() { var result = new ArrayList(); diff --git a/src/main/java/dev/pcvolkmer/onco/datamapper/mapper/JsonToMolAltVarianteMapper.java b/src/main/java/dev/pcvolkmer/onco/datamapper/mapper/JsonToMolAltVarianteMapper.java index d01b9fa..15bd068 100644 --- a/src/main/java/dev/pcvolkmer/onco/datamapper/mapper/JsonToMolAltVarianteMapper.java +++ b/src/main/java/dev/pcvolkmer/onco/datamapper/mapper/JsonToMolAltVarianteMapper.java @@ -23,7 +23,6 @@ package dev.pcvolkmer.onco.datamapper.mapper; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import dev.pcvolkmer.mv64e.mtb.Coding; import dev.pcvolkmer.mv64e.mtb.GeneAlterationReference; import dev.pcvolkmer.mv64e.mtb.Reference; import dev.pcvolkmer.onco.datamapper.exceptions.DataAccessException; @@ -53,19 +52,11 @@ public class JsonToMolAltVarianteMapper { }).stream() .map(variante -> { var resultBuilder = GeneAlterationReference.builder(); - GeneUtils.findBySymbol(variante.getGen()).ifPresent(gene -> { - resultBuilder - .gene( - Coding.builder() - .code(gene.getHgncId()) - .display(gene.getSymbol()) - .system("https://www.genenames.org/") - .build() - ) - .variant( - Reference.builder().id(variante.id).type("Variant").build() - ); - }); + GeneUtils.findBySymbol(variante.getGen()).ifPresent(gene -> resultBuilder + .gene(GeneUtils.toCoding(gene)) + .variant( + Reference.builder().id(variante.id).type("Variant").build() + )); return resultBuilder.build(); }) .collect(Collectors.toList()); diff --git a/src/main/java/dev/pcvolkmer/onco/datamapper/mapper/KpaMolekulargenetikDataMapper.java b/src/main/java/dev/pcvolkmer/onco/datamapper/mapper/KpaMolekulargenetikDataMapper.java new file mode 100644 index 0000000..1751706 --- /dev/null +++ b/src/main/java/dev/pcvolkmer/onco/datamapper/mapper/KpaMolekulargenetikDataMapper.java @@ -0,0 +1,166 @@ +/* + * 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.*; +import dev.pcvolkmer.onco.datamapper.PropertyCatalogue; +import dev.pcvolkmer.onco.datamapper.datacatalogues.*; +import dev.pcvolkmer.onco.datamapper.genes.GeneUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Mapper class to load and map prozedur data from database table 'dk_molekulargenetik' + * + * @author Paul-Christian Volkmer + * @since 0.1 + */ +public class KpaMolekulargenetikDataMapper implements DataMapper { + + private final MolekulargenetikCatalogue catalogue; + private final MolekulargenuntersuchungCatalogue untersuchungCatalogue; + + public KpaMolekulargenetikDataMapper( + final MolekulargenetikCatalogue catalogue, + final MolekulargenuntersuchungCatalogue untersuchungCatalogue, + final PropertyCatalogue propertyCatalogue + ) { + this.catalogue = catalogue; + this.untersuchungCatalogue = untersuchungCatalogue; + } + + /** + * Loads and maps Prozedur related by database id + * + * @param id The database id of the procedure data set + * @return The loaded Procedure + */ + @Override + public SomaticNgsReport getById(final int id) { + var data = catalogue.getById(id); + + var builder = SomaticNgsReport.builder(); + builder + .id(data.getString("id")) + .patient(data.getPatientReference()) + .issuedOn(data.getDate("datum")) + .specimen(Reference.builder().id(data.getString("einsendenummer")).type("Specimen").build()) + .results(this.getNgsReportResults(id)) + ; + + return builder.build(); + + } + + /** + * Loads and maps all Prozedur related by KPA database id + * + * @param kpaId The database id of the KPA procedure data set + * @return The loaded Procedures + */ + public List getAllByKpaId(final int kpaId) { + return this.catalogue.getIdsByKpaId(kpaId).stream() + .distinct() + .map(this::getById) + .collect(Collectors.toList()); + } + + private NgsReportResults getNgsReportResults(int id) { + var subforms = this.untersuchungCatalogue.getAllByParentId(id); + + var resultBuilder = NgsReportResults.builder(); + resultBuilder.simpleVariants( + subforms.stream() + // P => Einfache Variante + .filter(subform -> "P".equals(subform.getString("ergebnis"))) + .map(subform -> { + final var geneOptional = GeneUtils.findBySymbol(subform.getString("untersucht")); + if (geneOptional.isEmpty()) { + return null; + } + + final var snvBuilder = Snv.builder() + .id(subform.getString("id")) + .patient(subform.getPatientReference()) + .gene(GeneUtils.toCoding(geneOptional.get())) + .exonId(subform.getString("exon")) + .dnaChange(subform.getString("cdnanomenklatur")) + .proteinChange(subform.getString("proteinebenenomenklatur")); + + if (null != subform.getLong("allelfrequenz")) { + snvBuilder.allelicFrequency(subform.getLong("allelfrequenz")); + } + if (null != subform.getLong("evreaddepth")) { + snvBuilder.readDepth(subform.getLong("evreaddepth")); + } + geneOptional.get().getSingleChromosomeInPropertyForm().ifPresent(snvBuilder::chromosome); + + return snvBuilder.build(); + }) + .collect(Collectors.toList()) + ); + + resultBuilder.copyNumberVariants( + subforms.stream() + .filter(subform -> "CNV".equals(subform.getString("ergebnis"))) + .map(subform -> { + final var geneOptional = GeneUtils.findBySymbol(subform.getString("untersucht")); + if (geneOptional.isEmpty()) { + return null; + } + + final var reportedAffectedGenes = new ArrayList(); + reportedAffectedGenes.add(subform.getString("untersucht")); + + // Weitere betroffene Gene aus Freitextfeld? + if (null != subform.getString("cnvbetroffenegene")) { + reportedAffectedGenes.addAll( + Arrays.stream(subform.getString("cnvbetroffenegene").split("\\s")).collect(Collectors.toList()) + ); + } + + final var cnvBuilder = Cnv.builder() + .id(subform.getString("id")) + .patient(subform.getPatientReference()) + .reportedAffectedGenes( + reportedAffectedGenes.stream() + .distinct() + .map(GeneUtils::findBySymbol) + .filter(Optional::isPresent) + .map(gene -> GeneUtils.toCoding(gene.get())) + .collect(Collectors.toList()) + ) + .totalCopyNumber(subform.getLong("cnvtotalcn")); + + geneOptional.get().getSingleChromosomeInPropertyForm().ifPresent(cnvBuilder::chromosome); + + return cnvBuilder.build(); + }) + .collect(Collectors.toList()) + ); + return resultBuilder.build(); + } + +} 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 43b492f..0f40bee 100644 --- a/src/main/java/dev/pcvolkmer/onco/datamapper/mapper/MtbDataMapper.java +++ b/src/main/java/dev/pcvolkmer/onco/datamapper/mapper/MtbDataMapper.java @@ -124,6 +124,8 @@ public class MtbDataMapper implements DataMapper { propertyCatalogue ); + var kpaMolekulargenetikDataMapper = new KpaMolekulargenetikDataMapper(molekulargenetikCatalogue, catalogueFactory.catalogue(MolekulargenuntersuchungCatalogue.class), propertyCatalogue); + var resultBuilder = Mtb.builder(); try { @@ -156,6 +158,11 @@ public class MtbDataMapper implements DataMapper { Reference.builder().id(diagnosis.getId()).type("MTBDiagnosis").build() ) ) + // NGS Berichte + .ngsReports( + kpaMolekulargenetikDataMapper.getAllByKpaId(kpaId) + ) + ; } catch (DataAccessException e) { logger.error("Error while getting Mtb.", e); diff --git a/src/test/java/dev/pcvolkmer/onco/datamapper/datacatalogues/MolekulargenetikCatalogueTest.java b/src/test/java/dev/pcvolkmer/onco/datamapper/datacatalogues/MolekulargenetikCatalogueTest.java index 019b3b9..cc67e90 100644 --- a/src/test/java/dev/pcvolkmer/onco/datamapper/datacatalogues/MolekulargenetikCatalogueTest.java +++ b/src/test/java/dev/pcvolkmer/onco/datamapper/datacatalogues/MolekulargenetikCatalogueTest.java @@ -64,19 +64,4 @@ class MolekulargenetikCatalogueTest { .isEqualTo("SELECT patient.patienten_id, dk_molekulargenetik.*, prozedur.* FROM dk_molekulargenetik JOIN prozedur ON (prozedur.id = dk_molekulargenetik.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_molekulargenetik.*, prozedur.* FROM dk_molekulargenetik JOIN prozedur ON (prozedur.id = dk_molekulargenetik.id) JOIN patient ON (patient.id = prozedur.patient_id) WHERE geloescht = 0 AND hauptprozedur_id = ?"); - } - } diff --git a/src/test/java/dev/pcvolkmer/onco/datamapper/datacatalogues/MolekulargenuntersuchungCatalogueTest.java b/src/test/java/dev/pcvolkmer/onco/datamapper/datacatalogues/MolekulargenuntersuchungCatalogueTest.java new file mode 100644 index 0000000..a0f96e7 --- /dev/null +++ b/src/test/java/dev/pcvolkmer/onco/datamapper/datacatalogues/MolekulargenuntersuchungCatalogueTest.java @@ -0,0 +1,82 @@ +/* + * 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.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.doAnswer; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class MolekulargenuntersuchungCatalogueTest { + + JdbcTemplate jdbcTemplate; + MolekulargenuntersuchungCatalogue catalogue; + + @BeforeEach + void setUp(@Mock JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + this.catalogue = MolekulargenuntersuchungCatalogue.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_molekulargenuntersuchung.*, prozedur.* FROM dk_molekulargenuntersuchung JOIN prozedur ON (prozedur.id = dk_molekulargenuntersuchung.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_molekulargenuntersuchung.*, prozedur.* FROM dk_molekulargenuntersuchung JOIN prozedur ON (prozedur.id = dk_molekulargenuntersuchung.id) JOIN patient ON (patient.id = prozedur.patient_id) WHERE geloescht = 0 AND hauptprozedur_id = ?"); + } + +}