Skip to content

Commit

Permalink
feat: add initial SystemischeTherapieProcedureMapper (#165)
Browse files Browse the repository at this point in the history
* feat: add initial SystemischeTherapieMapper

* feat: set code.systemische_therapie_art if available

* fix: handle oBDS DatumTagOderMonatGenauTyp

* fix: use absent category

* test: add tests for conversion to DateTimeType

* refactor: rename mapper class to reflect destination type

* refactor: rename init method

* feat: add outcome if available in oBDS
  • Loading branch information
pcvolkmer authored Nov 21, 2024
1 parent 75a545e commit 04f8399
Show file tree
Hide file tree
Showing 10 changed files with 457 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public static class FhirExtensions {
private String miiExOnkoStrahlentherapieIntention;
private String miiExOnkoStrahlentherapieBestrahlung;
private String miiExOnkoHistologyMorphologyBehaviorIcdo3;
private String miiExOnkoSystemischeTherapieIntention;
}

@Data
Expand Down Expand Up @@ -74,7 +75,10 @@ public static class FhirSystems {
private String miiCsOnkoStrahlentherapieStrahlenart;
private String miiCsOnkoStrahlentherapieZielgebiet;
private String strahlentherapieProcedureId;
private String systemischeTherapieProcedureId;
private String miiCsOnkoSystemischeTherapieArt;
private String miiCsOnkoSeitenlokalisation;
private String miiCsTherapieGrundEnde;
private String conditionVerStatus;
private String icdo3MorphologieOid;
}
Expand All @@ -95,6 +99,7 @@ public static class FhirProfiles {
private String deathObservation;
private String miiPrOnkoDiagnosePrimaertumor;
private String miiPrOnkoStrahlentherapie;
private String miiPrOnkoSystemischeTherapie;
}

@Data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import com.google.common.hash.Hashing;
import de.basisdatensatz.obds.v3.DatumTagOderMonatGenauTyp;
import de.basisdatensatz.obds.v3.DatumTagOderMonatOderJahrOderNichtGenauTyp;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
Expand All @@ -12,6 +13,7 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.xml.datatype.XMLGregorianCalendar;
import org.hl7.fhir.r4.model.*;
import org.miracum.streams.ume.obdstofhir.FhirProperties;
import org.miracum.streams.ume.obdstofhir.model.Meldeanlass;
Expand Down Expand Up @@ -273,4 +275,34 @@ public static DateType convertObdsDatumToDateType(
}
return date;
}

public static Optional<DateTimeType> convertObdsDatumToDateTimeType(
DatumTagOderMonatGenauTyp obdsDatum) {
if (null == obdsDatum) {
return Optional.empty();
}

var date = new DateTimeType(obdsDatum.getValue().toGregorianCalendar().getTime());
switch (obdsDatum.getDatumsgenauigkeit()) {
// exakt (entspricht taggenau)
case E:
date.setPrecision(TemporalPrecisionEnum.DAY);
break;
// Tag geschätzt (entspricht monatsgenau)
case T:
date.setPrecision(TemporalPrecisionEnum.MONTH);
break;
}
return Optional.of(date);
}

public static Optional<DateTimeType> convertObdsDatumToDateTimeType(
XMLGregorianCalendar obdsDatum) {
if (null == obdsDatum) {
return Optional.empty();
}
var date = new DateTimeType(obdsDatum.toGregorianCalendar().getTime());
date.setPrecision(TemporalPrecisionEnum.DAY);
return Optional.of(date);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package org.miracum.streams.ume.obdstofhir.mapper.mii;

import de.basisdatensatz.obds.v3.SYSTTyp;
import java.util.Objects;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.r4.model.CodeType;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.Enumerations.ResourceType;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Period;
import org.hl7.fhir.r4.model.Procedure;
import org.hl7.fhir.r4.model.Reference;
import org.miracum.streams.ume.obdstofhir.FhirProperties;
import org.miracum.streams.ume.obdstofhir.mapper.ObdsToFhirMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class SystemischeTherapieProcedureMapper extends ObdsToFhirMapper {

private static final Logger LOG =
LoggerFactory.getLogger(SystemischeTherapieProcedureMapper.class);

public SystemischeTherapieProcedureMapper(FhirProperties fhirProperties) {
super(fhirProperties);
}

public Procedure map(SYSTTyp syst, Reference subject) {
Objects.requireNonNull(syst, "Systemtherapie must not be null");
Objects.requireNonNull(subject, "Reference must not be null");

Validate.notBlank(syst.getSYSTID(), "Required SYST_ID is unset");
Validate.isTrue(
Objects.equals(
subject.getReferenceElement().getResourceType(), ResourceType.PATIENT.toCode()),
"The subject reference should point to a Patient resource");

var procedure = new Procedure();
procedure.getMeta().addProfile(fhirProperties.getProfiles().getMiiPrOnkoSystemischeTherapie());

// TODO: can we be sure that this SYST-ID is globally unqiue across all SYSTs? -
// if not we may instead need to construct the ID from the patient-id + others.
var identifier =
new Identifier()
.setSystem(fhirProperties.getSystems().getSystemischeTherapieProcedureId())
.setValue(syst.getSYSTID());
procedure.addIdentifier(identifier);
procedure.setId(computeResourceIdFromIdentifier(identifier));

// Status
if (syst.getMeldeanlass() == SYSTTyp.Meldeanlass.BEHANDLUNGSENDE) {
procedure.setStatus(Procedure.ProcedureStatus.COMPLETED);
} else {
procedure.setStatus(Procedure.ProcedureStatus.INPROGRESS);
}

procedure.setSubject(subject);

var dataAbsentExtension =
new Extension(
fhirProperties.getExtensions().getDataAbsentReason(), new CodeType("unknown"));
var dataAbsentCode = new CodeType();
dataAbsentCode.addExtension(dataAbsentExtension);

if (syst.getBeginn() == null && syst.getEnde() == null) {
var performedStart = new DateTimeType();
performedStart.addExtension(dataAbsentExtension);
var performed = new Period().setStartElement(performedStart);
procedure.setPerformed(performed);
} else {
var performed = new Period();
convertObdsDatumToDateTimeType(syst.getBeginn()).ifPresent(performed::setStartElement);
convertObdsDatumToDateTimeType(syst.getEnde()).ifPresent(performed::setEndElement);
procedure.setPerformed(performed);
}

var code = new CodeableConcept();
// Always add absent OPS
code.addCoding().setSystem(fhirProperties.getSystems().getOps()).setCodeElement(dataAbsentCode);
if (null != syst.getTherapieart()) {
code.addCoding()
.setSystem(fhirProperties.getSystems().getMiiCsOnkoSystemischeTherapieArt())
.setCode(syst.getTherapieart().value());
}

procedure.setCode(code);

var category = new CodeableConcept();
category
.addCoding()
.setSystem(fhirProperties.getSystems().getSnomed())
.setCodeElement(dataAbsentCode);
procedure.setCategory(category);

var intention = new CodeableConcept();
intention
.addCoding()
.setSystem(fhirProperties.getSystems().getMiiCsOnkoIntention())
.setCode(syst.getIntention()); // Direct mapping from oBDS value
procedure.addExtension(
fhirProperties.getExtensions().getMiiExOnkoSystemischeTherapieIntention(), intention);

if (null != syst.getEndeGrund()) {
var outcome = new CodeableConcept();
outcome
.addCoding()
.setSystem(fhirProperties.getSystems().getMiiCsTherapieGrundEnde())
.setCode(syst.getEndeGrund().value());
procedure.setOutcome(outcome);
}

return procedure;
}
}
5 changes: 5 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ fhir:
mii-ex-onko-strahlentherapie-intention: "https://www.medizininformatik-initiative.de/fhir/ext/modul-onko/StructureDefinition/mii-ex-onko-strahlentherapie-intention"
mii-ex-onko-strahlentherapie-bestrahlung: "https://www.medizininformatik-initiative.de/fhir/ext/modul-onko/StructureDefinition/mii-ex-onko-strahlentherapie-bestrahlung"
mii-ex-onko-histology-morphology-behavior-icdo3: "https://www.medizininformatik-initiative.de/fhir/ext/modul-onko/StructureDefinition/mii-ex-onko-histology-morphology-behavior-icdo3"
mii-ex-onko-systemische-therapie-intention: "https://www.medizininformatik-initiative.de/fhir/ext/modul-onko/StructureDefinition/mii-ex-onko-systemische-therapie-intention"
systems:
patientId: "${fhir.default.baseSystemUrl}/obds-to-fhir/identifiers/patient-id"
identifier-type: "http://terminology.hl7.org/CodeSystem/v2-0203"
Expand All @@ -28,6 +29,7 @@ fhir:
gleasonScoreObservationId: "${fhir.default.baseSystemUrl}/obds-to-fhir/identifiers/gleason-score-observation-id"
psaObservationId: "${fhir.default.baseSystemUrl}/obds-to-fhir/identifiers/psa-observation-id"
strahlentherapie-procedure-id: "${fhir.default.baseSystemUrl}/obds-to-fhir/identifiers/strahlentherapie-procedure-id"
systemische-therapie-procedure-id: "${fhir.default.baseSystemUrl}/obds-to-fhir/identifiers/systemische-therapie-procedure-id"
loinc: "http://loinc.org"
icdo3Morphologie: "http://terminology.hl7.org/CodeSystem/icd-o-3"
icdo3MorphologieOid: "urn:oid:2.16.840.1.113883.6.43.1"
Expand Down Expand Up @@ -65,6 +67,8 @@ fhir:
mii-cs-onko-strahlentherapie-strahlenart: "https://www.medizininformatik-initiative.de/fhir/ext/modul-onko/CodeSystem/mii-cs-onko-strahlentherapie-strahlenart"
mii-cs-onko-strahlentherapie-zielgebiet: "https://www.medizininformatik-initiative.de/fhir/ext/modul-onko/CodeSystem/mii-cs-onko-strahlentherapie-zielgebiet"
mii-cs-onko-seitenlokalisation: "https://www.medizininformatik-initiative.de/fhir/ext/modul-onko/CodeSystem/mii-cs-onko-seitenlokalisation"
mii-cs-onko-systemische-therapie-art: "https://www.medizininformatik-initiative.de/fhir/ext/modul-onko/CodeSystem/mii-cs-onko-therapie-typ"
mii-cs-therapie-grund-ende: "https://www.medizininformatik-initiative.de/fhir/ext/modul-onko/CodeSystem/mii-cs-therapie-grund-ende"
profiles:
histologie: "http://dktk.dkfz.de/fhir/StructureDefinition/onco-core-Observation-Histologie"
grading: "http://dktk.dkfz.de/fhir/StructureDefinition/onco-core-Observation-Grading"
Expand All @@ -81,6 +85,7 @@ fhir:
mii-patient-pseudonymisiert: "https://www.medizininformatik-initiative.de/fhir/core/modul-person/StructureDefinition/PatientPseudonymisiert"
mii-pr-onko-diagnose-primaertumor: "https://www.medizininformatik-initiative.de/fhir/ext/modul-onko/StructureDefinition/mii-pr-onko-diagnose-primaertumor"
mii-pr-onko-strahlentherapie: "https://www.medizininformatik-initiative.de/fhir/ext/modul-onko/StructureDefinition/mii-pr-onko-strahlentherapie"
mii-pr-onko-systemische-therapie: "https://www.medizininformatik-initiative.de/fhir/ext/modul-onko/StructureDefinition/mii-pr-onko-systemische-therapie"
display:
histologyLoinc: "Histology and Behavior ICD-O-3 Cancer"
gradingLoinc: "Grade pathology value Cancer"
Expand Down
9 changes: 9 additions & 0 deletions src/main/resources/schema/oBDS_v3.0.3.bindings.xjb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@
</jaxb:bindings>
<jaxb:bindings node="//xs:complexType[@name='ST_Typ']//xs:element[@name='Meldeanlass']/xs:simpleType">
<jaxb:typesafeEnumClass name="Meldeanlass" />
</jaxb:bindings>
<jaxb:bindings node="//xs:complexType[@name='SYST_Typ']//xs:element[@name='Meldeanlass']/xs:simpleType">
<jaxb:typesafeEnumClass name="Meldeanlass" />
</jaxb:bindings>
<jaxb:bindings node="//xs:element[@name='Therapieart']/xs:simpleType">
<jaxb:typesafeEnumClass name="Therapieart" />
</jaxb:bindings>
<jaxb:bindings node="//xs:complexType[@name='SYST_Typ']//xs:element[@name='Ende_Grund']/xs:simpleType">
<jaxb:typesafeEnumClass name="EndeGrund" />
</jaxb:bindings>
</jaxb:bindings>
</jaxb:bindings>
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

import de.basisdatensatz.obds.v3.DatumTagOderMonatGenauTyp;
import de.basisdatensatz.obds.v3.DatumTagOderMonatGenauTyp.DatumsgenauigkeitTagOderMonatGenau;
import java.time.DateTimeException;
import java.util.Arrays;
import java.util.stream.Stream;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;
import org.hl7.fhir.r4.model.DateTimeType;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
Expand Down Expand Up @@ -98,9 +103,7 @@ void extractDateTimeFromADTDate_withGivenObdsDate_shouldConvertToExpectedFhirDat
void convertObdsDateToDateTimeTypeShouldThrowExceptionOnUnparsableDateString() {
assertThrows(
DateTimeException.class,
() -> {
ObdsToFhirMapper.convertObdsDateToDateTimeType("some shiny day somewere");
});
() -> ObdsToFhirMapper.convertObdsDateToDateTimeType("some shiny day somewere"));
}

@ParameterizedTest
Expand Down Expand Up @@ -139,4 +142,61 @@ private static Stream<Arguments> icd10GmCodeValidationData() {
Arguments.of("", false),
Arguments.of(null, false));
}

@ParameterizedTest
@MethodSource("obdsDatumToDateTimeTypeData")
void shouldConvertDatumTagOderMonatGenauTypToDateTimeType(
XMLGregorianCalendar sourceDate,
DatumsgenauigkeitTagOderMonatGenau genauigkeit,
DateTimeType expected) {
var data = new DatumTagOderMonatGenauTyp();
data.setValue(sourceDate);
data.setDatumsgenauigkeit(genauigkeit);

var actual = ObdsToFhirMapper.convertObdsDatumToDateTimeType(data);

assertThat(actual)
.hasValueSatisfying(
dateTime ->
assertThat(dateTime.getValueAsString()).isEqualTo(expected.getValueAsString()));
}

private static Stream<Arguments> obdsDatumToDateTimeTypeData() {
return Stream.of(
Arguments.of(
DatatypeFactory.newDefaultInstance().newXMLGregorianCalendar("2024-11-21"),
DatumsgenauigkeitTagOderMonatGenau.E,
DateTimeType.parseV3("20241121")),
Arguments.of(
DatatypeFactory.newDefaultInstance().newXMLGregorianCalendar("2024-11-21"),
DatumsgenauigkeitTagOderMonatGenau.T,
DateTimeType.parseV3("202411")));
}

@Test
void shouldConvertCalendarToDateTimeType() {
var actual =
ObdsToFhirMapper.convertObdsDatumToDateTimeType(
DatatypeFactory.newDefaultInstance().newXMLGregorianCalendar("2024-11-21"));

assertThat(actual)
.hasValueSatisfying(
dateTime ->
assertThat(dateTime.getValueAsString())
.isEqualTo(DateTimeType.parseV3("20241121").getValueAsString()));
}

@Test
void shouldNotConvertCalendarToDateTimeTypeFromNull() {
var actual = ObdsToFhirMapper.convertObdsDatumToDateTimeType((XMLGregorianCalendar) null);

assertThat(actual).isEmpty();
}

@Test
void shouldNotConvertDatumTagOderMonatGenauTypToDateTimeTypeFromNull() {
var actual = ObdsToFhirMapper.convertObdsDatumToDateTimeType((DatumTagOderMonatGenauTyp) null);

assertThat(actual).isEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package org.miracum.streams.ume.obdstofhir.mapper.mii;

import static org.assertj.core.api.Assertions.assertThat;

import ca.uhn.fhir.context.FhirContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationModule;
import de.basisdatensatz.obds.v3.OBDS;
import java.io.IOException;
import org.approvaltests.Approvals;
import org.hl7.fhir.r4.model.Reference;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.miracum.streams.ume.obdstofhir.FhirProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest(classes = {FhirProperties.class})
@EnableConfigurationProperties
class SystemischeTherapieProcedureMapperTest {
private static SystemischeTherapieProcedureMapper sut;

@BeforeAll
static void beforeAll(@Autowired FhirProperties fhirProps) {
sut = new SystemischeTherapieProcedureMapper(fhirProps);
}

@ParameterizedTest
@CsvSource({"Testpatient_1.xml", "Testpatient_2.xml", "Testpatient_3.xml"})
void map_withGivenObds_shouldCreateValidProcedure(String sourceFile) throws IOException {
final var resource = this.getClass().getClassLoader().getResource("obds3/" + sourceFile);
assertThat(resource).isNotNull();

final var xmlMapper =
XmlMapper.builder()
.defaultUseWrapper(false)
.addModule(new JakartaXmlBindAnnotationModule())
.addModule(new Jdk8Module())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.build();

final var obds = xmlMapper.readValue(resource.openStream(), OBDS.class);

var obdsPatient = obds.getMengePatient().getPatient().getFirst();

var subject = new Reference("Patient/any");
var systMeldung =
obdsPatient.getMengeMeldung().getMeldung().stream()
.filter(m -> m.getSYST() != null)
.findFirst()
.get();
var procedure = sut.map(systMeldung.getSYST(), subject);

var fhirParser = FhirContext.forR4().newJsonParser().setPrettyPrint(true);
var fhirJson = fhirParser.encodeResourceToString(procedure);
Approvals.verify(
fhirJson, Approvals.NAMES.withParameters(sourceFile).forFile().withExtension(".fhir.json"));
}
}
Loading

0 comments on commit 04f8399

Please sign in to comment.