refs/refs/ETL-German-FHIR-Core/src/main/java/org/miracum/etl/fhirtoomop/mapper/ProcedureMapper.java

lines 703–734 1141 lines · java
1package org.miracum.etl.fhirtoomop.mapper;
3import static org.miracum.etl.fhirtoomop.Constants.CONCEPT_EHR;
4import static org.miracum.etl.fhirtoomop.Constants.FHIR_RESOURCE_ACCEPTABLE_EVENT_STATUS_LIST;
5import static org.miracum.etl.fhirtoomop.Constants.OMOP_DOMAIN_DRUG;
6import static org.miracum.etl.fhirtoomop.Constants.OMOP_DOMAIN_MEASUREMENT;
7import static org.miracum.etl.fhirtoomop.Constants.OMOP_DOMAIN_OBSERVATION;
8import static org.miracum.etl.fhirtoomop.Constants.OMOP_DOMAIN_PROCEDURE;
9import static org.miracum.etl.fhirtoomop.Constants.SOURCE_VOCABULARY_ID_PROCEDURE_BODYSITE;
10import static org.miracum.etl.fhirtoomop.Constants.SOURCE_VOCABULARY_ID_PROCEDURE_DICOM;
11import static org.miracum.etl.fhirtoomop.Constants.VOCABULARY_OPS;
12import static org.miracum.etl.fhirtoomop.Constants.VOCABULARY_SNOMED;
14import com.google.common.base.Strings;
15import io.micrometer.core.instrument.Counter;
16import java.sql.Timestamp;
17import java.time.LocalDate;
18import java.time.LocalDateTime;
19import java.util.ArrayList;
20import java.util.Arrays;
21import java.util.Collections;
22import java.util.List;
23import lombok.extern.slf4j.Slf4j;
24import org.apache.commons.lang3.tuple.Pair;
25import org.hl7.fhir.r4.model.CodeableConcept;
26import org.hl7.fhir.r4.model.Coding;
27import org.hl7.fhir.r4.model.Procedure;
28import org.miracum.etl.fhirtoomop.DbMappings;
29import org.miracum.etl.fhirtoomop.config.FhirSystems;
30import org.miracum.etl.fhirtoomop.mapper.helpers.FindOmopConcepts;
31import org.miracum.etl.fhirtoomop.mapper.helpers.MapperMetrics;
32import org.miracum.etl.fhirtoomop.mapper.helpers.ResourceCheckDataAbsentReason;
33import org.miracum.etl.fhirtoomop.mapper.helpers.ResourceFhirReferenceUtils;
34import org.miracum.etl.fhirtoomop.mapper.helpers.ResourceOmopReferenceUtils;
35import org.miracum.etl.fhirtoomop.mapper.helpers.ResourceOnset;
36import org.miracum.etl.fhirtoomop.model.OmopModelWrapper;
37import org.miracum.etl.fhirtoomop.model.OpsStandardDomainLookup;
38import org.miracum.etl.fhirtoomop.model.omop.Concept;
39import org.miracum.etl.fhirtoomop.model.omop.DeviceExposure;
40import org.miracum.etl.fhirtoomop.model.omop.DrugExposure;
41import org.miracum.etl.fhirtoomop.model.omop.Measurement;
42import org.miracum.etl.fhirtoomop.model.omop.OmopObservation;
43import org.miracum.etl.fhirtoomop.model.omop.ProcedureOccurrence;
44import org.miracum.etl.fhirtoomop.model.omop.SourceToConceptMap;
45import org.miracum.etl.fhirtoomop.repository.service.DeviceExposureMapperServiceImpl;
46import org.miracum.etl.fhirtoomop.repository.service.OmopConceptServiceImpl;
47import org.miracum.etl.fhirtoomop.repository.service.ProcedureMapperServiceImpl;
48import org.springframework.beans.factory.annotation.Autowired;
49import org.springframework.lang.Nullable;
50import org.springframework.stereotype.Component;
52/**
53 * The ProcedureMapper class describes the business logic of transforming a FHIR Procedure resource
54 * to OMOP CDM.
55 *
56 * @author Elisa Henke
57 * @author Yuan Peng
58 */
59@Slf4j
60@Component
61public class ProcedureMapper implements FhirMapper<Procedure> {
63 private static final FhirSystems fhirSystems = new FhirSystems();
65 private final Boolean bulkload;
66 private final DbMappings dbMappings;
67 private final List<String> listOfProcedureVocabularyId =
68 Arrays.asList(SOURCE_VOCABULARY_ID_PROCEDURE_DICOM, VOCABULARY_OPS, VOCABULARY_SNOMED);
70 private static final Counter noStartDateCounter =
71 MapperMetrics.setNoStartDateCounter("stepProcessProcedures");
72 private static final Counter noPersonIdCounter =
73 MapperMetrics.setNoPersonIdCounter("stepProcessProcedures");
74 private static final Counter invalidCodeCounter =
75 MapperMetrics.setInvalidCodeCounter("stepProcessProcedures");
76 private static final Counter noCodeCounter =
77 MapperMetrics.setNoCodeCounter("stepProcessProcedures");
78 private static final Counter noFhirReferenceCounter =
79 MapperMetrics.setNoFhirReferenceCounter("stepProcessProcedures");
80 private static final Counter deletedFhirReferenceCounter =
81 MapperMetrics.setDeletedFhirRessourceCounter("stepProcessProcedures");
83 @Autowired OmopConceptServiceImpl omopConceptService;
84 @Autowired ResourceOmopReferenceUtils omopReferenceUtils;
85 @Autowired ProcedureMapperServiceImpl procedureService;
86 @Autowired DeviceExposureMapperServiceImpl deviceExposureService;
87 @Autowired ResourceFhirReferenceUtils fhirReferenceUtils;
88 @Autowired ResourceCheckDataAbsentReason checkDataAbsentReason;
89 @Autowired FindOmopConcepts findOmopConcepts;
91 /**
92 * Constructor for objects of the class ProcedureMapper.
93 *
94 * @param bulkload parameter which indicates whether the Job should be run as bulk load or
95 * incremental load
96 * @param dbMappings collections for the intermediate storage of data from OMOP CDM in RAM
97 */
98 @Autowired
99 public ProcedureMapper(Boolean bulkload, DbMappings dbMappings) {
100 this.bulkload = bulkload;
101 this.dbMappings = dbMappings;
104 /**
105 * Maps a FHIR Procedure resource to procedure_occurrence table in OMOP CDM.
107 * @param srcProcedure FHIR Procedure resource
108 * @param isDeleted a flag, whether the FHIR resource is deleted in the source
109 * @return OmopModelWrapper cache of newly created OMOP CDM records from the FHIR Procedure
110 * resource
111 */
112 @Override
113 public OmopModelWrapper map(Procedure srcProcedure, boolean isDeleted) {
115 var wrapper = new OmopModelWrapper();
117 var procedureLogicId = fhirReferenceUtils.extractId(srcProcedure);
118 var procedureSourceIdentifier = fhirReferenceUtils.extractResourceFirstIdentifier(srcProcedure);
119 if (Strings.isNullOrEmpty(procedureLogicId)
120 && Strings.isNullOrEmpty(procedureSourceIdentifier)) {
121 log.warn("No [Identifier] or [Id] found. [Procedure] resource is invalid. Skip resource");
122 noFhirReferenceCounter.increment();
123 return null;
126 String procedureId = "";
127 if (!Strings.isNullOrEmpty(procedureLogicId)) {
128 procedureId = srcProcedure.getId();
131 if (bulkload.equals(Boolean.FALSE)) {
132 deleteExistingProcedureEntry(procedureLogicId, procedureSourceIdentifier);
133 if (isDeleted) {
134 deletedFhirReferenceCounter.increment();
135 log.info("Found a deleted [Procedure] resource {}. Deleting from OMOP DB.", procedureId);
136 return null;
140 var statusElement = srcProcedure.getStatusElement();
141 var statusValue = checkDataAbsentReason.getValue(statusElement);
142 if (Strings.isNullOrEmpty(statusValue)
143 || !FHIR_RESOURCE_ACCEPTABLE_EVENT_STATUS_LIST.contains(statusValue)) {
144 log.error(
145 "The [status]: {} of {} is not acceptable for writing into OMOP CDM. Skip resource.",
146 statusValue,
147 procedureId);
148 return null;
151 var personId = getPersonId(srcProcedure, procedureLogicId, procedureId);
152 if (personId == null) {
153 log.warn("No matching [Person] found for [Procedure]: {}. Skip resource", procedureId);
154 noPersonIdCounter.increment();
155 return null;
158 var procedureCodings = getProcedureCodings(srcProcedure, procedureLogicId);
159 if (procedureCodings.isEmpty()) {
160 log.warn("No [Code] found in [Procedure]: {}. Skip resource", procedureId);
161 noCodeCounter.increment();
162 return null;
165 var procedureOnset = getProcedureOnset(srcProcedure);
166 if (procedureOnset.getStartDateTime() == null) {
167 log.warn(
168 "Unable to determine [Performed DateTime] for [Procedure]: {}. Skip resource",
169 procedureId);
170 noStartDateCounter.increment();
171 return null;
174 var visitOccId = getVisitOccId(srcProcedure, personId, procedureId);
176 createProcedureMapping(
177 wrapper,
178 procedureCodings,
179 procedureOnset.getStartDateTime(),
180 personId,
181 visitOccId,
182 procedureLogicId,
183 procedureSourceIdentifier,
184 srcProcedure,
185 procedureId);
187 var usedCodesCodeableConcepts = srcProcedure.getUsedCode();
188 if (!usedCodesCodeableConcepts.isEmpty()) {
189 var deviceExposures =
190 createDeviceExposure(
191 personId,
192 visitOccId,
193 procedureOnset,
194 procedureLogicId,
195 procedureSourceIdentifier,
196 usedCodesCodeableConcepts,
197 procedureId);
198 wrapper.setDeviceExposure(deviceExposures);
201 return wrapper;
204 private List<Coding> extractDeviceCode(List<CodeableConcept> usedCodesCodeableConcepts) {
206 List<Coding> devicesCodes = new ArrayList<>();
207 for (var usedCodesCodeableConcept : usedCodesCodeableConcepts) {
208 var usedCodesCodings = usedCodesCodeableConcept.getCoding();
209 if (usedCodesCodings.isEmpty()) {
211 continue;
213 usedCodesCodings.forEach(devicesCodes::add);
215 return devicesCodes;
218 private List<DeviceExposure> createDeviceExposure(
219 Long personId,
220 Long visitOccId,
221 ResourceOnset procedureOnset,
222 String procedureLogicId,
223 String procedureSourceIdentifier,
224 List<CodeableConcept> usedCodesCodeableConcepts,
225 String procedureId) {
226 var deviceCodings = extractDeviceCode(usedCodesCodeableConcepts);
227 if (deviceCodings.isEmpty()) {
229 return Collections.emptyList();
231 List<DeviceExposure> deviceExposures = new ArrayList<>();
232 for (var deviceCoding : deviceCodings) {
233 var deviceCode = checkDataAbsentReason.getValue(deviceCoding.getCodeElement());
234 if (Strings.isNullOrEmpty(deviceCode)) {
235 continue;
238 var startDateTime = procedureOnset.getStartDateTime();
239 var endDateTime = procedureOnset.getEndDateTime();
240 var deviceConcept =
241 findOmopConcepts.getConcepts(
242 deviceCoding, startDateTime.toLocalDate(), bulkload, dbMappings, procedureId);
243 if (deviceConcept == null) {
244 continue;
246 var deviceExposure =
247 DeviceExposure.builder()
248 .personId(personId)
249 .visitOccurrenceId(visitOccId)
250 .fhirIdentifier(procedureSourceIdentifier)
251 .fhirLogicalId(procedureLogicId)
252 .deviceExposureStartDate(startDateTime.toLocalDate())
253 .deviceExposureStartDatetime(startDateTime)
254 .deviceExposureEndDate(endDateTime == null ? null : endDateTime.toLocalDate())
255 .deviceExposureEndDatetime(endDateTime)
256 .deviceTypeConceptId(CONCEPT_EHR)
257 .deviceConceptId(deviceConcept.getConceptId())
258 .deviceSourceConceptId(deviceConcept.getConceptId())
259 .deviceSourceValue(deviceCode)
260 .build();
262 addToList(deviceExposures, deviceExposure);
264 return deviceExposures;
267 private void addToList(List<DeviceExposure> deviceExposures, DeviceExposure deviceExposure) {
268 if (deviceExposures.isEmpty()) {
269 deviceExposures.add(deviceExposure);
270 return;
272 if (deviceExposures.contains(deviceExposure)) {
273 return;
275 deviceExposures.add(deviceExposure);
278 /**
279 * Creates a new record of the procedure_occurrence table in OMOP CDM for the processed FHIR
280 * Procedure resource.
282 * @param procedureCodings a list of Coding elements from Procedure FHIR resource
283 * @param procedureStartDatetime date time of the FHIR Procedure resource
284 * @param personId person_id of the referenced FHIR Patient resource
285 * @param visitOccId visit_occurrence_id of the referenced FHIR Encounter resource
286 * @param procedureLogicId logical id of the FHIR Procedure resource
287 * @param procedureSourceIdentifier identifier of the FHIR Procedure resource
288 * @param bodySite anatomical body site information from procedure FHIR resource
289 */
290 private void createProcedureMapping(
291 OmopModelWrapper wrapper,
292 List<Coding> procedureCodings,
293 LocalDateTime procedureStartDatetime,
294 Long personId,
295 Long visitOccId,
296 String procedureLogicId,
297 String procedureSourceIdentifier,
298 Procedure srcProcedure,
299 String procedureId) {
301 var codingSize = procedureCodings.size();
302 if (codingSize == 1) {
303 setProcedureConceptsUsingSingleCoding(
304 wrapper,
305 procedureStartDatetime,
306 procedureCodings.get(0),
307 srcProcedure,
308 personId,
309 visitOccId,
310 procedureLogicId,
311 procedureSourceIdentifier,
312 procedureId);
313 } else {
314 setProcedureConceptsUsingMultipleCodings(
315 wrapper,
316 procedureStartDatetime,
317 procedureCodings,
318 srcProcedure,
319 personId,
320 visitOccId,
321 procedureLogicId,
322 procedureSourceIdentifier,
323 procedureId);
327 /**
328 * @param procedureStartDatetime
329 * @param personId
330 * @param visitOccId
331 * @param procedureLogicId
332 * @param procedureSourceIdentifier
333 * @param procedureCoding
334 * @return
335 */
336 private void setProcedureConceptsUsingSingleCoding(
337 OmopModelWrapper wrapper,
338 LocalDateTime procedureStartDatetime,
339 Coding procedureCoding,
340 Procedure srcProcedure,
341 Long personId,
342 Long visitOccId,
343 String procedureLogicId,
344 String procedureSourceIdentifier,
345 String procedureId) {
347 List<Pair<String, List<OpsStandardDomainLookup>>> opsStandardMapPairList = null;
348 SourceToConceptMap dicomConcept = null;
349 Concept snomedConcept = null;
351 var procedureCodeExist =
352 checkIfAnyProcedureCodesExist(procedureCoding, listOfProcedureVocabularyId);
353 if (!procedureCodeExist) {
354 return;
357 var procedureVocabularyId = findOmopConcepts.getOmopVocabularyId(procedureCoding.getSystem());
359 var procedureBodySiteLocalization =
360 getBodySiteLocalization(
361 srcProcedure, procedureCoding, procedureStartDatetime.toLocalDate(), procedureId);
363 if (procedureVocabularyId.equals(VOCABULARY_OPS)) {
364 // for OPS codes
366 opsStandardMapPairList =
367 getValidOpsCodes(procedureCoding, procedureStartDatetime.toLocalDate(), procedureId);
369 if (opsStandardMapPairList.isEmpty()) {
370 return;
372 for (var singlePair : opsStandardMapPairList) {
373 procedureProcessor(
374 singlePair,
375 null,
376 null,
377 wrapper,
378 procedureBodySiteLocalization,
379 procedureStartDatetime,
380 procedureLogicId,
381 procedureSourceIdentifier,
382 personId,
383 visitOccId,
384 procedureId);
387 } else if (procedureVocabularyId.equals(SOURCE_VOCABULARY_ID_PROCEDURE_DICOM)) {
388 // for DICOM codes
390 dicomConcept =
391 findOmopConcepts.getCustomConcepts(
392 procedureCoding.getCode(), procedureVocabularyId, dbMappings);
393 if (dicomConcept == null) {
394 return;
397 procedureProcessor(
398 null,
399 null,
400 dicomConcept,
401 wrapper,
402 procedureBodySiteLocalization,
403 procedureStartDatetime,
404 procedureLogicId,
405 procedureSourceIdentifier,
406 personId,
407 visitOccId,
408 procedureId);
410 } else if (procedureVocabularyId.equals(VOCABULARY_SNOMED)) {
411 // for SNOMED codes
413 snomedConcept =
414 findOmopConcepts.getConcepts(
415 procedureCoding,
416 procedureStartDatetime.toLocalDate(),
417 bulkload,
418 dbMappings,
419 procedureId);
421 if (snomedConcept == null) {
422 return;
425 procedureProcessor(
426 null,
427 snomedConcept,
428 null,
429 wrapper,
430 procedureBodySiteLocalization,
431 procedureStartDatetime,
432 procedureLogicId,
433 procedureSourceIdentifier,
434 personId,
435 visitOccId,
436 procedureId);
440 /**
441 * @param procedureStartDatetime
442 * @param personId
443 * @param visitOccId
444 * @param procedureLogicId
445 * @param procedureSourceIdentifier
446 * @param opsCoding
447 * @param snomedCoding
448 * @return
449 */
450 private void setProcedureConceptsUsingMultipleCodings(
451 OmopModelWrapper wrapper,
452 LocalDateTime procedureStartDatetime,
453 List<Coding> procedureCodings,
454 Procedure srcProcedure,
455 Long personId,
456 Long visitOccId,
457 String procedureLogicId,
458 String procedureSourceIdentifier,
459 String procedureId) {
461 Coding uncheckedOpsCoding = null;
462 Coding uncheckedDicomCoding = null;
463 Coding uncheckedSnomedCoding = null;
464 Coding procedureCoding = null;
466 for (var uncheckedCoding : procedureCodings) {
467 var procedureVocabularyId = findOmopConcepts.getOmopVocabularyId(uncheckedCoding.getSystem());
468 if (procedureVocabularyId.equals(VOCABULARY_OPS)) {
469 uncheckedOpsCoding = uncheckedCoding;
471 if (procedureVocabularyId.equals(SOURCE_VOCABULARY_ID_PROCEDURE_DICOM)) {
472 uncheckedDicomCoding = uncheckedCoding;
474 if (procedureVocabularyId.equals(VOCABULARY_SNOMED)) {
475 uncheckedSnomedCoding = uncheckedCoding;
478 if (uncheckedOpsCoding == null
479 && uncheckedDicomCoding == null
480 && uncheckedSnomedCoding == null) {
481 return;
484 // OPS
485 var opsStandardMapPairList =
486 getValidOpsCodes(uncheckedOpsCoding, procedureStartDatetime.toLocalDate(), procedureId);
488 // DICOM
489 SourceToConceptMap dicomConcept = null;
490 if (uncheckedDicomCoding != null) {
491 dicomConcept =
492 findOmopConcepts.getCustomConcepts(
493 uncheckedDicomCoding.getCode(),
494 findOmopConcepts.getOmopVocabularyId(uncheckedDicomCoding.getSystem()),
495 dbMappings);
497 // SNOMED
498 var snomedConcept =
499 findOmopConcepts.getConcepts(
500 uncheckedSnomedCoding,
501 procedureStartDatetime.toLocalDate(),
502 bulkload,
503 dbMappings,
504 procedureId);
506 if (opsStandardMapPairList.isEmpty() && snomedConcept == null && dicomConcept == null) {
507 return;
508 } else if (!opsStandardMapPairList.isEmpty()) {
509 // OPS
510 procedureCoding = uncheckedOpsCoding;
511 } else if (dicomConcept != null) {
512 // DICOM
513 procedureCoding = uncheckedDicomCoding;
514 } else if (snomedConcept != null) {
515 // SNOMED
516 procedureCoding = uncheckedSnomedCoding;
519 setProcedureConceptsUsingSingleCoding(
520 wrapper,
521 procedureStartDatetime,
522 procedureCoding,
523 srcProcedure,
524 personId,
525 visitOccId,
526 procedureLogicId,
527 procedureSourceIdentifier,
528 procedureId);
531 /**
532 * Processes information from FHIR Procedure resource and transforms them into records OMOP CDM
533 * tables.
535 * @param singlePair one pair of OPS code and its OMOP standard concept_id and domain information
536 * @param omopConcept extracted Concept from OMOP
537 * @param dicomConcept source_to_concept_map entry for DICOM code
538 * @param wrapper the OMOP model wrapper
539 * @param procedureDate date of the FHIR Procedure resource
540 * @param procedureLogicId logical id of the FHIR Procedure resource
541 * @param procedureSourceIdentifier identifier of the FHIR Procedure resource
542 * @param personId person_id of the referenced FHIR Patient resource
543 * @param visiOccId visit_occurrence_id of the referenced FHIR Encounter resource
544 */
545 private void procedureProcessor(
546 @Nullable Pair<String, List<OpsStandardDomainLookup>> opsStandardPair,
547 @Nullable Concept snomedConcept,
548 @Nullable SourceToConceptMap dicomConcept,
549 OmopModelWrapper wrapper,
550 Pair<String, Integer> procedureBodySiteLocalization,
551 LocalDateTime procedureStartDatetime,
552 String procedureLogicId,
553 String procedureSourceIdentifier,
554 Long personId,
555 Long visitOccId,
556 String procedureId) {
558 if (opsStandardPair == null && snomedConcept == null && dicomConcept == null) {
559 return;
562 if (opsStandardPair != null) {
563 var opsCode = opsStandardPair.getLeft();
564 var opsStandardMaps = opsStandardPair.getRight();
566 for (var opsStandardMap : opsStandardMaps) {
567 setProcedure(
568 wrapper,
569 procedureBodySiteLocalization,
570 procedureStartDatetime,
571 procedureLogicId,
572 procedureSourceIdentifier,
573 personId,
574 visitOccId,
575 opsCode,
576 opsStandardMap.getStandardConceptId(),
577 opsStandardMap.getSourceConceptId(),
578 opsStandardMap.getStandardDomainId(),
579 procedureId);
582 } else if (dicomConcept != null) {
583 setProcedure(
584 wrapper,
585 procedureBodySiteLocalization,
586 procedureStartDatetime,
587 procedureLogicId,
588 procedureSourceIdentifier,
589 personId,
590 visitOccId,
591 dicomConcept.getSourceCode(),
592 dicomConcept.getTargetConceptId(),
593 dicomConcept.getTargetConceptId(),
594 OMOP_DOMAIN_PROCEDURE,
595 procedureId);
597 } else {
598 setProcedure(
599 wrapper,
600 procedureBodySiteLocalization,
601 procedureStartDatetime,
602 procedureLogicId,
603 procedureSourceIdentifier,
604 personId,
605 visitOccId,
606 snomedConcept.getConceptCode(),
607 snomedConcept.getConceptId(),
608 snomedConcept.getConceptId(),
609 snomedConcept.getDomainId(),
610 procedureId);
614 /** Write procedure information into correct OMOP tables based on their domains. */
615 private void setProcedure(
616 OmopModelWrapper wrapper,
617 Pair<String, Integer> procedureBodySiteLocalization,
618 LocalDateTime procedureStartDatetime,
619 String procedureLogicId,
620 String procedureSourceIdentifier,
621 Long personId,
622 Long visitOccId,
623 String procedureCode,
624 Integer procedureConceptId,
625 Integer procedureSourceConceptId,
626 String domain,
627 String procedureId) {
628 switch (domain) {
629 case OMOP_DOMAIN_PROCEDURE:
630 var procedure =
631 setUpProcedure(
632 procedureStartDatetime,
633 procedureConceptId,
634 procedureSourceConceptId,
635 procedureCode,
636 personId,
637 visitOccId,
638 procedureBodySiteLocalization,
639 procedureLogicId,
640 procedureSourceIdentifier);
642 wrapper.getProcedureOccurrence().add(procedure);
644 break;
645 case OMOP_DOMAIN_OBSERVATION:
646 var observation =
647 setUpObservation(
648 procedureStartDatetime,
649 procedureConceptId,
650 procedureSourceConceptId,
651 procedureCode,
652 personId,
653 visitOccId,
654 procedureBodySiteLocalization,
655 procedureLogicId,
656 procedureSourceIdentifier);
658 wrapper.getObservation().add(observation);
660 break;
661 case OMOP_DOMAIN_DRUG:
662 var drug =
663 setUpDrug(
664 procedureStartDatetime,
665 procedureConceptId,
666 procedureSourceConceptId,
667 procedureCode,
668 personId,
669 visitOccId,
670 procedureLogicId,
671 procedureSourceIdentifier);
673 wrapper.getDrugExposure().add(drug);
675 break;
676 case OMOP_DOMAIN_MEASUREMENT:
677 var measurement =
678 setUpMeasurement(
679 procedureStartDatetime,
680 procedureConceptId,
681 procedureSourceConceptId,
682 procedureCode,
683 personId,
684 visitOccId,
685 procedureBodySiteLocalization,
686 procedureLogicId,
687 procedureSourceIdentifier);
689 wrapper.getMeasurement().add(measurement);
691 break;
692 default:
693 // throw new UnsupportedOperationException(String.format("Unsupported domain %s",
694 // domain));
695 log.error(
696 "[Unsupported domain] {} of code in [Procedure]: {}. Skip resource.",
697 domain,
698 procedureId);
699 break;
703 private ProcedureOccurrence setUpProcedure(
704 LocalDateTime procedureStartDatetime,
705 Integer procedureConceptId,
706 Integer procedureSourceConceptId,
707 String procedureCode,
708 Long personId,
709 Long visitOccId,
710 Pair<String, Integer> procedureBodySiteLocalization,
711 String procedureLogicId,
712 String procedureSourceIdentifier) {
714 var newProcedureOccurrence =
715 ProcedureOccurrence.builder()
716 .personId(personId)
717 .procedureDate(procedureStartDatetime.toLocalDate())
718 .procedureDatetime(procedureStartDatetime)
719 .visitOccurrenceId(visitOccId)
720 .procedureSourceConceptId(procedureSourceConceptId)
721 .procedureConceptId(procedureConceptId)
722 .procedureTypeConceptId(CONCEPT_EHR)
723 .procedureSourceValue(procedureCode)
724 .fhirLogicalId(procedureLogicId)
725 .fhirIdentifier(procedureSourceIdentifier)
726 .build();
728 if (procedureBodySiteLocalization != null) {
729 newProcedureOccurrence.setModifierSourceValue(procedureBodySiteLocalization.getLeft());
730 newProcedureOccurrence.setModifierConceptId(procedureBodySiteLocalization.getRight());
733 return newProcedureOccurrence;
736 private OmopObservation setUpObservation(
737 LocalDateTime procedureStartDatetime,
738 Integer procedureConceptId,
739 Integer procedureSourceConceptId,
740 String procedureCode,
741 Long personId,
742 Long visitOccId,
743 Pair<String, Integer> procedureBodySiteLocalization,
744 String procedureLogicId,
745 String procedureSourceIdentifier) {
747 var newObservation =
748 OmopObservation.builder()
749 .personId(personId)
750 .observationDate(procedureStartDatetime.toLocalDate())
751 .observationDatetime(procedureStartDatetime)
752 .visitOccurrenceId(visitOccId)
753 .observationSourceConceptId(procedureSourceConceptId)
754 .observationConceptId(procedureConceptId)
755 .observationTypeConceptId(CONCEPT_EHR)
756 .observationSourceValue(procedureCode)
757 .fhirLogicalId(procedureLogicId)
758 .fhirIdentifier(procedureSourceIdentifier)
759 .build();
761 if (procedureBodySiteLocalization != null) {
762 newObservation.setQualifierSourceValue(procedureBodySiteLocalization.getLeft());
763 newObservation.setQualifierConceptId(procedureBodySiteLocalization.getRight());
766 return newObservation;
769 private Measurement setUpMeasurement(
770 LocalDateTime procedureStartDatetime,
771 Integer procedureConceptId,
772 Integer procedureSourceConceptId,
773 String procedureCode,
774 Long personId,
775 Long visitOccId,
776 Pair<String, Integer> procedureBodySiteLocalization,
777 String procedureLogicId,
778 String procedureSourceIdentifier) {
780 var newMeasurement =
781 Measurement.builder()
782 .personId(personId)
783 .measurementDate(procedureStartDatetime.toLocalDate())
784 .measurementDatetime(procedureStartDatetime)
785 .visitOccurrenceId(visitOccId)
786 .measurementSourceConceptId(procedureSourceConceptId)
787 .measurementConceptId(procedureConceptId)
788 .measurementTypeConceptId(CONCEPT_EHR)
789 .measurementSourceValue(procedureCode)
790 .fhirLogicalId(procedureLogicId)
791 .fhirIdentifier(procedureSourceIdentifier)
792 .build();
794 if (procedureBodySiteLocalization != null) {
795 newMeasurement.setValueSourceValue(procedureBodySiteLocalization.getLeft());
796 newMeasurement.setValueAsConceptId(procedureBodySiteLocalization.getRight());
799 return newMeasurement;
802 private DrugExposure setUpDrug(
803 LocalDateTime procedureStartDatetime,
804 Integer procedureConceptId,
805 Integer procedureSourceConceptId,
806 String procedureCode,
807 Long personId,
808 Long visitOccId,
809 String procedureLogicId,
810 String procedureSourceIdentifier) {
812 return DrugExposure.builder()
813 .personId(personId)
814 .drugExposureStartDate(procedureStartDatetime.toLocalDate())
815 .drugExposureStartDatetime(procedureStartDatetime)
816 .drugExposureEndDate(procedureStartDatetime.toLocalDate())
817 .visitOccurrenceId(visitOccId)
818 .drugSourceConceptId(procedureSourceConceptId)
819 .drugConceptId(procedureConceptId)
820 .drugTypeConceptId(CONCEPT_EHR)
821 .drugSourceValue(procedureCode)
822 .fhirLogicalId(procedureLogicId)
823 .fhirIdentifier(procedureSourceIdentifier)
824 .build();
827 /**
828 * Extract valid pairs of OPS code and its OMOP concept_id and domain information as a list
830 * @param opsCoding
831 * @param procedureDate the date of procedure
832 * @return a list of valid pairs of OPS code and its OMOP concept_id and domain information
833 */
834 private List<Pair<String, List<OpsStandardDomainLookup>>> getValidOpsCodes(
835 Coding opsCoding, LocalDate procedureDate, String procedureId) {
836 if (opsCoding == null) {
837 return Collections.emptyList();
840 List<Pair<String, List<OpsStandardDomainLookup>>> validOpsStandardConceptMaps =
841 new ArrayList<>();
842 List<OpsStandardDomainLookup> opsStandardMap =
843 findOmopConcepts.getOpsStandardConcepts(
844 opsCoding, procedureDate, bulkload, dbMappings, procedureId);
845 if (opsStandardMap.isEmpty()) {
846 return Collections.emptyList();
849 validOpsStandardConceptMaps.add(Pair.of(opsCoding.getCode(), opsStandardMap));
851 return validOpsStandardConceptMaps;
854 /**
855 * Set procedure_occurrence modifier information.
857 * @param procedureOccurrence the new record of the procedure_occurrence
858 * @param procedureLocalization Localization coding from the FHIR Procedure resource
859 * @param procedureEffective date time of the FHIR Procedure resource
860 */
861 public void setProcedureModifier(
862 ProcedureOccurrence procedureOccurrence,
863 Pair<String, Integer> procedureBodySiteLocalization) {
865 if (procedureBodySiteLocalization == null) {
866 return;
868 procedureOccurrence.setModifierSourceValue(procedureBodySiteLocalization.getLeft());
869 procedureOccurrence.setModifierConceptId(procedureBodySiteLocalization.getRight());
872 /**
873 * Returns the person_id of the referenced FHIR Patient resource for the processed FHIR Procedure
874 * resource.
876 * @param srcProcedure FHIR Procedure resource
877 * @param procedureLogicId logical id of the FHIR Procedure resource
878 * @return person_id of the referenced FHIR Patient resource from person table in OMOP CDM
879 */
880 private Long getPersonId(Procedure srcProcedure, String procedureLogicId, String procedureId) {
881 var patientReferenceIdentifier = fhirReferenceUtils.getSubjectReferenceIdentifier(srcProcedure);
882 var patientReferenceLogicalId = fhirReferenceUtils.getSubjectReferenceLogicalId(srcProcedure);
883 return omopReferenceUtils.getPersonId(
884 patientReferenceIdentifier, patientReferenceLogicalId, procedureLogicId, procedureId);
887 /**
888 * Returns the visit_occurrence_id of the referenced FHIR Encounter resource for the processed
889 * FHIR Procedure resource.
891 * @param srcProcedure FHIR Procedure resource
892 * @param personId person_id of the referenced FHIR Patient resource
893 * @param procedureLogicId logical id of the FHIR Procedure resource
894 * @return visit_occurrence_id of the referenced FHIR Encounter resource from visit_occurrence
895 * table in OMOP CDM
896 */
897 private Long getVisitOccId(Procedure srcProcedure, Long personId, String procedureId) {
898 var encounterReferenceIdentifier =
899 fhirReferenceUtils.getEncounterReferenceIdentifier(srcProcedure);
900 var encounterReferenceLogicalId =
901 fhirReferenceUtils.getEncounterReferenceLogicalId(srcProcedure);
903 var visitOccId =
904 omopReferenceUtils.getVisitOccId(
905 encounterReferenceIdentifier, encounterReferenceLogicalId, personId, procedureId);
907 if (visitOccId == null) {
908 log.debug("No matching [Encounter] found for [Procedure]: {}.", procedureId);
911 return visitOccId;
914 /**
915 * Extracts date time information from the FHIR Procedure resource.
917 * @param srcProcedure FHIR Procedure resource
918 * @return date time of the FHIR Procedure resource
919 */
920 private ResourceOnset getProcedureOnset(Procedure srcProcedure) {
921 var resourceOnset = new ResourceOnset();
923 if (srcProcedure.hasPerformedDateTimeType()
924 && !srcProcedure.getPerformedDateTimeType().isEmpty()) {
926 var performedDateTimeType = srcProcedure.getPerformedDateTimeType();
927 var performedDateTime = checkDataAbsentReason.getValue(performedDateTimeType);
928 if (performedDateTime != null) {
929 resourceOnset.setStartDateTime(performedDateTime);
930 return resourceOnset;
934 if (srcProcedure.hasPerformedPeriod() && !srcProcedure.getPerformedPeriod().isEmpty()) {
935 var performedPeriodElement = srcProcedure.getPerformedPeriod();
937 var performedPeriod = checkDataAbsentReason.getValue(performedPeriodElement);
938 if (performedPeriod == null) {
939 return resourceOnset;
942 if (!performedPeriod.getStartElement().isEmpty()) {
943 resourceOnset.setStartDateTime(
944 new Timestamp(performedPeriod.getStartElement().getValue().getTime())
945 .toLocalDateTime());
947 if (!performedPeriod.getEndElement().isEmpty()) {
948 resourceOnset.setEndDateTime(
949 new Timestamp(performedPeriod.getEndElement().getValue().getTime()).toLocalDateTime());
952 return resourceOnset;
955 /**
956 * Extracts the procedure codings from the FHIR Procedure resource as a list.
958 * @param srcProcedure FHIR Procedure resource
959 * @param procedureLogicId logical id of the FHIR Procedure resource
960 * @return a list of procedure codings from the FHIR Procedure resource
961 */
962 private List<Coding> getProcedureCodings(Procedure srcProcedure, String procedureLogicId) {
963 List<Coding> codingList = new ArrayList<>();
964 var procedureCodings = srcProcedure.getCode().getCoding();
965 if (procedureCodings.size() == 1) {
966 var procedureCoding = procedureCodings.get(0);
967 if (checkIfCodeExist(procedureCoding)) {
968 codingList.add(procedureCodings.get(0));
969 return codingList;
971 return Collections.emptyList();
974 if (procedureCodings.size() > 1) {
975 for (var coding : procedureCodings) {
976 if (checkIfCodeExist(coding)) {
977 codingList.add(coding);
980 return codingList;
982 return Collections.emptyList();
985 /**
986 * Extract Coding from Procedure FHIR resource based on the vocabulary ID in OMOP.
988 * @param procedureCodings a list of Coding elements from Procedure FHIR resource
989 * @param vocabularyId vocabulary Id in OMOP based on the used system URL in Coding
990 * @return a Coding from Procedure FHIR
991 */
992 private Coding getCoding(List<Coding> procedureCodings, String vocabularyId) {
993 var codingOptional =
994 procedureCodings.stream()
995 .filter(
996 procedureCoding ->
997 findOmopConcepts
998 .getOmopVocabularyId(procedureCoding.getSystem())
999 .equals(vocabularyId))
1000 .findFirst();
1001 if (codingOptional.isPresent()) {
1002 return codingOptional.get();
1004 return null;
1007 private Pair<String, Integer> getBodySiteLocalization(
1008 Procedure srcProcedure, Coding procedureCoding, LocalDate procedureDate, String procedureId) {
1009 var siteLocalization = getOpsSiteLocalization(procedureCoding);
1010 var procedureBodySite = getBodySite(srcProcedure);
1011 if (siteLocalization == null && procedureBodySite == null) {
1012 return null;
1013 } else if (siteLocalization != null) {
1014 var opsSiteLocalizationConcept =
1015 findOmopConcepts.getCustomConcepts(
1016 siteLocalization.getCode(), SOURCE_VOCABULARY_ID_PROCEDURE_BODYSITE, dbMappings);
1017 return Pair.of(siteLocalization.getCode(), opsSiteLocalizationConcept.getTargetConceptId());
1019 var procedureBodySiteConcept =
1020 findOmopConcepts.getConcepts(
1021 procedureBodySite, procedureDate, bulkload, dbMappings, procedureId);
1022 if (procedureBodySiteConcept == null) {
1023 return null;
1025 return Pair.of(procedureBodySite.getCode(), procedureBodySiteConcept.getConceptId());
1028 /**
1029 * Extracts OPS site localization information from the FHIR Procedure resource.
1031 * @param opsCoding OPS coding from the FHIR Procedure resource
1032 * @return OPS site localization from FHIR Procedure resource
1033 */
1034 private Coding getOpsSiteLocalization(Coding opsCoding) {
1035 if (opsCoding == null) {
1036 return null;
1038 var opsSiteLocalizationExtension =
1039 opsCoding.getExtensionByUrl(fhirSystems.getSiteLocalizationExtension());
1040 if (opsSiteLocalizationExtension == null) {
1041 return null;
1043 var opsSiteLocalizationType = opsSiteLocalizationExtension.getValue();
1044 if (opsSiteLocalizationType == null) {
1045 return null;
1048 var opsSiteLocalizationCoding = opsSiteLocalizationType.castToCoding(opsSiteLocalizationType);
1049 var opsSiteLocalizationCode = opsSiteLocalizationCoding.getCode();
1050 if (Strings.isNullOrEmpty(opsSiteLocalizationCode)) {
1051 return null;
1053 return opsSiteLocalizationCoding;
1056 /**
1057 * Extracts body site information from the FHIR Procedure resource.
1059 * @param srcProcedure FHIR Procedure resource
1060 * @return body site from FHIR Procedure resource
1061 */
1062 private Coding getBodySite(Procedure srcProcedure) {
1064 if (!srcProcedure.hasBodySite() || srcProcedure.getBodySite().isEmpty()) {
1065 return null;
1068 var bodySiteCodingOptional =
1069 srcProcedure.getBodySite().get(0).getCoding().stream()
1070 .filter(code -> code.getSystem().equalsIgnoreCase(fhirSystems.getSnomed()))
1071 .findAny();
1072 if (!bodySiteCodingOptional.isPresent()) {
1073 return null;
1076 var bodySiteCoding = bodySiteCodingOptional.get();
1077 var bodySiteCode = bodySiteCoding.getCode();
1078 if (Strings.isNullOrEmpty(bodySiteCode)) {
1079 return null;
1081 return bodySiteCoding;
1084 /**
1085 * Check if the used procedure code exists in FHIR Procedure resource
1087 * @param procedureCoding procedure codings from the FHIR Procedure resource
1088 * @return a boolean value
1089 */
1090 private boolean checkIfCodeExist(Coding procedureCoding) {
1091 var codeElement = procedureCoding.getCodeElement();
1092 if (codeElement.isEmpty()) {
1093 return false;
1095 var procedureCode = checkDataAbsentReason.getValue(codeElement);
1096 if (Strings.isNullOrEmpty(procedureCode)) {
1097 return false;
1100 return true;
1103 /**
1104 * Check if the used procedure code exists in OMOP
1106 * @param procedureCoding Coding element from Procedure FHIR resource
1107 * @param vocabularyId vocabulary Id in OMOP based on the used system URL in Coding
1108 * @return a boolean value
1109 */
1110 private boolean checkIfAnyProcedureCodesExist(Coding procedureCoding, List<String> vocabularyId) {
1111 if (procedureCoding == null) {
1112 return false;
1114 var codingVocabularyId = findOmopConcepts.getOmopVocabularyId(procedureCoding.getSystem());
1115 return vocabularyId.contains(codingVocabularyId);
1118 private boolean checkIfSpecificProcedureCodesExist(Coding procedureCoding, String vocabularyId) {
1119 if (procedureCoding == null) {
1120 return false;
1122 var codingVocabularyId = findOmopConcepts.getOmopVocabularyId(procedureCoding.getSystem());
1123 return codingVocabularyId.equals(vocabularyId);
1126 /**
1127 * Deletes FHIR Procedure resources from OMOP CDM tables using fhir_logical_id and fhir_identifier
1129 * @param procedureLogicId logical id of the FHIR Procedure resource
1130 * @param procedureSourceIdentifier identifier of the FHIR Procedure resource
1131 */
1132 private void deleteExistingProcedureEntry(
1133 String procedureLogicId, String procedureSourceIdentifier) {
1134 if (!Strings.isNullOrEmpty(procedureLogicId)) {
1135 procedureService.deleteExistingProceduresByFhirLogicalId(procedureLogicId);
1136 } else {
1137 procedureService.deleteExistingProceduresByFhirIdentifier(procedureSourceIdentifier);