From 71188e5dba617301663c426eb80a20f87019dd68 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 24 Feb 2024 00:04:01 +0000 Subject: [PATCH 01/21] fix(deps): update hapifhirversion to v6.10.5 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index dd74ec8..18866df 100644 --- a/build.gradle +++ b/build.gradle @@ -22,7 +22,7 @@ ext { set('springCloudDepsVersion', "2023.0.0") set('springCloudStreamVersion', "4.1.0") set('springKafkaVersion', "3.1.1") - set("hapiFhirVersion", "6.8.5") + set("hapiFhirVersion", "6.10.5") } dependencies { From aed30b229c8ba603e9acc58866fae9ac710ca3e5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 25 Feb 2024 09:24:12 +0000 Subject: [PATCH 02/21] fix(deps): update dependency io.micrometer:micrometer-registry-prometheus to v1.12.3 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index dd74ec8..b5d1955 100644 --- a/build.gradle +++ b/build.gradle @@ -34,7 +34,7 @@ dependencies { // metrics implementation "org.springframework.boot:spring-boot-starter-web:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion" - implementation 'io.micrometer:micrometer-registry-prometheus:1.12.1' + implementation 'io.micrometer:micrometer-registry-prometheus:1.12.3' // hapi fhir implementation "ca.uhn.hapi.fhir:hapi-fhir-base:$hapiFhirVersion" From 47eabfb06345df3cbb70fb4f0cae20a0bc76272d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20St=C3=B6cker?= Date: Thu, 7 Mar 2024 19:06:51 +0100 Subject: [PATCH 03/21] feat!: add mapping update processor --- build.gradle | 7 +- dev/compose-data.yaml | 2 + dev/compose.yaml | 2 + .../unimarburg/diz/labtofhir/LabRunner.java | 161 ++++++++++++++++++ .../diz/labtofhir/LabToFhirApplication.java | 2 + .../diz/labtofhir/UpdateCompleted.java | 10 ++ .../configuration/FhirProperties.java | 1 - .../configuration/KafkaConfiguration.java | 118 +++++++++++-- .../configuration/MappingConfiguration.java | 152 ++++++++++++++++- .../configuration/MappingProperties.java | 73 ++++++++ .../diz/labtofhir/mapper/LoincMapper.java | 18 +- .../diz/labtofhir/model/LabOffsets.java | 9 + .../diz/labtofhir/model/LoincMap.java | 42 +++++ .../diz/labtofhir/model/LoincMapEntry.java | 19 +++ .../diz/labtofhir/model/MappingInfo.java | 5 + .../diz/labtofhir/model/MappingUpdate.java | 70 ++++++++ .../processor/LabUpdateProcessor.java | 125 ++++++++++++++ src/main/resources/application.yml | 18 +- .../labtofhir/mapper/LoincMapperTests.java | 1 - .../diz/labtofhir/model/LoincMapTests.java | 33 ++++ .../diz/labtofhir/serde/JsonSerdes.java | 6 +- 21 files changed, 846 insertions(+), 28 deletions(-) create mode 100644 src/main/java/de/unimarburg/diz/labtofhir/LabRunner.java create mode 100644 src/main/java/de/unimarburg/diz/labtofhir/UpdateCompleted.java create mode 100644 src/main/java/de/unimarburg/diz/labtofhir/configuration/MappingProperties.java create mode 100644 src/main/java/de/unimarburg/diz/labtofhir/model/LabOffsets.java create mode 100644 src/main/java/de/unimarburg/diz/labtofhir/model/MappingInfo.java create mode 100644 src/main/java/de/unimarburg/diz/labtofhir/model/MappingUpdate.java create mode 100644 src/main/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessor.java create mode 100644 src/test/java/de/unimarburg/diz/labtofhir/model/LoincMapTests.java diff --git a/build.gradle b/build.gradle index dd74ec8..26565d1 100644 --- a/build.gradle +++ b/build.gradle @@ -27,10 +27,15 @@ ext { dependencies { // spring cloud stream kafka - implementation "org.springframework.cloud:spring-cloud-stream:$springCloudStreamVersion" + implementation "org.springframework.cloud:spring-cloud-stream-binder-kafka:$springCloudStreamVersion" implementation "org.springframework.cloud:spring-cloud-stream-binder-kafka-streams:$springCloudStreamVersion" + implementation "org.springframework.cloud:spring-cloud-starter-stream-kafka:$springCloudStreamVersion" implementation "org.springframework.kafka:spring-kafka:$springKafkaVersion" + // retry + implementation 'org.springframework.retry:spring-retry:2.0.3' + implementation 'org.springframework:spring-aspects:6.0.11' + // metrics implementation "org.springframework.boot:spring-boot-starter-web:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion" diff --git a/dev/compose-data.yaml b/dev/compose-data.yaml index 83bbf60..ff0e516 100644 --- a/dev/compose-data.yaml +++ b/dev/compose-data.yaml @@ -1,5 +1,7 @@ version: "3.7" +name: lab-to-fhir + services: lab-data-loader: image: confluentinc/cp-kafkacat:7.0.1 diff --git a/dev/compose.yaml b/dev/compose.yaml index a09e94b..52be071 100644 --- a/dev/compose.yaml +++ b/dev/compose.yaml @@ -1,5 +1,7 @@ version: "3.7" +name: lab-to-fhir + services: zoo: image: zookeeper:3.6.1 diff --git a/src/main/java/de/unimarburg/diz/labtofhir/LabRunner.java b/src/main/java/de/unimarburg/diz/labtofhir/LabRunner.java new file mode 100644 index 0000000..a94e804 --- /dev/null +++ b/src/main/java/de/unimarburg/diz/labtofhir/LabRunner.java @@ -0,0 +1,161 @@ +package de.unimarburg.diz.labtofhir; + +import de.unimarburg.diz.labtofhir.model.MappingInfo; +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import javax.annotation.Nullable; +import org.apache.kafka.clients.admin.AdminClient; +import org.apache.kafka.common.TopicPartition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.cloud.stream.binding.BindingsLifecycleController.State; +import org.springframework.cloud.stream.endpoint.BindingsEndpoint; +import org.springframework.context.event.EventListener; +import org.springframework.kafka.core.KafkaAdmin; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryListener; +import org.springframework.retry.annotation.Retryable; +import org.springframework.retry.policy.AlwaysRetryPolicy; +import org.springframework.retry.support.RetryTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +@Service +@Retryable +public class LabRunner implements ApplicationRunner { + + private static final Logger LOG = LoggerFactory.getLogger(LabRunner.class); + private final BindingsEndpoint endpoint; + private final KafkaAdmin kafkaAdmin; + private final MappingInfo mappingInfo; + private final String updateGroup; + private final RetryTemplate retryTemplate; + + + static final long RETRY_BACKOFF_PERIOD = 2_000L; + + @SuppressWarnings("checkstyle:LineLength") + public LabRunner(BindingsEndpoint endpoint, KafkaAdmin kafkaAdmin, + @Nullable MappingInfo mappingInfo, @Value( + "${spring.cloud.stream.kafka.streams.binder.functions.update" + + ".applicationId}") String updateGroup) { + this.endpoint = endpoint; + this.kafkaAdmin = kafkaAdmin; + this.mappingInfo = mappingInfo; + this.updateGroup = updateGroup; + this.retryTemplate = setupRetryTemplate(); + } + + @Override + public void run(ApplicationArguments args) throws Exception { + + if (mappingInfo != null) { + + // reset only on new update version + if (!mappingInfo.resume()) { + // reset update consumer + + try (var client = createAdminClient()) { + var offsets = client + .listConsumerGroupOffsets(updateGroup) + .partitionsToOffsetAndMetadata() + .get(); + + if (!offsets.isEmpty()) { + LOG.info("Starting mapping update from {} to {}", + mappingInfo + .update() + .getOldVersion(), mappingInfo + .update() + .getVersion()); + + // start update at the beginning + // delete consumer group + deleteUpdateConsumerGroup(); + } + } + } + + // start update processor + endpoint.changeState("update-in-0", State.STARTED); + } + + // start regular lab processor + endpoint.changeState("process-in-0", State.STARTED); + } + + void resetUpdateConsumer(AdminClient client, Set offsets) + throws ExecutionException, InterruptedException { + + client + .deleteConsumerGroupOffsets(updateGroup, offsets) + .all() + .get(); + } + + private AdminClient createAdminClient() { + return AdminClient.create(kafkaAdmin.getConfigurationProperties()); + } + + RetryTemplate setupRetryTemplate() { + + return RetryTemplate + .builder() + .customPolicy(new AlwaysRetryPolicy()) + .fixedBackoff(RETRY_BACKOFF_PERIOD) + .withListener(new RetryListener() { + @Override + public void onError( + RetryContext context, RetryCallback callback, + Throwable throwable) { + LOG.debug( + "Delete Consumer group failed: {}. " + "Retrying {}", + throwable + .getCause() + .getMessage(), context.getRetryCount()); + } + }) + .retryOn(ExecutionException.class) + .build(); + } + + public void stopAndDeleteUpdateConsumer() throws Exception { + + // start update processor + endpoint.changeState("update-in-0", State.STOPPED); + + LOG.info("Update consumer stopped"); + + // delete consumer group + LOG.info("Deleting update consumer group..."); + deleteUpdateConsumerGroup(); + } + + + private void deleteUpdateConsumerGroup() throws Exception { + try (var client = createAdminClient()) { + retryTemplate.execute(ctx -> client + .deleteConsumerGroups(Collections.singleton(updateGroup)) + .all() + .get()); + } + } + + + @EventListener + @Async + public void onApplicationEvent(UpdateCompleted event) { + try { + LOG.info("Update process complete"); + stopAndDeleteUpdateConsumer(); + } catch (Exception e) { + LOG.error("stopAndDeleteUpdateConsumer", e); + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/de/unimarburg/diz/labtofhir/LabToFhirApplication.java b/src/main/java/de/unimarburg/diz/labtofhir/LabToFhirApplication.java index 39b67af..35cb032 100644 --- a/src/main/java/de/unimarburg/diz/labtofhir/LabToFhirApplication.java +++ b/src/main/java/de/unimarburg/diz/labtofhir/LabToFhirApplication.java @@ -4,8 +4,10 @@ import java.util.TimeZone; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; @SpringBootApplication +@EnableAsync public class LabToFhirApplication { public static void main(String[] args) { diff --git a/src/main/java/de/unimarburg/diz/labtofhir/UpdateCompleted.java b/src/main/java/de/unimarburg/diz/labtofhir/UpdateCompleted.java new file mode 100644 index 0000000..d33c157 --- /dev/null +++ b/src/main/java/de/unimarburg/diz/labtofhir/UpdateCompleted.java @@ -0,0 +1,10 @@ +package de.unimarburg.diz.labtofhir; + +import org.springframework.context.ApplicationEvent; + +public class UpdateCompleted extends ApplicationEvent { + + public UpdateCompleted(Object source) { + super(source); + } +} diff --git a/src/main/java/de/unimarburg/diz/labtofhir/configuration/FhirProperties.java b/src/main/java/de/unimarburg/diz/labtofhir/configuration/FhirProperties.java index 8b229e4..7dfdbf9 100644 --- a/src/main/java/de/unimarburg/diz/labtofhir/configuration/FhirProperties.java +++ b/src/main/java/de/unimarburg/diz/labtofhir/configuration/FhirProperties.java @@ -4,7 +4,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; - @ConfigurationProperties(prefix = "fhir") @Validated public class FhirProperties { diff --git a/src/main/java/de/unimarburg/diz/labtofhir/configuration/KafkaConfiguration.java b/src/main/java/de/unimarburg/diz/labtofhir/configuration/KafkaConfiguration.java index e9b8048..8e88742 100644 --- a/src/main/java/de/unimarburg/diz/labtofhir/configuration/KafkaConfiguration.java +++ b/src/main/java/de/unimarburg/diz/labtofhir/configuration/KafkaConfiguration.java @@ -1,9 +1,23 @@ package de.unimarburg.diz.labtofhir.configuration; +import de.unimarburg.diz.labtofhir.model.LabOffsets; +import de.unimarburg.diz.labtofhir.model.MappingUpdate; import de.unimarburg.diz.labtofhir.serializer.FhirSerde; -import java.util.Objects; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; +import org.apache.kafka.clients.admin.AdminClient; +import org.apache.kafka.clients.admin.NewTopic; +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.Producer; +import org.apache.kafka.clients.producer.ProducerConfig; import org.apache.kafka.common.serialization.Serde; -import org.apache.kafka.streams.StreamsConfig; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.apache.kafka.common.serialization.StringSerializer; import org.apache.kafka.streams.errors.StreamsUncaughtExceptionHandler; import org.hl7.fhir.r4.model.Bundle; import org.slf4j.Logger; @@ -13,37 +27,113 @@ import org.springframework.context.annotation.Configuration; import org.springframework.kafka.annotation.EnableKafka; import org.springframework.kafka.config.StreamsBuilderFactoryBeanConfigurer; +import org.springframework.kafka.config.TopicBuilder; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaAdmin; +import org.springframework.kafka.support.serializer.JsonDeserializer; +import org.springframework.kafka.support.serializer.JsonSerializer; +import org.springframework.retry.annotation.EnableRetry; +@SuppressWarnings("checkstyle:LineLength") @Configuration @EnableKafka +@EnableRetry public class KafkaConfiguration { private static final Logger LOG = LoggerFactory.getLogger( KafkaConfiguration.class); + private static final String USE_TYPE_INFO_HEADERS = "spring.cloud.stream.kafka.streams.binder.configuration.spring.json.use.type.headers"; - @SuppressWarnings("checkstyle:LineLength") @Bean - public StreamsBuilderFactoryBeanConfigurer streamsBuilderCustomizer( - @Value("${app.kafka.rocksdb.level-compaction:false}") boolean enableLevelCompaction) { - return factoryBean -> { - factoryBean.setKafkaStreamsCustomizer( + public StreamsBuilderFactoryBeanConfigurer streamsBuilderCustomizer() { + + return fb -> { + fb.setKafkaStreamsCustomizer( kafkaStreams -> kafkaStreams.setUncaughtExceptionHandler(e -> { LOG.error("Uncaught exception occurred.", e); // default handler response return StreamsUncaughtExceptionHandler.StreamThreadExceptionResponse.SHUTDOWN_CLIENT; })); - - if (enableLevelCompaction) { - Objects - .requireNonNull(factoryBean.getStreamsConfiguration()) - .put(StreamsConfig.ROCKSDB_CONFIG_SETTER_CLASS_CONFIG, - RocksDbConfig.class); - } }; } + + @Bean + public LabOffsets getOffsets(KafkaAdmin kafkaAdmin, + @Value("${spring.cloud.stream.kafka.streams.binder.functions.process.applicationId}") String processGroup, + @Value("${spring.cloud.stream.kafka.streams.binder.functions.update.applicationId}") String updateGroup) + throws ExecutionException, InterruptedException { + // get current offsets + try (var client = AdminClient.create( + kafkaAdmin.getConfigurationProperties())) { + var processOffsets = client + .listConsumerGroupOffsets(processGroup) + .partitionsToOffsetAndMetadata() + .get(); + + var updateOffsets = client + .listConsumerGroupOffsets(updateGroup) + .partitionsToOffsetAndMetadata() + .get(); + + return new LabOffsets(processOffsets + .entrySet() + .stream() + .collect(Collectors.toMap(e -> e + .getKey() + .partition(), Map.Entry::getValue)), updateOffsets + .entrySet() + .stream() + .collect(Collectors.toMap(e -> e + .getKey() + .partition(), Map.Entry::getValue))); + } + } + + @Bean + public Producer createUpdateProducer( + DefaultKafkaProducerFactory pf) { + + var props = new HashMap<>(pf.getConfigurationProperties()); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, + StringSerializer.class); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, + JsonSerializer.class); + + return new KafkaProducer<>(props); + } + + @Bean + public NewTopic mappingTopic( + @Value("${spring.cloud.stream.kafka.streams.binder.replicationFactor}") int replicas) { + return TopicBuilder + .name("mapping") + .partitions(1) + .replicas(replicas) + .build(); + } + + @Bean + public Consumer createUpdateConsumer( + DefaultKafkaConsumerFactory cf, + @Value("${" + USE_TYPE_INFO_HEADERS + "}") boolean useTypeHeaders) { + + var props = new HashMap<>(cf.getConfigurationProperties()); + props.put(ConsumerConfig.GROUP_ID_CONFIG, "mapping-update"); + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, + StringDeserializer.class); + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, + JsonDeserializer.class); + props.put(JsonDeserializer.VALUE_DEFAULT_TYPE, MappingUpdate.class); + props.put(JsonDeserializer.USE_TYPE_INFO_HEADERS, useTypeHeaders); + + return new KafkaConsumer<>(props); + } + @Bean public Serde bundleSerde() { return new FhirSerde<>(Bundle.class); } + } diff --git a/src/main/java/de/unimarburg/diz/labtofhir/configuration/MappingConfiguration.java b/src/main/java/de/unimarburg/diz/labtofhir/configuration/MappingConfiguration.java index 346c744..e09274e 100644 --- a/src/main/java/de/unimarburg/diz/labtofhir/configuration/MappingConfiguration.java +++ b/src/main/java/de/unimarburg/diz/labtofhir/configuration/MappingConfiguration.java @@ -1,8 +1,16 @@ package de.unimarburg.diz.labtofhir.configuration; +import de.unimarburg.diz.labtofhir.mapper.LoincMapper; +import de.unimarburg.diz.labtofhir.model.LabOffsets; +import de.unimarburg.diz.labtofhir.model.MappingInfo; +import de.unimarburg.diz.labtofhir.model.MappingUpdate; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.time.Duration; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutionException; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; @@ -10,9 +18,15 @@ import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.clients.producer.Producer; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.TopicPartition; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; @@ -21,13 +35,37 @@ import org.springframework.util.StreamUtils; @Configuration +@EnableConfigurationProperties public class MappingConfiguration { private static final Logger LOG = LoggerFactory.getLogger( MappingConfiguration.class); + private static final long MAX_CONSUMER_POLL_DURATION_SECONDS = 10L; + + @Bean + public MappingProperties mappingProperties() { + return new MappingProperties(); + } @Bean("mappingPackage") - public Resource getMappingFile( + public Resource currentMappingFile(MappingProperties mp) + throws IOException { + return getMappingFile(mp + .getLoinc() + .getVersion(), mp + .getLoinc() + .getCredentials() + .getUser(), mp + .getLoinc() + .getCredentials() + .getPassword(), mp + .getLoinc() + .getProxy(), mp + .getLoinc() + .getLocal()); + } + + private Resource getMappingFile( @Value("${mapping.loinc.version}") String version, @Value("${mapping.loinc.credentials.user}") String user, @Value("${mapping.loinc.credentials.password}") String password, @@ -77,4 +115,116 @@ public Resource getMappingFile( new ClassPathResource(localPkg).getFile()); } } + + @Bean + public LoincMapper createMapper(FhirProperties fhirProperties, + @Qualifier("mappingPackage") Resource mappingPackage) throws Exception { + + return new LoincMapper(fhirProperties, mappingPackage).initialize(); + } + + @Bean("mappingInfo") + public MappingInfo buildMappingUpdate(LoincMapper loincMapper, + MappingProperties mappingProperties, LabOffsets labOffsets, + Consumer consumer, + Producer producer) + throws ExecutionException, InterruptedException, IOException { + // check versions + var configuredVersion = loincMapper + .getMap() + .getMetadata() + .getVersion(); + + // 1. consume latest from (mapping) update topic + var lastUpdate = getLastMappingUpdate(consumer); + if (lastUpdate == null) { + // job runs the first time: save current state + lastUpdate = new MappingUpdate(configuredVersion, null, List.of()); + try { + saveMappingUpdate(producer, lastUpdate); + } catch (InterruptedException | ExecutionException e) { + LOG.error("Failed to save mapping update to topic."); + throw e; + } + + } + + // 2. check if already on latest + if (Objects.equals(configuredVersion, lastUpdate.getVersion())) { + LOG.info("Configured mapping version ({}) matches last version " + + "({}). " + "No update necesssary", configuredVersion, + lastUpdate.getVersion()); + + if (!labOffsets + .updateOffsets() + .isEmpty()) { + // update in progress, continue + return new MappingInfo(lastUpdate, true); + } + + return null; + } + + // 3. calculate diff of mapping versions and save new update + // get last version's mapping + var lastMap = LoincMapper.getSwlLoincMapping( + getMappingFile(lastUpdate.getVersion(), mappingProperties + .getLoinc() + .getCredentials() + .getUser(), mappingProperties + .getLoinc() + .getCredentials() + .getPassword(), mappingProperties + .getLoinc() + .getProxy(), mappingProperties + .getLoinc() + .getLocal())); + // ceate diff + var updates = loincMapper + .getMap() + .diff(lastMap); + var update = new MappingUpdate(configuredVersion, + lastUpdate.getVersion(), updates); + + // save new mapping update + saveMappingUpdate(producer, update); + + return new MappingInfo(update, false); + } + + private void saveMappingUpdate(Producer producer, + MappingUpdate mappingUpdate) + throws ExecutionException, InterruptedException { + + producer + .send(new ProducerRecord<>("mapping", "lab-update", mappingUpdate)) + .get(); + } + + private MappingUpdate getLastMappingUpdate( + Consumer consumer) { + var topic = "mapping"; + + var partition = new TopicPartition(topic, 0); + var partitions = List.of(partition); + + try (consumer) { + consumer.assign(partitions); + consumer.seekToEnd(partitions); + var position = consumer.position(partition); + if (position == 0) { + return null; + } + + consumer.seek(partition, position - 1); + + var record = consumer + .poll(Duration.ofSeconds(MAX_CONSUMER_POLL_DURATION_SECONDS)) + .iterator() + .next(); + + consumer.unsubscribe(); + return record.value(); + } + } } diff --git a/src/main/java/de/unimarburg/diz/labtofhir/configuration/MappingProperties.java b/src/main/java/de/unimarburg/diz/labtofhir/configuration/MappingProperties.java new file mode 100644 index 0000000..2557dc8 --- /dev/null +++ b/src/main/java/de/unimarburg/diz/labtofhir/configuration/MappingProperties.java @@ -0,0 +1,73 @@ +package de.unimarburg.diz.labtofhir.configuration; + +import java.io.Serializable; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "mapping") +public class MappingProperties implements Serializable { + + private final Loinc loinc = new Loinc(); + + public Loinc getLoinc() { + return loinc; + } + + public static class Loinc implements Serializable { + + private final Credentials credentials = new Credentials(); + private String version; + private String proxy; + private String local; + + public Credentials getCredentials() { + return credentials; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getProxy() { + return proxy; + } + + public void setProxy(String proxy) { + this.proxy = proxy; + } + + public String getLocal() { + return local; + } + + public void setLocal(String local) { + this.local = local; + } + + public static class Credentials implements Serializable { + + private String user; + private String password; + + public String getUser() { + return user; + } + + public void setUser(String user) { + this.user = user; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + } + } + +} diff --git a/src/main/java/de/unimarburg/diz/labtofhir/mapper/LoincMapper.java b/src/main/java/de/unimarburg/diz/labtofhir/mapper/LoincMapper.java index 09ac752..56dd78d 100644 --- a/src/main/java/de/unimarburg/diz/labtofhir/mapper/LoincMapper.java +++ b/src/main/java/de/unimarburg/diz/labtofhir/mapper/LoincMapper.java @@ -5,7 +5,6 @@ import de.unimarburg.diz.labtofhir.model.LoincMap; import de.unimarburg.diz.labtofhir.model.LoincMapEntry; import io.micrometer.core.instrument.Metrics; -import jakarta.annotation.PostConstruct; import java.io.IOException; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Observation; @@ -14,9 +13,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.core.io.Resource; -import org.springframework.stereotype.Service; -@Service +//@Service public class LoincMapper { private final FhirProperties fhirProperties; @@ -24,6 +22,11 @@ public class LoincMapper { private static final Logger LOG = LoggerFactory.getLogger( LoincMapper.class); private final TagCounter unmappedCodes; + + public LoincMap getMap() { + return loincMap; + } + private LoincMap loincMap; @Autowired @@ -47,12 +50,17 @@ public LoincMapper(FhirProperties fhirProperties, LoincMap loincMap) { this.mappingPackage = null; } - private LoincMap getSwlLoincMapping(Resource mappingPackage) + public static LoincMap getSwlLoincMapping(Resource mappingPackage) throws IOException { return new LoincMap().with(mappingPackage, ','); } - @PostConstruct + public LoincMapper initialize() throws Exception { + initializeMap(); + return this; + } + + // @PostConstruct private void initializeMap() throws Exception { if (this.mappingPackage != null) { this.loincMap = getSwlLoincMapping(mappingPackage); diff --git a/src/main/java/de/unimarburg/diz/labtofhir/model/LabOffsets.java b/src/main/java/de/unimarburg/diz/labtofhir/model/LabOffsets.java new file mode 100644 index 0000000..478d84b --- /dev/null +++ b/src/main/java/de/unimarburg/diz/labtofhir/model/LabOffsets.java @@ -0,0 +1,9 @@ +package de.unimarburg.diz.labtofhir.model; + +import java.util.Map; +import org.apache.kafka.clients.consumer.OffsetAndMetadata; + +public record LabOffsets(Map processOffsets, + Map updateOffsets) { + +} diff --git a/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMap.java b/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMap.java index 0d174b3..bc622a6 100644 --- a/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMap.java +++ b/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMap.java @@ -14,10 +14,13 @@ import java.io.InputStream; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; +import java.util.stream.Collectors; import java.util.zip.ZipFile; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -27,9 +30,18 @@ public class LoincMap { private final Map> internalMap = new HashMap<>(); + + public Map> getInternalMap() { + return internalMap; + } + private final Logger log = LoggerFactory.getLogger(LoincMap.class); private CsvPackageMetadata metadata; + public CsvPackageMetadata getMetadata() { + return metadata; + } + private LoincMap parsePackage(Resource pkg, char delimiter) throws IOException { @@ -161,4 +173,34 @@ public LoincMap with(Resource pkgResource, char delimiter) throw new IllegalArgumentException("Mapping resource must be a file."); } + public Set diff(LoincMap fromMap) { + + var keys = new HashSet<>(fromMap + .getInternalMap() + .keySet()); + + // remove keys present in current map + keys.removeAll(this + .getInternalMap() + .keySet()); + // add keys of entries that differ + keys.addAll(this + .getInternalMap() + .entrySet() + .stream() + .filter(e -> + // new entry or updated + !fromMap + .getInternalMap() + .containsKey(e.getKey()) || !fromMap + .getInternalMap() + .get(e.getKey()) + .stream() + .collect(Collectors.toUnmodifiableSet()) + .equals(e.getValue())) + .map(Entry::getKey) + .toList()); + + return keys; + } } diff --git a/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMapEntry.java b/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMapEntry.java index ca73401..ab43dde 100644 --- a/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMapEntry.java +++ b/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMapEntry.java @@ -1,6 +1,7 @@ package de.unimarburg.diz.labtofhir.model; import com.fasterxml.jackson.annotation.JsonSetter; +import java.util.Objects; import org.apache.commons.lang3.StringUtils; public class LoincMapEntry { @@ -50,4 +51,22 @@ public LoincMapEntry setUcum(String ucum) { return this; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + LoincMapEntry that = (LoincMapEntry) o; + return Objects.equals(swl, that.swl) && Objects.equals(loinc, + that.loinc) && Objects.equals(ucum, that.ucum) && Objects.equals( + meta, that.meta); + } + + @Override + public int hashCode() { + return Objects.hash(swl, loinc, ucum, meta); + } } diff --git a/src/main/java/de/unimarburg/diz/labtofhir/model/MappingInfo.java b/src/main/java/de/unimarburg/diz/labtofhir/model/MappingInfo.java new file mode 100644 index 0000000..b2f267e --- /dev/null +++ b/src/main/java/de/unimarburg/diz/labtofhir/model/MappingInfo.java @@ -0,0 +1,5 @@ +package de.unimarburg.diz.labtofhir.model; + +public record MappingInfo(MappingUpdate update, boolean resume) { + +} diff --git a/src/main/java/de/unimarburg/diz/labtofhir/model/MappingUpdate.java b/src/main/java/de/unimarburg/diz/labtofhir/model/MappingUpdate.java new file mode 100644 index 0000000..f55dbf3 --- /dev/null +++ b/src/main/java/de/unimarburg/diz/labtofhir/model/MappingUpdate.java @@ -0,0 +1,70 @@ +package de.unimarburg.diz.labtofhir.model; + +import com.fasterxml.jackson.annotation.JsonSetter; +import java.io.Serializable; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +public class MappingUpdate implements Serializable { + + public MappingUpdate() { + } + + public MappingUpdate(String actualVersion, String oldVersion, + Collection updates) { + this.version = actualVersion; + this.oldVersion = oldVersion; + this.updates = updates; + } + + private String version; + private String oldVersion; + private Collection updates; + + public String getVersion() { + return version; + } + + @JsonSetter("version") + public void setVersion(String version) { + this.version = version; + } + + public String getOldVersion() { + return oldVersion; + } + + @JsonSetter("oldVersion") + public void setOldVersion(String oldVersion) { + this.oldVersion = oldVersion; + } + + public Collection getUpdates() { + return updates; + } + + @JsonSetter("updates") + public void setUpdates(List updates) { + this.updates = updates; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MappingUpdate that = (MappingUpdate) o; + return Objects.equals(version, that.version) && Objects.equals( + oldVersion, that.oldVersion) && Objects.equals(updates, + that.updates); + } + + @Override + public int hashCode() { + return Objects.hash(version, oldVersion, updates); + } +} diff --git a/src/main/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessor.java b/src/main/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessor.java new file mode 100644 index 0000000..49c1ee0 --- /dev/null +++ b/src/main/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessor.java @@ -0,0 +1,125 @@ +package de.unimarburg.diz.labtofhir.processor; + +import de.unimarburg.diz.labtofhir.LabRunner; +import de.unimarburg.diz.labtofhir.UpdateCompleted; +import de.unimarburg.diz.labtofhir.mapper.MiiLabReportMapper; +import de.unimarburg.diz.labtofhir.model.LabOffsets; +import de.unimarburg.diz.labtofhir.model.LaboratoryReport; +import de.unimarburg.diz.labtofhir.model.MappingInfo; +import de.unimarburg.diz.labtofhir.model.MappingUpdate; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.apache.kafka.streams.kstream.KStream; +import org.apache.kafka.streams.processor.api.ContextualProcessor; +import org.apache.kafka.streams.processor.api.Record; +import org.hl7.fhir.r4.model.Bundle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Service; + +@Service +public class LabUpdateProcessor { + + private static final Logger LOG = LoggerFactory.getLogger( + LabUpdateProcessor.class); + private MappingUpdate mappingVersion; + private final MiiLabReportMapper reportMapper; + private final ConcurrentHashMap offsetState; + private final ApplicationEventPublisher eventPublisher; + + + public LabUpdateProcessor(LabRunner labRunner, + MiiLabReportMapper reportMapper, @Nullable MappingInfo mappingInfo, + LabOffsets offsets, ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + this.reportMapper = reportMapper; + if (mappingInfo != null) { + this.mappingVersion = mappingInfo.update(); + } + + this.offsetState = new ConcurrentHashMap<>(offsets + .processOffsets() + .entrySet() + .stream() + .collect(Collectors.toMap(Entry::getKey, e -> new OffsetTarget(e + .getValue() + .offset(), false)))); + } + + @SuppressWarnings("checkstyle:LineLength") + @Bean + @ConditionalOnBean(MappingInfo.class) + public Function, KStream> update() { + + return report -> report.process( + () -> new ContextualProcessor() { + + @Override + public void process(Record record) { + var currentPartition = context() + .recordMetadata() + .orElseThrow() + .partition(); + var currentOffset = context() + .recordMetadata() + .orElseThrow() + .offset(); + + // check partitions and offsets + var partitionState = offsetState.get(currentPartition); + if (partitionState.offset() <= currentOffset + 1) { + if (!partitionState.done()) { + + offsetState.computeIfPresent(currentPartition, + (partition, target) -> new OffsetTarget( + target.offset(), true)); + } + + // all done + if (offsetState + .values() + .stream() + .allMatch(s -> s.done)) { + + // send completed event + eventPublisher.publishEvent( + new UpdateCompleted(this)); + } + + return; + } + + // filter for update codes + if (record + .value() + .getObservations() + .stream() + .anyMatch(o -> o + .getCode() + .getCoding() + .stream() + .anyMatch(c -> mappingVersion + .getUpdates() + .contains(c.getCode())))) { + + // map + var bundle = reportMapper.apply(record.value()); + + context().forward(record.withValue(bundle)); + } + } + }) + // filter + .filter((k, v) -> v != null); + } + + private record OffsetTarget(long offset, boolean done) { + + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c3b9e89..b376fd9 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -11,16 +11,30 @@ spring: key-store-password: ${SSL_KEY_STORE_PASSWORD} cloud: + function: + definition: process;update stream: bindings: process-in-0: destination: ${INPUT_TOPIC:aim-lab} + consumer: + auto-startup: false process-out-0: destination: ${OUTPUT_TOPIC:lab-fhir} + update-in-0: + destination: ${INPUT_TOPIC:aim-lab} + consumer: + auto-startup: false + update-out-0: + destination: ${OUTPUT_TOPIC:lab-fhir} kafka: streams: binder: - applicationId: lab-to-fhir + functions: + process: + applicationId: lab-to-fhir + update: + applicationId: lab-update configuration: compression.type: gzip max.request.size: 5242880 @@ -44,7 +58,7 @@ fhir: mapping: loinc: - version: "2.0.1" + version: "2.0.2" credentials: user: password: diff --git a/src/test/java/de/unimarburg/diz/labtofhir/mapper/LoincMapperTests.java b/src/test/java/de/unimarburg/diz/labtofhir/mapper/LoincMapperTests.java index 09795c0..523576a 100644 --- a/src/test/java/de/unimarburg/diz/labtofhir/mapper/LoincMapperTests.java +++ b/src/test/java/de/unimarburg/diz/labtofhir/mapper/LoincMapperTests.java @@ -34,7 +34,6 @@ public class LoincMapperTests { @Autowired private FhirProperties fhirProperties; - // private static LoincMap testLoincMap; @TestConfiguration static class LoincMapperTestConfiguration { diff --git a/src/test/java/de/unimarburg/diz/labtofhir/model/LoincMapTests.java b/src/test/java/de/unimarburg/diz/labtofhir/model/LoincMapTests.java new file mode 100644 index 0000000..9ced3c0 --- /dev/null +++ b/src/test/java/de/unimarburg/diz/labtofhir/model/LoincMapTests.java @@ -0,0 +1,33 @@ +package de.unimarburg.diz.labtofhir.model; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Set; +import org.junit.jupiter.api.Test; + +public class LoincMapTests { + + @Test + void diffCreatesCodeDiff() { + var original = new LoincMap(); + original.put("FOO", new LoincMapEntry() + .setLoinc("1000-0") + .setUcum("mmol/L")); + original.put("BAR", new LoincMapEntry() + .setLoinc("1000-0") + .setUcum("mmol/L")); + + var from = new LoincMap(); + from.put("FOO", new LoincMapEntry() + .setLoinc("1000-0") + .setUcum("mmol/L")); + from.put("BAR", new LoincMapEntry() + .setLoinc("1111-0") + .setUcum("mmol/L")); + + var diff = original.diff(from); + + assertThat(diff).isEqualTo(Set.of("BAR")); + } + +} diff --git a/src/test/java/de/unimarburg/diz/labtofhir/serde/JsonSerdes.java b/src/test/java/de/unimarburg/diz/labtofhir/serde/JsonSerdes.java index 1086579..d9ceef3 100644 --- a/src/test/java/de/unimarburg/diz/labtofhir/serde/JsonSerdes.java +++ b/src/test/java/de/unimarburg/diz/labtofhir/serde/JsonSerdes.java @@ -1,7 +1,7 @@ package de.unimarburg.diz.labtofhir.serde; import de.unimarburg.diz.labtofhir.model.LaboratoryReport; -import de.unimarburg.diz.labtofhir.model.LoincMap; +import de.unimarburg.diz.labtofhir.model.LoincMapTests; import org.springframework.kafka.support.serializer.JsonSerde; public class JsonSerdes { @@ -10,8 +10,8 @@ public static JsonSerde laboratoryReport() { return new JsonSerde<>(LaboratoryReport.class); } - public static JsonSerde loincMapEntry() { - return new JsonSerde<>(LoincMap.class); + public static JsonSerde loincMapEntry() { + return new JsonSerde<>(LoincMapTests.class); } } From 367770b7840cba5de5b9fd990071974f0273ee2b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 10 Mar 2024 20:36:08 +0000 Subject: [PATCH 04/21] chore(deps): update github-actions --- .github/workflows/build.yml | 4 ++-- .github/workflows/mega-linter.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ac1f10e..f3220d2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,9 +18,9 @@ jobs: java-version: "17" distribution: "temurin" - name: Validate Gradle wrapper - uses: gradle/wrapper-validation-action@v2.1.0 + uses: gradle/wrapper-validation-action@v2.1.1 - name: Setup Gradle - uses: gradle/gradle-build-action@v3.0.0 + uses: gradle/gradle-build-action@v3.1.0 - name: Execute Gradle build run: ./gradlew build - name: Upload coverage to Codecov diff --git a/.github/workflows/mega-linter.yml b/.github/workflows/mega-linter.yml index d303747..67adef4 100644 --- a/.github/workflows/mega-linter.yml +++ b/.github/workflows/mega-linter.yml @@ -44,7 +44,7 @@ jobs: id: ml # You can override MegaLinter flavor used to have faster performances # More info at https://megalinter.io/flavors/ - uses: oxsecurity/megalinter/flavors/cupcake@v7.8.0 + uses: oxsecurity/megalinter/flavors/cupcake@v7.10.0 env: # All available variables are described in documentation # https://megalinter.io/configuration/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8a08d27..9463a71 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,7 +49,7 @@ jobs: uses: actions/checkout@v4 - name: Log in to the Container registry - uses: docker/login-action@3d58c274f17dffee475a5520cbe67f0a882c4dbb + uses: docker/login-action@5139682d94efc37792e6b54386b5b470a68a4737 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} From d2d2c2b1af7eee45fca5a7e9c989eb3af8cdbd64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20St=C3=B6cker?= Date: Mon, 11 Mar 2024 11:29:47 +0100 Subject: [PATCH 05/21] test: fix lab processor tests --- .editorconfig | 10 - .../configuration/MappingConfiguration.java | 183 +----------------- .../MappingUpdateConfiguration.java | 134 +++++++++++++ .../diz/labtofhir/mapper/LoincMapper.java | 6 +- .../diz/labtofhir/util/ResourceHelper.java | 73 +++++++ .../processor/LabToFhirProcessorTests.java | 1 - 6 files changed, 214 insertions(+), 193 deletions(-) delete mode 100644 .editorconfig create mode 100644 src/main/java/de/unimarburg/diz/labtofhir/configuration/MappingUpdateConfiguration.java create mode 100644 src/main/java/de/unimarburg/diz/labtofhir/util/ResourceHelper.java diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 862a77b..0000000 --- a/.editorconfig +++ /dev/null @@ -1,10 +0,0 @@ -root = true - -[*] -insert_final_newline = true -charset = utf-8 - -[*.java] -indent_style = space -indent_size = 4 -# ij_continuation_indent_size = 8 diff --git a/src/main/java/de/unimarburg/diz/labtofhir/configuration/MappingConfiguration.java b/src/main/java/de/unimarburg/diz/labtofhir/configuration/MappingConfiguration.java index e09274e..67474f0 100644 --- a/src/main/java/de/unimarburg/diz/labtofhir/configuration/MappingConfiguration.java +++ b/src/main/java/de/unimarburg/diz/labtofhir/configuration/MappingConfiguration.java @@ -1,38 +1,14 @@ package de.unimarburg.diz.labtofhir.configuration; -import de.unimarburg.diz.labtofhir.mapper.LoincMapper; -import de.unimarburg.diz.labtofhir.model.LabOffsets; -import de.unimarburg.diz.labtofhir.model.MappingInfo; -import de.unimarburg.diz.labtofhir.model.MappingUpdate; -import java.io.File; -import java.io.FileOutputStream; +import de.unimarburg.diz.labtofhir.util.ResourceHelper; import java.io.IOException; -import java.time.Duration; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.ExecutionException; -import org.apache.commons.lang3.StringUtils; -import org.apache.http.HttpHost; -import org.apache.http.auth.AuthScope; -import org.apache.http.auth.UsernamePasswordCredentials; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.BasicCredentialsProvider; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.producer.Producer; -import org.apache.kafka.clients.producer.ProducerRecord; -import org.apache.kafka.common.TopicPartition; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; -import org.springframework.util.StreamUtils; @Configuration @EnableConfigurationProperties @@ -40,7 +16,6 @@ public class MappingConfiguration { private static final Logger LOG = LoggerFactory.getLogger( MappingConfiguration.class); - private static final long MAX_CONSUMER_POLL_DURATION_SECONDS = 10L; @Bean public MappingProperties mappingProperties() { @@ -72,159 +47,7 @@ private Resource getMappingFile( @Value("${mapping.loinc.proxy}") String proxyServer, @Value("${mapping.loinc.local}") String localPkg) throws IOException { - if (StringUtils.isBlank(localPkg)) { - // load from remote location - LOG.info("Using LOINC mapping package from remote location"); - - var provider = new BasicCredentialsProvider(); - var credentials = new UsernamePasswordCredentials(user, password); - provider.setCredentials(AuthScope.ANY, credentials); - - var clientBuilder = HttpClientBuilder - .create() - .setDefaultCredentialsProvider(provider); - - if (!StringUtils.isBlank(proxyServer)) { - clientBuilder.setProxy(HttpHost.create(proxyServer)); - } - - var response = clientBuilder - .build() - .execute(new HttpGet(String.format( - "https://gitlab.diz.uni-marburg.de/" - + "api/v4/projects/63/packages/generic/" - + "mapping-swl-loinc/%s/mapping-swl-loinc.zip", - version))); - - LOG.info("Package registry responded with: " + response - .getStatusLine() - .toString()); - - var tmpFile = File.createTempFile("download", ".zip"); - StreamUtils.copy(response - .getEntity() - .getContent(), new FileOutputStream(tmpFile)); - - return new FileSystemResource(tmpFile); - - } else { - - // load local file from classpath - LOG.info("Using local LOINC mapping package from: {}", localPkg); - return new FileSystemResource( - new ClassPathResource(localPkg).getFile()); - } - } - - @Bean - public LoincMapper createMapper(FhirProperties fhirProperties, - @Qualifier("mappingPackage") Resource mappingPackage) throws Exception { - - return new LoincMapper(fhirProperties, mappingPackage).initialize(); - } - - @Bean("mappingInfo") - public MappingInfo buildMappingUpdate(LoincMapper loincMapper, - MappingProperties mappingProperties, LabOffsets labOffsets, - Consumer consumer, - Producer producer) - throws ExecutionException, InterruptedException, IOException { - // check versions - var configuredVersion = loincMapper - .getMap() - .getMetadata() - .getVersion(); - - // 1. consume latest from (mapping) update topic - var lastUpdate = getLastMappingUpdate(consumer); - if (lastUpdate == null) { - // job runs the first time: save current state - lastUpdate = new MappingUpdate(configuredVersion, null, List.of()); - try { - saveMappingUpdate(producer, lastUpdate); - } catch (InterruptedException | ExecutionException e) { - LOG.error("Failed to save mapping update to topic."); - throw e; - } - - } - - // 2. check if already on latest - if (Objects.equals(configuredVersion, lastUpdate.getVersion())) { - LOG.info("Configured mapping version ({}) matches last version " - + "({}). " + "No update necesssary", configuredVersion, - lastUpdate.getVersion()); - - if (!labOffsets - .updateOffsets() - .isEmpty()) { - // update in progress, continue - return new MappingInfo(lastUpdate, true); - } - - return null; - } - - // 3. calculate diff of mapping versions and save new update - // get last version's mapping - var lastMap = LoincMapper.getSwlLoincMapping( - getMappingFile(lastUpdate.getVersion(), mappingProperties - .getLoinc() - .getCredentials() - .getUser(), mappingProperties - .getLoinc() - .getCredentials() - .getPassword(), mappingProperties - .getLoinc() - .getProxy(), mappingProperties - .getLoinc() - .getLocal())); - // ceate diff - var updates = loincMapper - .getMap() - .diff(lastMap); - var update = new MappingUpdate(configuredVersion, - lastUpdate.getVersion(), updates); - - // save new mapping update - saveMappingUpdate(producer, update); - - return new MappingInfo(update, false); - } - - private void saveMappingUpdate(Producer producer, - MappingUpdate mappingUpdate) - throws ExecutionException, InterruptedException { - - producer - .send(new ProducerRecord<>("mapping", "lab-update", mappingUpdate)) - .get(); - } - - private MappingUpdate getLastMappingUpdate( - Consumer consumer) { - var topic = "mapping"; - - var partition = new TopicPartition(topic, 0); - var partitions = List.of(partition); - - try (consumer) { - consumer.assign(partitions); - consumer.seekToEnd(partitions); - var position = consumer.position(partition); - if (position == 0) { - return null; - } - - consumer.seek(partition, position - 1); - - var record = consumer - .poll(Duration.ofSeconds(MAX_CONSUMER_POLL_DURATION_SECONDS)) - .iterator() - .next(); - - consumer.unsubscribe(); - return record.value(); - } + return ResourceHelper.getMappingFile(version, user, password, + proxyServer, localPkg); } } diff --git a/src/main/java/de/unimarburg/diz/labtofhir/configuration/MappingUpdateConfiguration.java b/src/main/java/de/unimarburg/diz/labtofhir/configuration/MappingUpdateConfiguration.java new file mode 100644 index 0000000..40010d5 --- /dev/null +++ b/src/main/java/de/unimarburg/diz/labtofhir/configuration/MappingUpdateConfiguration.java @@ -0,0 +1,134 @@ +package de.unimarburg.diz.labtofhir.configuration; + +import de.unimarburg.diz.labtofhir.mapper.LoincMapper; +import de.unimarburg.diz.labtofhir.model.LabOffsets; +import de.unimarburg.diz.labtofhir.model.MappingInfo; +import de.unimarburg.diz.labtofhir.model.MappingUpdate; +import de.unimarburg.diz.labtofhir.util.ResourceHelper; +import java.io.IOException; +import java.time.Duration; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.clients.producer.Producer; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.TopicPartition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MappingUpdateConfiguration { + + private static final Logger LOG = LoggerFactory.getLogger( + MappingUpdateConfiguration.class); + private static final long MAX_CONSUMER_POLL_DURATION_SECONDS = 10L; + + @Bean("mappingInfo") + public MappingInfo buildMappingUpdate(LoincMapper loincMapper, + MappingProperties mappingProperties, LabOffsets labOffsets, + Consumer consumer, + Producer producer) + throws ExecutionException, InterruptedException, IOException { + // check versions + var configuredVersion = loincMapper + .getMap() + .getMetadata() + .getVersion(); + + // 1. consume latest from (mapping) update topic + var lastUpdate = getLastMappingUpdate(consumer); + if (lastUpdate == null) { + // job runs the first time: save current state + lastUpdate = new MappingUpdate(configuredVersion, null, List.of()); + try { + saveMappingUpdate(producer, lastUpdate); + } catch (InterruptedException | ExecutionException e) { + LOG.error("Failed to save mapping update to topic."); + throw e; + } + + } + + // 2. check if already on latest + if (Objects.equals(configuredVersion, lastUpdate.getVersion())) { + LOG.info("Configured mapping version ({}) matches last version " + + "({}). " + "No update necesssary", configuredVersion, + lastUpdate.getVersion()); + + if (!labOffsets + .updateOffsets() + .isEmpty()) { + // update in progress, continue + return new MappingInfo(lastUpdate, true); + } + + return null; + } + + // 3. calculate diff of mapping versions and save new update + // get last version's mapping + var lastMap = LoincMapper.getSwlLoincMapping( + ResourceHelper.getMappingFile(lastUpdate.getVersion(), + mappingProperties + .getLoinc() + .getCredentials() + .getUser(), mappingProperties + .getLoinc() + .getCredentials() + .getPassword(), mappingProperties + .getLoinc() + .getProxy(), mappingProperties + .getLoinc() + .getLocal())); + // ceate diff + var updates = loincMapper + .getMap() + .diff(lastMap); + var update = new MappingUpdate(configuredVersion, + lastUpdate.getVersion(), updates); + + // save new mapping update + saveMappingUpdate(producer, update); + + return new MappingInfo(update, false); + } + + private void saveMappingUpdate(Producer producer, + MappingUpdate mappingUpdate) + throws ExecutionException, InterruptedException { + + producer + .send(new ProducerRecord<>("mapping", "lab-update", mappingUpdate)) + .get(); + } + + private MappingUpdate getLastMappingUpdate( + Consumer consumer) { + var topic = "mapping"; + + var partition = new TopicPartition(topic, 0); + var partitions = List.of(partition); + + try (consumer) { + consumer.assign(partitions); + consumer.seekToEnd(partitions); + var position = consumer.position(partition); + if (position == 0) { + return null; + } + + consumer.seek(partition, position - 1); + + var record = consumer + .poll(Duration.ofSeconds(MAX_CONSUMER_POLL_DURATION_SECONDS)) + .iterator() + .next(); + + consumer.unsubscribe(); + return record.value(); + } + } +} diff --git a/src/main/java/de/unimarburg/diz/labtofhir/mapper/LoincMapper.java b/src/main/java/de/unimarburg/diz/labtofhir/mapper/LoincMapper.java index 56dd78d..b0a0246 100644 --- a/src/main/java/de/unimarburg/diz/labtofhir/mapper/LoincMapper.java +++ b/src/main/java/de/unimarburg/diz/labtofhir/mapper/LoincMapper.java @@ -5,6 +5,7 @@ import de.unimarburg.diz.labtofhir.model.LoincMap; import de.unimarburg.diz.labtofhir.model.LoincMapEntry; import io.micrometer.core.instrument.Metrics; +import jakarta.annotation.PostConstruct; import java.io.IOException; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Observation; @@ -13,8 +14,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.core.io.Resource; +import org.springframework.stereotype.Service; -//@Service +@Service public class LoincMapper { private final FhirProperties fhirProperties; @@ -55,12 +57,12 @@ public static LoincMap getSwlLoincMapping(Resource mappingPackage) return new LoincMap().with(mappingPackage, ','); } + @PostConstruct public LoincMapper initialize() throws Exception { initializeMap(); return this; } - // @PostConstruct private void initializeMap() throws Exception { if (this.mappingPackage != null) { this.loincMap = getSwlLoincMapping(mappingPackage); diff --git a/src/main/java/de/unimarburg/diz/labtofhir/util/ResourceHelper.java b/src/main/java/de/unimarburg/diz/labtofhir/util/ResourceHelper.java new file mode 100644 index 0000000..bf56eab --- /dev/null +++ b/src/main/java/de/unimarburg/diz/labtofhir/util/ResourceHelper.java @@ -0,0 +1,73 @@ +package de.unimarburg.diz.labtofhir.util; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.HttpClientBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.util.StreamUtils; + +public class ResourceHelper { + + private static final Logger LOG = LoggerFactory.getLogger( + ResourceHelper.class); + + public static Resource getMappingFile(String version, String user, + String password, String proxyServer, String localPkg) + throws IOException { + + if (StringUtils.isBlank(localPkg)) { + // load from remote location + LOG.info("Using LOINC mapping package from remote location"); + + var provider = new BasicCredentialsProvider(); + var credentials = new UsernamePasswordCredentials(user, password); + provider.setCredentials(AuthScope.ANY, credentials); + + var clientBuilder = HttpClientBuilder + .create() + .setDefaultCredentialsProvider(provider); + + if (!StringUtils.isBlank(proxyServer)) { + clientBuilder.setProxy(HttpHost.create(proxyServer)); + } + + var response = clientBuilder + .build() + .execute(new HttpGet(String.format( + "https://gitlab.diz.uni-marburg.de/" + + "api/v4/projects/63/packages/generic/" + + "mapping-swl-loinc/%s/mapping-swl-loinc.zip", + version))); + + LOG.info("Package registry responded with: " + response + .getStatusLine() + .toString()); + + var tmpFile = File.createTempFile("download", ".zip"); + StreamUtils.copy(response + .getEntity() + .getContent(), new FileOutputStream(tmpFile)); + + return new FileSystemResource(tmpFile); + + } else { + + // load local file from classpath + LOG.info("Using local LOINC mapping package from: {}", localPkg); + return new FileSystemResource( + new ClassPathResource(localPkg).getFile()); + } + } + +} diff --git a/src/test/java/de/unimarburg/diz/labtofhir/processor/LabToFhirProcessorTests.java b/src/test/java/de/unimarburg/diz/labtofhir/processor/LabToFhirProcessorTests.java index 8c0d97c..a84c1f6 100644 --- a/src/test/java/de/unimarburg/diz/labtofhir/processor/LabToFhirProcessorTests.java +++ b/src/test/java/de/unimarburg/diz/labtofhir/processor/LabToFhirProcessorTests.java @@ -124,5 +124,4 @@ public void observationIsLoincMapped() { .getLaboratorySystem(), "NA")).isTrue(); } } - } From c55ea42e58023d7459b91b16ad9968d7365789de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20St=C3=B6cker?= Date: Mon, 11 Mar 2024 11:45:17 +0100 Subject: [PATCH 06/21] ci: reenable devskim --- .github/workflows/mega-linter.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/mega-linter.yml b/.github/workflows/mega-linter.yml index d303747..b293fc9 100644 --- a/.github/workflows/mega-linter.yml +++ b/.github/workflows/mega-linter.yml @@ -52,7 +52,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # ADD YOUR CUSTOM ENV VARIABLES HERE OR DEFINE THEM IN A FILE .mega-linter.yml AT THE ROOT OF YOUR REPOSITORY DISABLE: SPELL # ignore spell checks - DISABLE_ERRORS_LINTERS: REPOSITORY_DEVSKIM # missing git safe.directory DISABLE_LINTERS: JAVA_PMD # Upload MegaLinter artifacts From d164de56acb1655cc20426f5a2db8a5b0cd72ae0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 10:48:28 +0000 Subject: [PATCH 07/21] fix(deps): update spring boot to v3.2.3 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index dd74ec8..b0608d8 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ plugins { id 'java' id 'jacoco' - id 'org.springframework.boot' version '3.2.2' + id 'org.springframework.boot' version '3.2.3' id 'io.spring.dependency-management' version '1.1.4' id 'checkstyle' } @@ -18,7 +18,7 @@ repositories { } ext { - set('springBootVersion', "3.2.2") + set('springBootVersion', "3.2.3") set('springCloudDepsVersion', "2023.0.0") set('springCloudStreamVersion', "4.1.0") set('springKafkaVersion', "3.1.1") From 9dc323fe702a79890d8a8e3b7ccb41398cfc5751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20St=C3=B6cker?= Date: Mon, 11 Mar 2024 12:41:33 +0100 Subject: [PATCH 08/21] ci: use codecov token --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f3220d2..723cdee 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,3 +25,5 @@ jobs: run: ./gradlew build - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} From c302dd135c9fb88c6bfe782f29ec423d073e915b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20St=C3=B6cker?= Date: Mon, 11 Mar 2024 13:19:41 +0100 Subject: [PATCH 09/21] test: configure test reports --- build.gradle | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 1d36c70..8dd0f70 100644 --- a/build.gradle +++ b/build.gradle @@ -71,16 +71,17 @@ dependencyManagement { test { useJUnitPlatform() - finalizedBy jacocoTestReport } jacocoTestReport { reports { - xml.required.set(false) + xml.required = true + html.required = false } - dependsOn test } +check.dependsOn jacocoTestReport + jar { enabled = false From cc49efbcd2c39b2c075284bae9103bae1a440f21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20St=C3=B6cker?= Date: Tue, 12 Mar 2024 16:20:41 +0100 Subject: [PATCH 10/21] test: add unit tests --- .../processor/LabUpdateProcessor.java | 9 +- .../labtofhir/model/LoincMapEntryTests.java | 32 ++++ .../processor/LabToFhirProcessorTests.java | 6 +- .../processor/LabUpdateProcessorTests.java | 168 ++++++++++++++++++ 4 files changed, 207 insertions(+), 8 deletions(-) create mode 100644 src/test/java/de/unimarburg/diz/labtofhir/model/LoincMapEntryTests.java create mode 100644 src/test/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessorTests.java diff --git a/src/main/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessor.java b/src/main/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessor.java index 49c1ee0..adf8e77 100644 --- a/src/main/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessor.java +++ b/src/main/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessor.java @@ -1,6 +1,5 @@ package de.unimarburg.diz.labtofhir.processor; -import de.unimarburg.diz.labtofhir.LabRunner; import de.unimarburg.diz.labtofhir.UpdateCompleted; import de.unimarburg.diz.labtofhir.mapper.MiiLabReportMapper; import de.unimarburg.diz.labtofhir.model.LabOffsets; @@ -34,9 +33,9 @@ public class LabUpdateProcessor { private final ApplicationEventPublisher eventPublisher; - public LabUpdateProcessor(LabRunner labRunner, - MiiLabReportMapper reportMapper, @Nullable MappingInfo mappingInfo, - LabOffsets offsets, ApplicationEventPublisher eventPublisher) { + public LabUpdateProcessor(MiiLabReportMapper reportMapper, + @Nullable MappingInfo mappingInfo, LabOffsets offsets, + ApplicationEventPublisher eventPublisher) { this.eventPublisher = eventPublisher; this.reportMapper = reportMapper; if (mappingInfo != null) { @@ -73,7 +72,7 @@ public void process(Record record) { // check partitions and offsets var partitionState = offsetState.get(currentPartition); - if (partitionState.offset() <= currentOffset + 1) { + if (currentOffset > partitionState.offset()) { if (!partitionState.done()) { offsetState.computeIfPresent(currentPartition, diff --git a/src/test/java/de/unimarburg/diz/labtofhir/model/LoincMapEntryTests.java b/src/test/java/de/unimarburg/diz/labtofhir/model/LoincMapEntryTests.java new file mode 100644 index 0000000..7243fd0 --- /dev/null +++ b/src/test/java/de/unimarburg/diz/labtofhir/model/LoincMapEntryTests.java @@ -0,0 +1,32 @@ +package de.unimarburg.diz.labtofhir.model; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import org.junit.jupiter.api.Test; + +public class LoincMapEntryTests { + + @Test + void entryIsEqual() { + var entry = new LoincMapEntry(); + entry.setSwl("swisslab"); + entry.setLoinc("loinc"); + entry.setUcum("ucum"); + entry.setMeta("meta"); + var entry2 = new LoincMapEntry(); + entry2.setSwl("swisslab"); + entry2.setLoinc("loinc"); + entry2.setUcum("ucum"); + entry2.setMeta("meta"); + var entry3 = new LoincMapEntry(); + entry3.setSwl("swisslab"); + entry3.setLoinc("loinc"); + entry3.setUcum("ucum"); + entry3.setMeta("META"); + + assertThat(entry.equals(entry)).isTrue(); + assertThat(entry.equals(entry2)).isTrue(); + assertThat(entry.equals(entry3)).isFalse(); + } + +} diff --git a/src/test/java/de/unimarburg/diz/labtofhir/processor/LabToFhirProcessorTests.java b/src/test/java/de/unimarburg/diz/labtofhir/processor/LabToFhirProcessorTests.java index a84c1f6..d7c022e 100644 --- a/src/test/java/de/unimarburg/diz/labtofhir/processor/LabToFhirProcessorTests.java +++ b/src/test/java/de/unimarburg/diz/labtofhir/processor/LabToFhirProcessorTests.java @@ -12,9 +12,9 @@ import de.unimarburg.diz.labtofhir.serializer.FhirDeserializer; import de.unimarburg.diz.labtofhir.serializer.FhirSerializer; import java.util.List; -import org.apache.kafka.common.serialization.IntegerSerializer; import org.apache.kafka.common.serialization.Serdes; import org.apache.kafka.common.serialization.StringDeserializer; +import org.apache.kafka.common.serialization.StringSerializer; import org.apache.kafka.streams.StreamsBuilder; import org.apache.kafka.streams.TopologyTestDriver; import org.apache.kafka.streams.kstream.Consumed; @@ -74,7 +74,7 @@ public void observationIsLoincMapped() { try (var driver = new TopologyTestDriver(builder.build())) { var labTopic = driver.createInputTopic("lab", - new IntegerSerializer(), new JsonSerializer<>()); + new StringSerializer(), new JsonSerializer<>()); var outputTopic = driver.createOutputTopic("lab-mapped", new StringDeserializer(), new FhirDeserializer<>(Bundle.class)); @@ -98,7 +98,7 @@ public void observationIsLoincMapped() { labReport.setObservations(List.of(obs)); // create input record - labTopic.pipeInput(labReport.getId(), labReport); + labTopic.pipeInput(String.valueOf(labReport.getId()), labReport); // get record from output topic var outputRecords = outputTopic.readRecordsToList(); diff --git a/src/test/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessorTests.java b/src/test/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessorTests.java new file mode 100644 index 0000000..857326b --- /dev/null +++ b/src/test/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessorTests.java @@ -0,0 +1,168 @@ +package de.unimarburg.diz.labtofhir.processor; + +import static org.assertj.core.api.Assertions.assertThat; + +import de.unimarburg.diz.labtofhir.configuration.FhirConfiguration; +import de.unimarburg.diz.labtofhir.configuration.FhirProperties; +import de.unimarburg.diz.labtofhir.configuration.MappingConfiguration; +import de.unimarburg.diz.labtofhir.mapper.LoincMapper; +import de.unimarburg.diz.labtofhir.mapper.MiiLabReportMapper; +import de.unimarburg.diz.labtofhir.model.LabOffsets; +import de.unimarburg.diz.labtofhir.model.LaboratoryReport; +import de.unimarburg.diz.labtofhir.model.MappingInfo; +import de.unimarburg.diz.labtofhir.model.MappingUpdate; +import de.unimarburg.diz.labtofhir.processor.LabUpdateProcessorTests.KafkaConfig; +import de.unimarburg.diz.labtofhir.serde.JsonSerdes; +import de.unimarburg.diz.labtofhir.serializer.FhirDeserializer; +import de.unimarburg.diz.labtofhir.serializer.FhirSerializer; +import java.util.List; +import java.util.Map; +import org.apache.kafka.clients.consumer.OffsetAndMetadata; +import org.apache.kafka.common.serialization.Serdes; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.apache.kafka.common.serialization.StringSerializer; +import org.apache.kafka.streams.KeyValue; +import org.apache.kafka.streams.StreamsBuilder; +import org.apache.kafka.streams.TestInputTopic; +import org.apache.kafka.streams.TopologyTestDriver; +import org.apache.kafka.streams.kstream.Consumed; +import org.apache.kafka.streams.kstream.KStream; +import org.apache.kafka.streams.kstream.Produced; +import org.apache.kafka.streams.test.TestRecord; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.DiagnosticReport; +import org.hl7.fhir.r4.model.Encounter; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Quantity; +import org.hl7.fhir.r4.model.Reference; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.kafka.support.serializer.JsonSerializer; +import org.springframework.test.context.TestPropertySource; + +@SpringBootTest(classes = {LabUpdateProcessor.class, MiiLabReportMapper.class, + LoincMapper.class, FhirConfiguration.class, MappingConfiguration.class, + KafkaConfig.class}) +@TestPropertySource(properties = {"mapping.loinc.version=''", + "mapping.loinc.credentials.user=''", + "mapping.loinc.credentials.password=''", + "mapping.loinc.local=mapping-swl-loinc.zip"}) +public class LabUpdateProcessorTests { + + @Autowired + private LabUpdateProcessor processor; + + + @Autowired + private FhirProperties fhirProperties; + + @SuppressWarnings({"checkstyle:MagicNumber", "checkstyle:LineLength"}) + @Test + void updateIsProcessed() { + + // build stream + var builder = new StreamsBuilder(); + final KStream labStream = builder.stream( + "lab", + Consumed.with(Serdes.String(), JsonSerdes.laboratoryReport())); + + processor + .update() + .apply(labStream) + .to("lab-mapped", Produced.with(Serdes.String(), + Serdes.serdeFrom(new FhirSerializer<>(), + new FhirDeserializer<>(Bundle.class)))); + + try (var driver = new TopologyTestDriver(builder.build())) { + + TestInputTopic labTopic = driver.createInputTopic( + "lab", new StringSerializer(), new JsonSerializer<>()); + var outputTopic = driver.createOutputTopic("lab-mapped", + new StringDeserializer(), new FhirDeserializer<>(Bundle.class)); + + var inputReports = List.of(createReport(1, "NA"), + createReport(2, "ERY"), createReport(3, "NA"), + createReport(4, "NA")); + + // create input records + labTopic.pipeKeyValueList(inputReports + .stream() + .map(r -> new KeyValue<>(String.valueOf(r.getId()), r)) + .toList()); + + // get record from output topic + var outputRecords = outputTopic.readRecordsToList(); + + // expected keys are: 1, 3 + assertThat(outputRecords + .stream() + .map(TestRecord::getKey) + .toList()).isEqualTo(List.of("1", "3")); + + // assert codes are mapped + var obsCodes = outputRecords + .stream() + .flatMap(b -> b + .getValue() + .getEntry() + .stream() + .map(BundleEntryComponent::getResource)) + .filter(Observation.class::isInstance) + .map(Observation.class::cast) + .map(Observation::getCode) + .toList(); + + assertThat(obsCodes).allMatch( + c -> c.hasCoding("http://loinc.org", "2951-2")); + } + } + + private LaboratoryReport createReport(int reportId, String labCode) { + var report = new LaboratoryReport(); + report.setId(reportId); + report.setResource(new DiagnosticReport() + .addIdentifier(new Identifier().setValue("report-id")) + .setSubject(new Reference( + new Patient().addIdentifier(new Identifier().setValue("1")))) + .setEncounter(new Reference(new Encounter().addIdentifier( + new Identifier().setValue("1"))))); + + var obs = new Observation() + .addIdentifier(new Identifier().setValue("obs-id")) + .setCode(new CodeableConcept().setCoding(List.of(new Coding( + fhirProperties + .getSystems() + .getLaboratorySystem(), labCode, null)))) + .setValue(new Quantity(1)); + obs.setId("obs-id"); + report.setObservations(List.of(obs)); + + return report; + } + + @TestConfiguration + static class KafkaConfig { + + @Bean + LabOffsets testOffsets() { + // offset target will be 2 on partition 0 + return new LabOffsets(Map.of(0, new OffsetAndMetadata(2L)), + Map.of()); + } + + @Bean + MappingInfo testMappingInfo() { + return new MappingInfo(new MappingUpdate(null, null, List.of("NA")), + false); + } + } + +} From 6049e6b109914216d8a1eeabc73ed3f685a77ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20St=C3=B6cker?= Date: Wed, 13 Mar 2024 22:08:41 +0100 Subject: [PATCH 11/21] test: add lab runner tests --- .../unimarburg/diz/labtofhir/LabRunner.java | 23 ++-- .../configuration/AdminClientProvider.java | 9 ++ .../configuration/KafkaConfiguration.java | 11 +- .../diz/labtofhir/LabRunnerTests.java | 100 ++++++++++++++++++ .../labtofhir/model/LoincMapEntryTests.java | 1 + .../processor/LabUpdateProcessorTests.java | 4 +- 6 files changed, 126 insertions(+), 22 deletions(-) create mode 100644 src/main/java/de/unimarburg/diz/labtofhir/configuration/AdminClientProvider.java create mode 100644 src/test/java/de/unimarburg/diz/labtofhir/LabRunnerTests.java diff --git a/src/main/java/de/unimarburg/diz/labtofhir/LabRunner.java b/src/main/java/de/unimarburg/diz/labtofhir/LabRunner.java index a94e804..fdaa833 100644 --- a/src/main/java/de/unimarburg/diz/labtofhir/LabRunner.java +++ b/src/main/java/de/unimarburg/diz/labtofhir/LabRunner.java @@ -1,12 +1,11 @@ package de.unimarburg.diz.labtofhir; +import de.unimarburg.diz.labtofhir.configuration.AdminClientProvider; import de.unimarburg.diz.labtofhir.model.MappingInfo; import java.util.Collections; -import java.util.Set; import java.util.concurrent.ExecutionException; import javax.annotation.Nullable; -import org.apache.kafka.clients.admin.AdminClient; -import org.apache.kafka.common.TopicPartition; +import org.apache.kafka.clients.admin.Admin; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -15,7 +14,6 @@ import org.springframework.cloud.stream.binding.BindingsLifecycleController.State; import org.springframework.cloud.stream.endpoint.BindingsEndpoint; import org.springframework.context.event.EventListener; -import org.springframework.kafka.core.KafkaAdmin; import org.springframework.retry.RetryCallback; import org.springframework.retry.RetryContext; import org.springframework.retry.RetryListener; @@ -31,7 +29,7 @@ public class LabRunner implements ApplicationRunner { private static final Logger LOG = LoggerFactory.getLogger(LabRunner.class); private final BindingsEndpoint endpoint; - private final KafkaAdmin kafkaAdmin; + private final AdminClientProvider kafkaAdmin; private final MappingInfo mappingInfo; private final String updateGroup; private final RetryTemplate retryTemplate; @@ -40,7 +38,7 @@ public class LabRunner implements ApplicationRunner { static final long RETRY_BACKOFF_PERIOD = 2_000L; @SuppressWarnings("checkstyle:LineLength") - public LabRunner(BindingsEndpoint endpoint, KafkaAdmin kafkaAdmin, + public LabRunner(BindingsEndpoint endpoint, AdminClientProvider kafkaAdmin, @Nullable MappingInfo mappingInfo, @Value( "${spring.cloud.stream.kafka.streams.binder.functions.update" + ".applicationId}") String updateGroup) { @@ -89,17 +87,8 @@ public void run(ApplicationArguments args) throws Exception { endpoint.changeState("process-in-0", State.STARTED); } - void resetUpdateConsumer(AdminClient client, Set offsets) - throws ExecutionException, InterruptedException { - - client - .deleteConsumerGroupOffsets(updateGroup, offsets) - .all() - .get(); - } - - private AdminClient createAdminClient() { - return AdminClient.create(kafkaAdmin.getConfigurationProperties()); + private Admin createAdminClient() { + return kafkaAdmin.createClient(); } RetryTemplate setupRetryTemplate() { diff --git a/src/main/java/de/unimarburg/diz/labtofhir/configuration/AdminClientProvider.java b/src/main/java/de/unimarburg/diz/labtofhir/configuration/AdminClientProvider.java new file mode 100644 index 0000000..aad0773 --- /dev/null +++ b/src/main/java/de/unimarburg/diz/labtofhir/configuration/AdminClientProvider.java @@ -0,0 +1,9 @@ +package de.unimarburg.diz.labtofhir.configuration; + +import org.apache.kafka.clients.admin.Admin; + +@FunctionalInterface +public interface AdminClientProvider { + + Admin createClient(); +} diff --git a/src/main/java/de/unimarburg/diz/labtofhir/configuration/KafkaConfiguration.java b/src/main/java/de/unimarburg/diz/labtofhir/configuration/KafkaConfiguration.java index 8e88742..7e1e255 100644 --- a/src/main/java/de/unimarburg/diz/labtofhir/configuration/KafkaConfiguration.java +++ b/src/main/java/de/unimarburg/diz/labtofhir/configuration/KafkaConfiguration.java @@ -58,15 +58,20 @@ public StreamsBuilderFactoryBeanConfigurer streamsBuilderCustomizer() { }; } + @Bean + public AdminClientProvider clientProvider(KafkaAdmin kafkaAdmin) { + return () -> AdminClient.create( + kafkaAdmin.getConfigurationProperties()); + } + @Bean - public LabOffsets getOffsets(KafkaAdmin kafkaAdmin, + public LabOffsets getOffsets(AdminClientProvider kafkaAdmin, @Value("${spring.cloud.stream.kafka.streams.binder.functions.process.applicationId}") String processGroup, @Value("${spring.cloud.stream.kafka.streams.binder.functions.update.applicationId}") String updateGroup) throws ExecutionException, InterruptedException { // get current offsets - try (var client = AdminClient.create( - kafkaAdmin.getConfigurationProperties())) { + try (var client = kafkaAdmin.createClient()) { var processOffsets = client .listConsumerGroupOffsets(processGroup) .partitionsToOffsetAndMetadata() diff --git a/src/test/java/de/unimarburg/diz/labtofhir/LabRunnerTests.java b/src/test/java/de/unimarburg/diz/labtofhir/LabRunnerTests.java new file mode 100644 index 0000000..df20789 --- /dev/null +++ b/src/test/java/de/unimarburg/diz/labtofhir/LabRunnerTests.java @@ -0,0 +1,100 @@ +package de.unimarburg.diz.labtofhir; + +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import de.unimarburg.diz.labtofhir.configuration.AdminClientProvider; +import de.unimarburg.diz.labtofhir.model.MappingInfo; +import de.unimarburg.diz.labtofhir.model.MappingUpdate; +import java.util.List; +import java.util.Map; +import org.apache.kafka.clients.admin.Admin; +import org.apache.kafka.clients.consumer.OffsetAndMetadata; +import org.apache.kafka.common.TopicPartition; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.cloud.stream.binding.BindingsLifecycleController.State; +import org.springframework.cloud.stream.endpoint.BindingsEndpoint; + +@ExtendWith(MockitoExtension.class) +public class LabRunnerTests { + + @Mock + private BindingsEndpoint endpoint; + @Mock + private AdminClientProvider kafkaAdmin; + + + @Test + void runAlwaysStartsLabBinder() throws Exception { + + // arrange + var runner = setupRunner(null); + + // act + runner.run(null); + + // assert + verify(endpoint).changeState("process-in-0", State.STARTED); + verify(endpoint, never()).changeState("update-in-0", State.STARTED); + } + + @Test + void runStartsUpdateBinderOnResume() throws Exception { + + var runner = setupRunner(new MappingInfo(null, true)); + + // act + runner.run(null); + + verify(endpoint).changeState("process-in-0", State.STARTED); + verify(endpoint).changeState("update-in-0", State.STARTED); + } + + @Test + void runResetsUpdateConsumerOnExistingOffsets() throws Exception { + + // admin client + var admin = setupAdminClient(); + + when(admin + .listConsumerGroupOffsets(anyString()) + .partitionsToOffsetAndMetadata() + .get()).thenReturn(Map.of(new TopicPartition("lab-input", 0), + new OffsetAndMetadata(0, null))); + + var runner = setupRunner( + new MappingInfo(new MappingUpdate("2.0", "1.0", List.of("NA")), + false)); + + // act + runner.run(null); + + // verify delete consumer group + verify(admin + .deleteConsumerGroups(anyCollection()) + .all()).get(); + + verify(endpoint).changeState("process-in-0", State.STARTED); + verify(endpoint).changeState("update-in-0", State.STARTED); + } + + private LabRunner setupRunner(MappingInfo mappingInfo) { + return new LabRunner(endpoint, kafkaAdmin, mappingInfo, "lab-update"); + } + + private Admin setupAdminClient() { + // admin client + var admin = mock(Admin.class, RETURNS_DEEP_STUBS); + when(kafkaAdmin.createClient()).thenReturn(admin); + return admin; + } + +} diff --git a/src/test/java/de/unimarburg/diz/labtofhir/model/LoincMapEntryTests.java b/src/test/java/de/unimarburg/diz/labtofhir/model/LoincMapEntryTests.java index 7243fd0..f55b4e8 100644 --- a/src/test/java/de/unimarburg/diz/labtofhir/model/LoincMapEntryTests.java +++ b/src/test/java/de/unimarburg/diz/labtofhir/model/LoincMapEntryTests.java @@ -26,6 +26,7 @@ void entryIsEqual() { assertThat(entry.equals(entry)).isTrue(); assertThat(entry.equals(entry2)).isTrue(); + assertThat(entry.equals(null)).isFalse(); assertThat(entry.equals(entry3)).isFalse(); } diff --git a/src/test/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessorTests.java b/src/test/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessorTests.java index 857326b..94f9f44 100644 --- a/src/test/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessorTests.java +++ b/src/test/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessorTests.java @@ -77,7 +77,7 @@ void updateIsProcessed() { processor .update() .apply(labStream) - .to("lab-mapped", Produced.with(Serdes.String(), + .to("output-topic", Produced.with(Serdes.String(), Serdes.serdeFrom(new FhirSerializer<>(), new FhirDeserializer<>(Bundle.class)))); @@ -85,7 +85,7 @@ void updateIsProcessed() { TestInputTopic labTopic = driver.createInputTopic( "lab", new StringSerializer(), new JsonSerializer<>()); - var outputTopic = driver.createOutputTopic("lab-mapped", + var outputTopic = driver.createOutputTopic("output-topic", new StringDeserializer(), new FhirDeserializer<>(Bundle.class)); var inputReports = List.of(createReport(1, "NA"), From c93359fc2de80d9391a47077f126420cf3f06232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20St=C3=B6cker?= Date: Thu, 14 Mar 2024 13:42:50 +0100 Subject: [PATCH 12/21] more tests --- .../unimarburg/diz/labtofhir/LabRunner.java | 9 ++-- .../diz/labtofhir/util/ResourceHelper.java | 8 +-- .../diz/labtofhir/LabRunnerTests.java | 39 +++++++++++++- .../labtofhir/util/ResourceHelperTests.java | 54 +++++++++++++++++++ 4 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 src/test/java/de/unimarburg/diz/labtofhir/util/ResourceHelperTests.java diff --git a/src/main/java/de/unimarburg/diz/labtofhir/LabRunner.java b/src/main/java/de/unimarburg/diz/labtofhir/LabRunner.java index fdaa833..cab63cf 100644 --- a/src/main/java/de/unimarburg/diz/labtofhir/LabRunner.java +++ b/src/main/java/de/unimarburg/diz/labtofhir/LabRunner.java @@ -3,6 +3,7 @@ import de.unimarburg.diz.labtofhir.configuration.AdminClientProvider; import de.unimarburg.diz.labtofhir.model.MappingInfo; import java.util.Collections; +import java.util.Optional; import java.util.concurrent.ExecutionException; import javax.annotation.Nullable; import org.apache.kafka.clients.admin.Admin; @@ -104,8 +105,9 @@ public void onError( Throwable throwable) { LOG.debug( "Delete Consumer group failed: {}. " + "Retrying {}", - throwable - .getCause() + Optional + .ofNullable(throwable.getCause()) + .orElse(throwable) .getMessage(), context.getRetryCount()); } }) @@ -144,7 +146,8 @@ public void onApplicationEvent(UpdateCompleted event) { stopAndDeleteUpdateConsumer(); } catch (Exception e) { LOG.error("stopAndDeleteUpdateConsumer", e); - throw new RuntimeException(e); + throw new RuntimeException("Failed to delete update consumer group", + e); } } } diff --git a/src/main/java/de/unimarburg/diz/labtofhir/util/ResourceHelper.java b/src/main/java/de/unimarburg/diz/labtofhir/util/ResourceHelper.java index bf56eab..4e7ebc9 100644 --- a/src/main/java/de/unimarburg/diz/labtofhir/util/ResourceHelper.java +++ b/src/main/java/de/unimarburg/diz/labtofhir/util/ResourceHelper.java @@ -7,6 +7,7 @@ import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.HttpClientBuilder; @@ -42,13 +43,14 @@ public static Resource getMappingFile(String version, String user, clientBuilder.setProxy(HttpHost.create(proxyServer)); } - var response = clientBuilder - .build() - .execute(new HttpGet(String.format( + CloseableHttpResponse response; + try (var client = clientBuilder.build()) { + response = client.execute(new HttpGet(String.format( "https://gitlab.diz.uni-marburg.de/" + "api/v4/projects/63/packages/generic/" + "mapping-swl-loinc/%s/mapping-swl-loinc.zip", version))); + } LOG.info("Package registry responded with: " + response .getStatusLine() diff --git a/src/test/java/de/unimarburg/diz/labtofhir/LabRunnerTests.java b/src/test/java/de/unimarburg/diz/labtofhir/LabRunnerTests.java index df20789..256a8da 100644 --- a/src/test/java/de/unimarburg/diz/labtofhir/LabRunnerTests.java +++ b/src/test/java/de/unimarburg/diz/labtofhir/LabRunnerTests.java @@ -1,5 +1,6 @@ package de.unimarburg.diz.labtofhir; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.anyCollection; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; @@ -13,6 +14,7 @@ import de.unimarburg.diz.labtofhir.model.MappingUpdate; import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutionException; import org.apache.kafka.clients.admin.Admin; import org.apache.kafka.clients.consumer.OffsetAndMetadata; import org.apache.kafka.common.TopicPartition; @@ -31,7 +33,6 @@ public class LabRunnerTests { @Mock private AdminClientProvider kafkaAdmin; - @Test void runAlwaysStartsLabBinder() throws Exception { @@ -86,6 +87,42 @@ void runResetsUpdateConsumerOnExistingOffsets() throws Exception { verify(endpoint).changeState("update-in-0", State.STARTED); } + @Test + void onEventCompletedDeletesConsumer() + throws ExecutionException, InterruptedException { + // setup + var admin = setupAdminClient(); + var runner = setupRunner(null); + + // act + runner.onApplicationEvent(new UpdateCompleted(this)); + + // verify consumer stopped and consumer group deleted + verify(endpoint).changeState("update-in-0", State.STOPPED); + verify(admin + .deleteConsumerGroups(anyCollection()) + .all()).get(); + } + + @Test + void onEventCompletedFailureThrows() + throws ExecutionException, InterruptedException { + // setup + var admin = setupAdminClient(); + when(admin + .deleteConsumerGroups(anyCollection()) + .all() + .get()).thenThrow(new InterruptedException("oops")); + var runner = setupRunner(null); + + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy( + () -> runner.onApplicationEvent(new UpdateCompleted(this))) + .withCauseInstanceOf(InterruptedException.class) + .withMessage("Failed to delete update consumer group"); + + } + private LabRunner setupRunner(MappingInfo mappingInfo) { return new LabRunner(endpoint, kafkaAdmin, mappingInfo, "lab-update"); } diff --git a/src/test/java/de/unimarburg/diz/labtofhir/util/ResourceHelperTests.java b/src/test/java/de/unimarburg/diz/labtofhir/util/ResourceHelperTests.java new file mode 100644 index 0000000..56ca7a7 --- /dev/null +++ b/src/test/java/de/unimarburg/diz/labtofhir/util/ResourceHelperTests.java @@ -0,0 +1,54 @@ +package de.unimarburg.diz.labtofhir.util; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.FileInputStream; +import java.io.IOException; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.springframework.core.io.FileSystemResource; + +public class ResourceHelperTests { + + @Test + void getMappingFileCallsPackageRegistry() throws IOException { + + try (var staticBuilder = Mockito.mockStatic(HttpClientBuilder.class)) { + + // mocks + var builder = mock(HttpClientBuilder.class, + InvocationOnMock::callRealMethod); + var client = mock(CloseableHttpClient.class, RETURNS_DEEP_STUBS); + var response = mock(CloseableHttpResponse.class, + RETURNS_DEEP_STUBS); + + // stubs + staticBuilder + .when(HttpClientBuilder::create) + .thenReturn(builder); + when(builder.build()).thenReturn(client); + when(client.execute(any(HttpGet.class))).thenReturn(response); + when(response + .getStatusLine() + .toString()).thenReturn("200"); + when(response + .getEntity() + .getContent()).thenReturn(FileInputStream.nullInputStream()); + + var resource = ResourceHelper.getMappingFile("1.0.0", "user", + "password", "http://dummy-proxy", null); + + assertThat(resource).isInstanceOf(FileSystemResource.class); + } + } + +} From 6e4cfce1028dfc16ec1234a7f56d5598d39df9c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20St=C3=B6cker?= Date: Fri, 15 Mar 2024 14:09:38 +0100 Subject: [PATCH 13/21] test: add integration test --- build.gradle | 1 - .../LabToFhirConfigurationTests.java | 40 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 src/test/java/de/unimarburg/diz/labtofhir/LabToFhirConfigurationTests.java diff --git a/build.gradle b/build.gradle index 8dd0f70..d0596f8 100644 --- a/build.gradle +++ b/build.gradle @@ -52,7 +52,6 @@ dependencies { implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-csv:2.16.1' // unit tests - testImplementation "org.springframework.cloud:spring-cloud-stream:$springCloudStreamVersion" testImplementation 'org.apache.kafka:kafka-streams-test-utils:3.6.1' testImplementation("org.springframework.boot:spring-boot-starter-test:$springBootVersion") { diff --git a/src/test/java/de/unimarburg/diz/labtofhir/LabToFhirConfigurationTests.java b/src/test/java/de/unimarburg/diz/labtofhir/LabToFhirConfigurationTests.java new file mode 100644 index 0000000..4aaae00 --- /dev/null +++ b/src/test/java/de/unimarburg/diz/labtofhir/LabToFhirConfigurationTests.java @@ -0,0 +1,40 @@ +package de.unimarburg.diz.labtofhir; + +import static org.assertj.core.api.Assertions.assertThat; + +import de.unimarburg.diz.labtofhir.processor.LabToFhirProcessor; +import de.unimarburg.diz.labtofhir.processor.LabUpdateProcessor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.stream.endpoint.BindingsEndpoint; +import org.springframework.context.annotation.Import; +import org.springframework.kafka.test.context.EmbeddedKafka; +import org.springframework.test.context.TestPropertySource; + + +@EmbeddedKafka(partitions = 1, brokerProperties = { + "listeners=PLAINTEXT://localhost:9092", "port=9092"}) +@SpringBootTest +@Import(BindingsEndpoint.class) +@TestPropertySource(properties = {"mapping.loinc.version=''", + "mapping.loinc.credentials.user=''", + "mapping.loinc.credentials.password=''", + "mapping.loinc.local=mapping-swl-loinc.zip", + "spring.cloud.stream.kafka.streams.binder.replicationFactor=1", + "spring.cloud.stream.kafka.streams.binder.minPartitionCount=1"}) +public class LabToFhirConfigurationTests { + + @Autowired + private LabToFhirProcessor labProcessor; + + @Autowired + private LabUpdateProcessor updateProcessor; + + @Test + void contexLoads() { + assertThat(labProcessor).isNotNull(); + assertThat(updateProcessor).isNotNull(); + } + +} From 13f97ebaa9f376d2d847fe568a20b898dca4cd32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20St=C3=B6cker?= Date: Fri, 15 Mar 2024 16:47:21 +0100 Subject: [PATCH 14/21] test: add equality tests --- build.gradle | 1 + .../diz/labtofhir/model/LoincMapEntry.java | 106 +++++++++++++----- .../diz/labtofhir/util/ResourceHelper.java | 18 +-- .../labtofhir/mapper/LoincMapperTests.java | 25 +++-- .../labtofhir/model/LoincMapEntryTests.java | 28 +---- .../diz/labtofhir/model/LoincMapTests.java | 28 +++-- .../processor/BaseProcessorTests.java | 70 ++++++++++++ .../processor/LabToFhirProcessorTests.java | 38 +------ .../processor/LabUpdateProcessorTests.java | 50 +-------- 9 files changed, 205 insertions(+), 159 deletions(-) create mode 100644 src/test/java/de/unimarburg/diz/labtofhir/processor/BaseProcessorTests.java diff --git a/build.gradle b/build.gradle index d0596f8..2067266 100644 --- a/build.gradle +++ b/build.gradle @@ -53,6 +53,7 @@ dependencies { // unit tests testImplementation 'org.apache.kafka:kafka-streams-test-utils:3.6.1' + testImplementation 'nl.jqno.equalsverifier:equalsverifier:3.15.8' testImplementation("org.springframework.boot:spring-boot-starter-test:$springBootVersion") { exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' diff --git a/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMapEntry.java b/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMapEntry.java index ab43dde..824c744 100644 --- a/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMapEntry.java +++ b/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMapEntry.java @@ -1,55 +1,73 @@ package de.unimarburg.diz.labtofhir.model; -import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import java.util.Objects; -import org.apache.commons.lang3.StringUtils; -public class LoincMapEntry { +@JsonDeserialize(builder = LoincMapEntry.Builder.class) +public final class LoincMapEntry { - private String swl; - private String loinc; - private String ucum; - private String meta; + private final String swl; + private final String loinc; + private final String ucum; + private final String meta; + + private LoincMapEntry(String swl, String loinc, String ucum, String meta) { + this.swl = swl; + this.loinc = loinc; + this.ucum = ucum; + this.meta = meta; + } + + // @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + // public LoincMapEntry(@JsonProperty("CODE") String swl, + // @JsonProperty("LOINC") String loinc, @JsonProperty("UCUM") String ucum, + // @JsonProperty("QUELLE") String meta) { + // this.swl = swl; + // this.loinc = loinc; + // this.ucum = ucum; + // this.meta = meta; + // } public String getSwl() { return this.swl; } - @JsonSetter("CODE") - public void setSwl(String swl) { - this.swl = swl; - } + // @JsonSetter("CODE") + // public void setSwl(String swl) { + // this.swl = swl; + // } public String getMeta() { return meta; } - @JsonSetter("QUELLE") - public LoincMapEntry setMeta(String meta) { - this.meta = StringUtils.isBlank(meta) ? null : meta; - - return this; - } + // @JsonSetter("QUELLE") + // public LoincMapEntry setMeta(String meta) { + // this.meta = StringUtils.isBlank(meta) ? null : meta; + // + // return this; + // } public String getLoinc() { return this.loinc; } - @JsonSetter("LOINC") - public LoincMapEntry setLoinc(String loinc) { - this.loinc = loinc; - return this; - } + // @JsonSetter("LOINC") + // public LoincMapEntry setLoinc(String loinc) { + // this.loinc = loinc; + // return this; + // } public String getUcum() { return this.ucum; } - @JsonSetter("UCUM_WERT") - public LoincMapEntry setUcum(String ucum) { - this.ucum = ucum; - return this; - } + // @JsonSetter("UCUM_WERT") + // public LoincMapEntry setUcum(String ucum) { + // this.ucum = ucum; + // return this; + // } @Override public boolean equals(Object o) { @@ -69,4 +87,38 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(swl, loinc, ucum, meta); } + + @JsonPOJOBuilder + public static class Builder { + + private String swl; + private String loinc; + private String ucum; + private String meta; + + public Builder withSwl(String swl) { + this.swl = swl; + return this; + } + + public Builder withLoinc(String loinc) { + this.loinc = loinc; + return this; + } + + public Builder withUcum(String ucum) { + this.ucum = ucum; + return this; + } + + public Builder withMeta(String meta) { + this.meta = meta; + return this; + } + + + public LoincMapEntry build() { + return new LoincMapEntry(swl, loinc, ucum, meta); + } + } } diff --git a/src/main/java/de/unimarburg/diz/labtofhir/util/ResourceHelper.java b/src/main/java/de/unimarburg/diz/labtofhir/util/ResourceHelper.java index 4e7ebc9..f1c7c27 100644 --- a/src/main/java/de/unimarburg/diz/labtofhir/util/ResourceHelper.java +++ b/src/main/java/de/unimarburg/diz/labtofhir/util/ResourceHelper.java @@ -50,18 +50,18 @@ public static Resource getMappingFile(String version, String user, + "api/v4/projects/63/packages/generic/" + "mapping-swl-loinc/%s/mapping-swl-loinc.zip", version))); - } - LOG.info("Package registry responded with: " + response - .getStatusLine() - .toString()); + LOG.info("Package registry responded with: " + response + .getStatusLine() + .toString()); - var tmpFile = File.createTempFile("download", ".zip"); - StreamUtils.copy(response - .getEntity() - .getContent(), new FileOutputStream(tmpFile)); + var tmpFile = File.createTempFile("download", ".zip"); + StreamUtils.copy(response + .getEntity() + .getContent(), new FileOutputStream(tmpFile)); - return new FileSystemResource(tmpFile); + return new FileSystemResource(tmpFile); + } } else { diff --git a/src/test/java/de/unimarburg/diz/labtofhir/mapper/LoincMapperTests.java b/src/test/java/de/unimarburg/diz/labtofhir/mapper/LoincMapperTests.java index 523576a..6b092c7 100644 --- a/src/test/java/de/unimarburg/diz/labtofhir/mapper/LoincMapperTests.java +++ b/src/test/java/de/unimarburg/diz/labtofhir/mapper/LoincMapperTests.java @@ -58,17 +58,20 @@ private static Stream mapCodeAndQuantityProvidesMetaCodeArguments() { public static void init() { testLoincMap = new LoincMap(); var testCode = "TEST"; - testLoincMap.put(testCode, new LoincMapEntry() - .setLoinc("1000-0") - .setUcum("mmol/L")); - testLoincMap.put(testCode, new LoincMapEntry() - .setMeta("meta1") - .setLoinc("1000-1") - .setUcum("mmol/L")); - testLoincMap.put(testCode, new LoincMapEntry() - .setMeta("meta2") - .setLoinc("1000-2") - .setUcum("mmol/L")); + testLoincMap.put(testCode, new LoincMapEntry.Builder() + .withLoinc("1000-0") + .withUcum("mmol/L") + .build()); + testLoincMap.put(testCode, new LoincMapEntry.Builder() + .withMeta("meta1") + .withLoinc("1000-1") + .withUcum("mmol/L") + .build()); + testLoincMap.put(testCode, new LoincMapEntry.Builder() + .withMeta("meta2") + .withLoinc("1000-2") + .withUcum("mmol/L") + .build()); } diff --git a/src/test/java/de/unimarburg/diz/labtofhir/model/LoincMapEntryTests.java b/src/test/java/de/unimarburg/diz/labtofhir/model/LoincMapEntryTests.java index f55b4e8..a1fd753 100644 --- a/src/test/java/de/unimarburg/diz/labtofhir/model/LoincMapEntryTests.java +++ b/src/test/java/de/unimarburg/diz/labtofhir/model/LoincMapEntryTests.java @@ -1,33 +1,15 @@ package de.unimarburg.diz.labtofhir.model; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; - +import nl.jqno.equalsverifier.EqualsVerifier; import org.junit.jupiter.api.Test; public class LoincMapEntryTests { @Test - void entryIsEqual() { - var entry = new LoincMapEntry(); - entry.setSwl("swisslab"); - entry.setLoinc("loinc"); - entry.setUcum("ucum"); - entry.setMeta("meta"); - var entry2 = new LoincMapEntry(); - entry2.setSwl("swisslab"); - entry2.setLoinc("loinc"); - entry2.setUcum("ucum"); - entry2.setMeta("meta"); - var entry3 = new LoincMapEntry(); - entry3.setSwl("swisslab"); - entry3.setLoinc("loinc"); - entry3.setUcum("ucum"); - entry3.setMeta("META"); - - assertThat(entry.equals(entry)).isTrue(); - assertThat(entry.equals(entry2)).isTrue(); - assertThat(entry.equals(null)).isFalse(); - assertThat(entry.equals(entry3)).isFalse(); + public void entryIsEqual() { + EqualsVerifier + .forClass(LoincMapEntry.class) + .verify(); } } diff --git a/src/test/java/de/unimarburg/diz/labtofhir/model/LoincMapTests.java b/src/test/java/de/unimarburg/diz/labtofhir/model/LoincMapTests.java index 9ced3c0..a1b8b31 100644 --- a/src/test/java/de/unimarburg/diz/labtofhir/model/LoincMapTests.java +++ b/src/test/java/de/unimarburg/diz/labtofhir/model/LoincMapTests.java @@ -10,20 +10,24 @@ public class LoincMapTests { @Test void diffCreatesCodeDiff() { var original = new LoincMap(); - original.put("FOO", new LoincMapEntry() - .setLoinc("1000-0") - .setUcum("mmol/L")); - original.put("BAR", new LoincMapEntry() - .setLoinc("1000-0") - .setUcum("mmol/L")); + original.put("FOO", new LoincMapEntry.Builder() + .withLoinc("1000-0") + .withUcum("mmol/L") + .build()); + original.put("BAR", new LoincMapEntry.Builder() + .withLoinc("1000-0") + .withUcum("mmol/L") + .build()); var from = new LoincMap(); - from.put("FOO", new LoincMapEntry() - .setLoinc("1000-0") - .setUcum("mmol/L")); - from.put("BAR", new LoincMapEntry() - .setLoinc("1111-0") - .setUcum("mmol/L")); + from.put("FOO", new LoincMapEntry.Builder() + .withLoinc("1000-0") + .withUcum("mmol/L") + .build()); + from.put("BAR", new LoincMapEntry.Builder() + .withLoinc("1111-0") + .withUcum("mmol/L") + .build()); var diff = original.diff(from); diff --git a/src/test/java/de/unimarburg/diz/labtofhir/processor/BaseProcessorTests.java b/src/test/java/de/unimarburg/diz/labtofhir/processor/BaseProcessorTests.java new file mode 100644 index 0000000..7eb4afe --- /dev/null +++ b/src/test/java/de/unimarburg/diz/labtofhir/processor/BaseProcessorTests.java @@ -0,0 +1,70 @@ +package de.unimarburg.diz.labtofhir.processor; + +import de.unimarburg.diz.labtofhir.model.LaboratoryReport; +import de.unimarburg.diz.labtofhir.serde.JsonSerdes; +import de.unimarburg.diz.labtofhir.serializer.FhirDeserializer; +import de.unimarburg.diz.labtofhir.serializer.FhirSerializer; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Stream; +import org.apache.kafka.common.serialization.Serdes; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.apache.kafka.common.serialization.StringSerializer; +import org.apache.kafka.streams.StreamsBuilder; +import org.apache.kafka.streams.TestInputTopic; +import org.apache.kafka.streams.TestOutputTopic; +import org.apache.kafka.streams.TopologyTestDriver; +import org.apache.kafka.streams.kstream.Consumed; +import org.apache.kafka.streams.kstream.KStream; +import org.apache.kafka.streams.kstream.Produced; +import org.apache.kafka.streams.test.TestRecord; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Observation; +import org.springframework.kafka.support.serializer.JsonSerializer; + +abstract class BaseProcessorTests { + + Stream getObservationsCodes( + List> records) { + return records + .stream() + .flatMap(b -> b + .getValue() + .getEntry() + .stream() + .map(BundleEntryComponent::getResource)) + .filter(Observation.class::isInstance) + .map(Observation.class::cast) + .map(Observation::getCode); + } + + TestInputTopic createInputTopic( + TopologyTestDriver driver) { + return driver.createInputTopic("lab", new StringSerializer(), + new JsonSerializer<>()); + } + + TestOutputTopic createOutputTopic( + TopologyTestDriver driver) { + return driver.createOutputTopic("lab-mapped", new StringDeserializer(), + new FhirDeserializer<>(Bundle.class)); + } + + TopologyTestDriver buildStream( + Function, KStream> processor) { + var builder = new StreamsBuilder(); + final KStream labStream = builder.stream( + "lab", + Consumed.with(Serdes.String(), JsonSerdes.laboratoryReport())); + + processor + .apply(labStream) + .to("lab-mapped", Produced.with(Serdes.String(), + Serdes.serdeFrom(new FhirSerializer<>(), + new FhirDeserializer<>(Bundle.class)))); + + return new TopologyTestDriver(builder.build()); + } +} diff --git a/src/test/java/de/unimarburg/diz/labtofhir/processor/LabToFhirProcessorTests.java b/src/test/java/de/unimarburg/diz/labtofhir/processor/LabToFhirProcessorTests.java index d7c022e..e7784c5 100644 --- a/src/test/java/de/unimarburg/diz/labtofhir/processor/LabToFhirProcessorTests.java +++ b/src/test/java/de/unimarburg/diz/labtofhir/processor/LabToFhirProcessorTests.java @@ -9,19 +9,11 @@ import de.unimarburg.diz.labtofhir.mapper.MiiLabReportMapper; import de.unimarburg.diz.labtofhir.model.LaboratoryReport; import de.unimarburg.diz.labtofhir.serde.JsonSerdes; -import de.unimarburg.diz.labtofhir.serializer.FhirDeserializer; -import de.unimarburg.diz.labtofhir.serializer.FhirSerializer; import java.util.List; import org.apache.kafka.common.serialization.Serdes; -import org.apache.kafka.common.serialization.StringDeserializer; -import org.apache.kafka.common.serialization.StringSerializer; import org.apache.kafka.streams.StreamsBuilder; -import org.apache.kafka.streams.TopologyTestDriver; import org.apache.kafka.streams.kstream.Consumed; import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.Produced; -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.DiagnosticReport; @@ -34,7 +26,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.kafka.support.serializer.JsonSerializer; import org.springframework.test.context.TestPropertySource; @@ -45,7 +36,7 @@ "mapping.loinc.credentials.password=''", "mapping.loinc.local=mapping-swl-loinc.zip"}) -public class LabToFhirProcessorTests { +public class LabToFhirProcessorTests extends BaseProcessorTests { @Autowired private LabToFhirProcessor processor; @@ -64,19 +55,11 @@ public void observationIsLoincMapped() { "lab", Consumed.with(Serdes.String(), JsonSerdes.laboratoryReport())); - processor - .process() - .apply(labStream) - .to("lab-mapped", Produced.with(Serdes.String(), - Serdes.serdeFrom(new FhirSerializer<>(), - new FhirDeserializer<>(Bundle.class)))); - - try (var driver = new TopologyTestDriver(builder.build())) { + // build stream + try (var driver = buildStream(processor.process())) { - var labTopic = driver.createInputTopic("lab", - new StringSerializer(), new JsonSerializer<>()); - var outputTopic = driver.createOutputTopic("lab-mapped", - new StringDeserializer(), new FhirDeserializer<>(Bundle.class)); + var labTopic = createInputTopic(driver); + var outputTopic = createOutputTopic(driver); var labReport = new LaboratoryReport(); labReport.setId(42); @@ -103,16 +86,7 @@ public void observationIsLoincMapped() { // get record from output topic var outputRecords = outputTopic.readRecordsToList(); - var obsCodes = outputRecords - .stream() - .flatMap(b -> b - .getValue() - .getEntry() - .stream() - .map(BundleEntryComponent::getResource)) - .filter(Observation.class::isInstance) - .map(Observation.class::cast) - .map(Observation::getCode) + var obsCodes = getObservationsCodes(outputRecords) .findAny() .orElseThrow(); diff --git a/src/test/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessorTests.java b/src/test/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessorTests.java index 94f9f44..c285613 100644 --- a/src/test/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessorTests.java +++ b/src/test/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessorTests.java @@ -12,25 +12,11 @@ import de.unimarburg.diz.labtofhir.model.MappingInfo; import de.unimarburg.diz.labtofhir.model.MappingUpdate; import de.unimarburg.diz.labtofhir.processor.LabUpdateProcessorTests.KafkaConfig; -import de.unimarburg.diz.labtofhir.serde.JsonSerdes; -import de.unimarburg.diz.labtofhir.serializer.FhirDeserializer; -import de.unimarburg.diz.labtofhir.serializer.FhirSerializer; import java.util.List; import java.util.Map; import org.apache.kafka.clients.consumer.OffsetAndMetadata; -import org.apache.kafka.common.serialization.Serdes; -import org.apache.kafka.common.serialization.StringDeserializer; -import org.apache.kafka.common.serialization.StringSerializer; import org.apache.kafka.streams.KeyValue; -import org.apache.kafka.streams.StreamsBuilder; -import org.apache.kafka.streams.TestInputTopic; -import org.apache.kafka.streams.TopologyTestDriver; -import org.apache.kafka.streams.kstream.Consumed; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.Produced; import org.apache.kafka.streams.test.TestRecord; -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.DiagnosticReport; @@ -45,7 +31,6 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; -import org.springframework.kafka.support.serializer.JsonSerializer; import org.springframework.test.context.TestPropertySource; @SpringBootTest(classes = {LabUpdateProcessor.class, MiiLabReportMapper.class, @@ -55,7 +40,7 @@ "mapping.loinc.credentials.user=''", "mapping.loinc.credentials.password=''", "mapping.loinc.local=mapping-swl-loinc.zip"}) -public class LabUpdateProcessorTests { +public class LabUpdateProcessorTests extends BaseProcessorTests { @Autowired private LabUpdateProcessor processor; @@ -69,24 +54,10 @@ public class LabUpdateProcessorTests { void updateIsProcessed() { // build stream - var builder = new StreamsBuilder(); - final KStream labStream = builder.stream( - "lab", - Consumed.with(Serdes.String(), JsonSerdes.laboratoryReport())); + try (var driver = buildStream(processor.update())) { - processor - .update() - .apply(labStream) - .to("output-topic", Produced.with(Serdes.String(), - Serdes.serdeFrom(new FhirSerializer<>(), - new FhirDeserializer<>(Bundle.class)))); - - try (var driver = new TopologyTestDriver(builder.build())) { - - TestInputTopic labTopic = driver.createInputTopic( - "lab", new StringSerializer(), new JsonSerializer<>()); - var outputTopic = driver.createOutputTopic("output-topic", - new StringDeserializer(), new FhirDeserializer<>(Bundle.class)); + var labTopic = createInputTopic(driver); + var outputTopic = createOutputTopic(driver); var inputReports = List.of(createReport(1, "NA"), createReport(2, "ERY"), createReport(3, "NA"), @@ -108,17 +79,7 @@ void updateIsProcessed() { .toList()).isEqualTo(List.of("1", "3")); // assert codes are mapped - var obsCodes = outputRecords - .stream() - .flatMap(b -> b - .getValue() - .getEntry() - .stream() - .map(BundleEntryComponent::getResource)) - .filter(Observation.class::isInstance) - .map(Observation.class::cast) - .map(Observation::getCode) - .toList(); + var obsCodes = getObservationsCodes(outputRecords).toList(); assertThat(obsCodes).allMatch( c -> c.hasCoding("http://loinc.org", "2951-2")); @@ -164,5 +125,4 @@ MappingInfo testMappingInfo() { false); } } - } From 97b8d7975bba0ca4d6c0064b51e35017f7a373bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20St=C3=B6cker?= Date: Mon, 18 Mar 2024 12:54:51 +0100 Subject: [PATCH 15/21] ci(lint): fix line length --- .../diz/labtofhir/model/LoincMapEntry.java | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMapEntry.java b/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMapEntry.java index 824c744..c8c5fc7 100644 --- a/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMapEntry.java +++ b/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMapEntry.java @@ -19,56 +19,22 @@ private LoincMapEntry(String swl, String loinc, String ucum, String meta) { this.meta = meta; } - // @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) - // public LoincMapEntry(@JsonProperty("CODE") String swl, - // @JsonProperty("LOINC") String loinc, @JsonProperty("UCUM") String ucum, - // @JsonProperty("QUELLE") String meta) { - // this.swl = swl; - // this.loinc = loinc; - // this.ucum = ucum; - // this.meta = meta; - // } - public String getSwl() { return this.swl; } - // @JsonSetter("CODE") - // public void setSwl(String swl) { - // this.swl = swl; - // } - public String getMeta() { return meta; } - // @JsonSetter("QUELLE") - // public LoincMapEntry setMeta(String meta) { - // this.meta = StringUtils.isBlank(meta) ? null : meta; - // - // return this; - // } - public String getLoinc() { return this.loinc; } - // @JsonSetter("LOINC") - // public LoincMapEntry setLoinc(String loinc) { - // this.loinc = loinc; - // return this; - // } - public String getUcum() { return this.ucum; } - // @JsonSetter("UCUM_WERT") - // public LoincMapEntry setUcum(String ucum) { - // this.ucum = ucum; - // return this; - // } - @Override public boolean equals(Object o) { if (this == o) { From 1f1351d58df1a546d812769b54376955df14a9f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20St=C3=B6cker?= Date: Mon, 18 Mar 2024 13:10:43 +0100 Subject: [PATCH 16/21] ci(lint): fix line length --- .../unimarburg/diz/labtofhir/processor/BaseProcessorTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/de/unimarburg/diz/labtofhir/processor/BaseProcessorTests.java b/src/test/java/de/unimarburg/diz/labtofhir/processor/BaseProcessorTests.java index 7eb4afe..e3afcd3 100644 --- a/src/test/java/de/unimarburg/diz/labtofhir/processor/BaseProcessorTests.java +++ b/src/test/java/de/unimarburg/diz/labtofhir/processor/BaseProcessorTests.java @@ -52,6 +52,7 @@ TestOutputTopic createOutputTopic( new FhirDeserializer<>(Bundle.class)); } + @SuppressWarnings("checkstyle:LineLength") TopologyTestDriver buildStream( Function, KStream> processor) { var builder = new StreamsBuilder(); From a2506d39ad6c1b9efa52839a3a48a5b07f193562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20St=C3=B6cker?= Date: Mon, 18 Mar 2024 13:37:42 +0100 Subject: [PATCH 17/21] ci(lint): refactor processor test code to base class --- .../processor/BaseProcessorTests.java | 29 +++++++++++- .../processor/LabToFhirProcessorTests.java | 45 +++---------------- .../processor/LabUpdateProcessorTests.java | 33 +++----------- 3 files changed, 38 insertions(+), 69 deletions(-) diff --git a/src/test/java/de/unimarburg/diz/labtofhir/processor/BaseProcessorTests.java b/src/test/java/de/unimarburg/diz/labtofhir/processor/BaseProcessorTests.java index e3afcd3..8e89929 100644 --- a/src/test/java/de/unimarburg/diz/labtofhir/processor/BaseProcessorTests.java +++ b/src/test/java/de/unimarburg/diz/labtofhir/processor/BaseProcessorTests.java @@ -21,9 +21,17 @@ import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.DiagnosticReport; +import org.hl7.fhir.r4.model.Encounter; +import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Quantity; +import org.hl7.fhir.r4.model.Reference; import org.springframework.kafka.support.serializer.JsonSerializer; +@SuppressWarnings("CheckStyle") abstract class BaseProcessorTests { Stream getObservationsCodes( @@ -52,7 +60,6 @@ TestOutputTopic createOutputTopic( new FhirDeserializer<>(Bundle.class)); } - @SuppressWarnings("checkstyle:LineLength") TopologyTestDriver buildStream( Function, KStream> processor) { var builder = new StreamsBuilder(); @@ -68,4 +75,24 @@ TopologyTestDriver buildStream( return new TopologyTestDriver(builder.build()); } + + LaboratoryReport createReport(int reportId, Coding labCoding) { + var report = new LaboratoryReport(); + report.setId(reportId); + report.setResource(new DiagnosticReport() + .addIdentifier(new Identifier().setValue("report-id")) + .setSubject(new Reference( + new Patient().addIdentifier(new Identifier().setValue("1")))) + .setEncounter(new Reference(new Encounter().addIdentifier( + new Identifier().setValue("1"))))); + + var obs = new Observation() + .addIdentifier(new Identifier().setValue("obs-id")) + .setCode(new CodeableConcept().setCoding(List.of(labCoding))) + .setValue(new Quantity(1)); + obs.setId("obs-id"); + report.setObservations(List.of(obs)); + + return report; + } } diff --git a/src/test/java/de/unimarburg/diz/labtofhir/processor/LabToFhirProcessorTests.java b/src/test/java/de/unimarburg/diz/labtofhir/processor/LabToFhirProcessorTests.java index e7784c5..4f77b80 100644 --- a/src/test/java/de/unimarburg/diz/labtofhir/processor/LabToFhirProcessorTests.java +++ b/src/test/java/de/unimarburg/diz/labtofhir/processor/LabToFhirProcessorTests.java @@ -7,22 +7,7 @@ import de.unimarburg.diz.labtofhir.configuration.MappingConfiguration; import de.unimarburg.diz.labtofhir.mapper.LoincMapper; import de.unimarburg.diz.labtofhir.mapper.MiiLabReportMapper; -import de.unimarburg.diz.labtofhir.model.LaboratoryReport; -import de.unimarburg.diz.labtofhir.serde.JsonSerdes; -import java.util.List; -import org.apache.kafka.common.serialization.Serdes; -import org.apache.kafka.streams.StreamsBuilder; -import org.apache.kafka.streams.kstream.Consumed; -import org.apache.kafka.streams.kstream.KStream; -import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; -import org.hl7.fhir.r4.model.DiagnosticReport; -import org.hl7.fhir.r4.model.Encounter; -import org.hl7.fhir.r4.model.Identifier; -import org.hl7.fhir.r4.model.Observation; -import org.hl7.fhir.r4.model.Patient; -import org.hl7.fhir.r4.model.Quantity; -import org.hl7.fhir.r4.model.Reference; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -48,37 +33,17 @@ public class LabToFhirProcessorTests extends BaseProcessorTests { @SuppressWarnings("checkstyle:MagicNumber") @Test public void observationIsLoincMapped() { - - // build stream - var builder = new StreamsBuilder(); - final KStream labStream = builder.stream( - "lab", - Consumed.with(Serdes.String(), JsonSerdes.laboratoryReport())); - // build stream try (var driver = buildStream(processor.process())) { var labTopic = createInputTopic(driver); var outputTopic = createOutputTopic(driver); - var labReport = new LaboratoryReport(); - labReport.setId(42); - labReport.setResource(new DiagnosticReport() - .addIdentifier(new Identifier().setValue("report-id")) - .setSubject(new Reference(new Patient().addIdentifier( - new Identifier().setValue("1")))) - .setEncounter(new Reference(new Encounter().addIdentifier( - new Identifier().setValue("1"))))); - - var obs = new Observation() - .addIdentifier(new Identifier().setValue("obs-id")) - .setCode(new CodeableConcept().setCoding(List.of(new Coding( - fhirProperties - .getSystems() - .getLaboratorySystem(), "NA", null)))) - .setValue(new Quantity(1)); - obs.setId("obs-id"); - labReport.setObservations(List.of(obs)); + var labReport = createReport(42, new Coding() + .setSystem(fhirProperties + .getSystems() + .getLaboratorySystem()) + .setCode("NA")); // create input record labTopic.pipeInput(String.valueOf(labReport.getId()), labReport); diff --git a/src/test/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessorTests.java b/src/test/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessorTests.java index c285613..fc7e907 100644 --- a/src/test/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessorTests.java +++ b/src/test/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessorTests.java @@ -17,15 +17,7 @@ import org.apache.kafka.clients.consumer.OffsetAndMetadata; import org.apache.kafka.streams.KeyValue; import org.apache.kafka.streams.test.TestRecord; -import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; -import org.hl7.fhir.r4.model.DiagnosticReport; -import org.hl7.fhir.r4.model.Encounter; -import org.hl7.fhir.r4.model.Identifier; -import org.hl7.fhir.r4.model.Observation; -import org.hl7.fhir.r4.model.Patient; -import org.hl7.fhir.r4.model.Quantity; -import org.hl7.fhir.r4.model.Reference; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -87,26 +79,11 @@ void updateIsProcessed() { } private LaboratoryReport createReport(int reportId, String labCode) { - var report = new LaboratoryReport(); - report.setId(reportId); - report.setResource(new DiagnosticReport() - .addIdentifier(new Identifier().setValue("report-id")) - .setSubject(new Reference( - new Patient().addIdentifier(new Identifier().setValue("1")))) - .setEncounter(new Reference(new Encounter().addIdentifier( - new Identifier().setValue("1"))))); - - var obs = new Observation() - .addIdentifier(new Identifier().setValue("obs-id")) - .setCode(new CodeableConcept().setCoding(List.of(new Coding( - fhirProperties - .getSystems() - .getLaboratorySystem(), labCode, null)))) - .setValue(new Quantity(1)); - obs.setId("obs-id"); - report.setObservations(List.of(obs)); - - return report; + return createReport(reportId, new Coding() + .setSystem(fhirProperties + .getSystems() + .getLaboratorySystem()) + .setCode(labCode)); } @TestConfiguration From c713d982c4c7400d666e26ea14bf6673084506c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20St=C3=B6cker?= Date: Mon, 18 Mar 2024 15:46:01 +0100 Subject: [PATCH 18/21] test: fix lab processor test --- .../de/unimarburg/diz/labtofhir/model/LoincMap.java | 2 ++ .../diz/labtofhir/model/LoincMapEntry.java | 13 +++++++++---- .../diz/labtofhir/processor/BaseProcessorTests.java | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMap.java b/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMap.java index bc622a6..070408d 100644 --- a/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMap.java +++ b/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMap.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.dataformat.csv.CsvMapper; +import com.fasterxml.jackson.dataformat.csv.CsvParser; import com.fasterxml.jackson.dataformat.csv.CsvSchema; import java.io.IOException; import java.io.InputStream; @@ -154,6 +155,7 @@ private List loadItems(Class type, InputStream inputStream, MappingIterator readValues = mapper .readerFor(type) .with(bootstrapSchema) + .withFeatures(CsvParser.Feature.EMPTY_STRING_AS_NULL) .readValues(inputStream); return readValues.readAll(); } catch (IOException e) { diff --git a/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMapEntry.java b/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMapEntry.java index c8c5fc7..204ff89 100644 --- a/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMapEntry.java +++ b/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMapEntry.java @@ -1,5 +1,6 @@ package de.unimarburg.diz.labtofhir.model; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import java.util.Objects; @@ -57,13 +58,17 @@ public int hashCode() { @JsonPOJOBuilder public static class Builder { - private String swl; + @JsonProperty("CODE") + private String code; + @JsonProperty("LOINC") private String loinc; + @JsonProperty("UCUM_WERT") private String ucum; + @JsonProperty("QUELLE") private String meta; - public Builder withSwl(String swl) { - this.swl = swl; + public Builder withCode(String swl) { + this.code = swl; return this; } @@ -84,7 +89,7 @@ public Builder withMeta(String meta) { public LoincMapEntry build() { - return new LoincMapEntry(swl, loinc, ucum, meta); + return new LoincMapEntry(code, loinc, ucum, meta); } } } diff --git a/src/test/java/de/unimarburg/diz/labtofhir/processor/BaseProcessorTests.java b/src/test/java/de/unimarburg/diz/labtofhir/processor/BaseProcessorTests.java index 8e89929..8ac286f 100644 --- a/src/test/java/de/unimarburg/diz/labtofhir/processor/BaseProcessorTests.java +++ b/src/test/java/de/unimarburg/diz/labtofhir/processor/BaseProcessorTests.java @@ -31,7 +31,7 @@ import org.hl7.fhir.r4.model.Reference; import org.springframework.kafka.support.serializer.JsonSerializer; -@SuppressWarnings("CheckStyle") +@SuppressWarnings("checkstyle:LineLength") abstract class BaseProcessorTests { Stream getObservationsCodes( From 1ae1bdbfadf26cbce28dbb7a5aa49533a7242fc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20St=C3=B6cker?= Date: Thu, 16 May 2024 22:59:04 +0200 Subject: [PATCH 19/21] fix: update processor offset handling --- dev/compose-data.yaml | 2 +- dev/lab-data-single.ndjson | 1 - dev/lab-data.ndjson | 4 +- .../unimarburg/diz/labtofhir/LabRunner.java | 35 ++----- .../configuration/KafkaConfiguration.java | 56 +++++----- .../labtofhir/mapper/MiiLabReportMapper.java | 2 +- .../diz/labtofhir/model/LoincMapEntry.java | 4 +- .../processor/LabUpdateProcessor.java | 96 +++++++++--------- src/main/resources/application.yml | 2 +- src/main/resources/mapping-swl-loinc.zip | Bin 114860 -> 118841 bytes .../processor/LabUpdateProcessorTests.java | 28 ++--- src/test/resources/mapping-swl-loinc.zip | Bin 114860 -> 118841 bytes 12 files changed, 106 insertions(+), 124 deletions(-) delete mode 100644 dev/lab-data-single.ndjson diff --git a/dev/compose-data.yaml b/dev/compose-data.yaml index ff0e516..b22378e 100644 --- a/dev/compose-data.yaml +++ b/dev/compose-data.yaml @@ -9,5 +9,5 @@ services: command: > "kafkacat -b localhost:9092 -K: -t laboratory -P -l /data/lab-data.ndjson" volumes: - - ./lab-data-single.ndjson:/data/lab-data.ndjson:ro + - ./lab-data.ndjson:/data/lab-data.ndjson:ro network_mode: host diff --git a/dev/lab-data-single.ndjson b/dev/lab-data-single.ndjson deleted file mode 100644 index 31cc865..0000000 --- a/dev/lab-data-single.ndjson +++ /dev/null @@ -1 +0,0 @@ -3:{"id":3,"fhir":"{\"code\": {\"text\": \"Laborwerte\", \"coding\": [{\"code\": \"11502-2\", \"system\": \"http://loinc.org\", \"display\": \"Laborwerte\"}]}, \"text\": {\"div\": \"
Laborwerte
StatusPARTIAL
NameValueInterpretationReference RangeStatus
Natrium 135 mmol/l 134 mmol/l - 143 mmol/l FINAL
Kalium 4.2 mmol/l 3.3 mmol/l - 4.6 mmol/l FINAL
Calcium 2.39 mmol/l 2.20 mmol/l - 2.66 mmol/l FINAL
AST (GOT) 129 U/l 15 U/l - 41 U/l FINAL
ALT (GPT) 104 U/l 8 U/l - 45 U/l FINAL
Alk. Phosphatase 225 U/l 75 U/l - 363 U/l FINAL
GGT 37 U/l 6 U/l - 30 U/l FINAL
Kreatinin 0.34 mg/dl 0.26 mg/dl - 0.77 mg/dl FINAL
Leukozyten 25.5 G/l 4.5 G/l - 11.4 G/l FINAL
Erythrozyten 2.5 T/l 4.10 T/l - 5.55 T/l FINAL
Hämoglobin 92 g/l 125 g/l - 160 g/l FINAL
Hämatokrit 0.26 l/l 0.37 l/l - 0.48 l/l FINAL
MCV 103 fl 78 fl - 93 fl FINAL
MCH 37 pg 26.0 pg - 32.5 pg FINAL
MCHC 359 g/l Ery 315 g/l Ery - 360 g/l Ery FINAL
Thrombozyten 891 G/l 170 G/l - 400 G/l PRELIMINARY
Normoblasten folgtREGISTERED
\", \"status\": \"generated\"}, \"result\": [{\"reference\": \"#embedded-observation-1\"}, {\"reference\": \"#embedded-observation-2\"}, {\"reference\": \"#embedded-observation-3\"}, {\"reference\": \"#embedded-observation-4\"}, {\"reference\": \"#embedded-observation-5\"}, {\"reference\": \"#embedded-observation-6\"}, {\"reference\": \"#embedded-observation-7\"}, {\"reference\": \"#embedded-observation-8\"}, {\"reference\": \"#embedded-observation-9\"}, {\"reference\": \"#embedded-observation-10\"}, {\"reference\": \"#embedded-observation-11\"}, {\"reference\": \"#embedded-observation-12\"}, {\"reference\": \"#embedded-observation-13\"}, {\"reference\": \"#embedded-observation-14\"}, {\"reference\": \"#embedded-observation-15\"}, {\"reference\": \"#embedded-observation-16\"}, {\"reference\": \"#embedded-observation-17\"}], \"status\": \"partial\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"KLIN\"}], \"contained\": [{\"id\": \"embedded-patient\", \"name\": [{\"given\": [\"Testvorname\"], \"family\": \"Testnachname\"}], \"gender\": \"male\", \"birthDate\": \"1990-06-01\", \"identifier\": [{\"value\": \"599999\"}], \"resourceType\": \"Patient\"}, {\"id\": \"embedded-visit\", \"class\": {\"code\": \"AMB\", \"system\": \"http://terminology.hl7.org/CodeSystem/v3-ActCode\", \"display\": \"ambulatory\"}, \"status\": \"unknown\", \"identifier\": [{\"value\": \"psn-90462334\"}], \"resourceType\": \"Encounter\"}, {\"id\": \"embedded-producer\", \"identifier\": [{\"value\": \"KLIN\"}], \"resourceType\": \"Organization\"}, {\"id\": \"embedded-observation-1\", \"code\": {\"text\": \"Natrium\", \"coding\": [{\"code\": \"NA\", \"display\": \"Natrium\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Klinische Chemie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_NA\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"mmol/l\", \"value\": 135}, \"referenceRange\": [{\"low\": {\"unit\": \"mmol/l\", \"value\": 134}, \"high\": {\"unit\": \"mmol/l\", \"value\": 143}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-2\", \"code\": {\"text\": \"Kalium\", \"coding\": [{\"code\": \"K\", \"display\": \"Kalium\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Klinische Chemie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_K\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"mmol/l\", \"value\": 4.2}, \"referenceRange\": [{\"low\": {\"unit\": \"mmol/l\", \"value\": 3.3}, \"high\": {\"unit\": \"mmol/l\", \"value\": 4.6}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-3\", \"code\": {\"text\": \"Calcium\", \"coding\": [{\"code\": \"CA\", \"display\": \"Calcium\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Klinische Chemie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_CA\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"mmol/l\", \"value\": 2.39}, \"referenceRange\": [{\"low\": {\"unit\": \"mmol/l\", \"value\": 2.20}, \"high\": {\"unit\": \"mmol/l\", \"value\": 2.66}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-4\", \"code\": {\"text\": \"AST (GOT)\", \"coding\": [{\"code\": \"AST\", \"display\": \"AST (GOT)\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Klinische Chemie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_AST\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"U/l\", \"value\": 129}, \"interpretation\": [{\"coding\": [{\"code\": \"H\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"U/l\", \"value\": 15}, \"high\": {\"unit\": \"U/l\", \"value\": 41}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-5\", \"code\": {\"text\": \"ALT (GPT)\", \"coding\": [{\"code\": \"ALT\", \"display\": \"ALT (GPT)\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Klinische Chemie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_ALT\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"U/l\", \"value\": 104}, \"interpretation\": [{\"coding\": [{\"code\": \"H\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"U/l\", \"value\": 8}, \"high\": {\"unit\": \"U/l\", \"value\": 45}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-6\", \"code\": {\"text\": \"Alk. Phosphatase\", \"coding\": [{\"code\": \"AP\", \"display\": \"Alk. Phosphatase\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Klinische Chemie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_AP\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"U/l\", \"value\": 225}, \"referenceRange\": [{\"low\": {\"unit\": \"U/l\", \"value\": 75}, \"high\": {\"unit\": \"U/l\", \"value\": 363}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-7\", \"code\": {\"text\": \"GGT\", \"coding\": [{\"code\": \"GGT\", \"display\": \"GGT\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Klinische Chemie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_GGT\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"U/l\", \"value\": 37}, \"interpretation\": [{\"coding\": [{\"code\": \"H\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"U/l\", \"value\": 6}, \"high\": {\"unit\": \"U/l\", \"value\": 30}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-8\", \"code\": {\"text\": \"Kreatinin\", \"coding\": [{\"code\": \"KREA\", \"display\": \"Kreatinin\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Klinische Chemie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_KREA\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"mg/dl\", \"value\": 0.34}, \"referenceRange\": [{\"low\": {\"unit\": \"mg/dl\", \"value\": 0.26}, \"high\": {\"unit\": \"mg/dl\", \"value\": 0.77}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-9\", \"code\": {\"text\": \"Leukozyten\", \"coding\": [{\"code\": \"LEU\", \"display\": \"Leukozyten\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_LEU\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"G/l\", \"value\": 25.5}, \"interpretation\": [{\"coding\": [{\"code\": \"H\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"G/l\", \"value\": 4.5}, \"high\": {\"unit\": \"G/l\", \"value\": 11.4}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-10\", \"code\": {\"text\": \"Erythrozyten\", \"coding\": [{\"code\": \"ERY\", \"display\": \"Erythrozyten\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_ERY\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"T/l\", \"value\": 2.5}, \"interpretation\": [{\"coding\": [{\"code\": \"L\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"T/l\", \"value\": 4.10}, \"high\": {\"unit\": \"T/l\", \"value\": 5.55}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-11\", \"code\": {\"text\": \"Hämoglobin\", \"coding\": [{\"code\": \"HB\", \"display\": \"Hämoglobin\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_HB\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"g/l\", \"value\": 92}, \"interpretation\": [{\"coding\": [{\"code\": \"L\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"g/l\", \"value\": 125}, \"high\": {\"unit\": \"g/l\", \"value\": 160}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-12\", \"code\": {\"text\": \"Hämatokrit\", \"coding\": [{\"code\": \"HK\", \"display\": \"Hämatokrit\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_HK\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"l/l\", \"value\": 0.26}, \"interpretation\": [{\"coding\": [{\"code\": \"L\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"l/l\", \"value\": 0.37}, \"high\": {\"unit\": \"l/l\", \"value\": 0.48}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-13\", \"code\": {\"text\": \"MCV\", \"coding\": [{\"code\": \"MCV\", \"display\": \"MCV\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_MCV\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"fl\", \"value\": 103}, \"interpretation\": [{\"coding\": [{\"code\": \"H\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"fl\", \"value\": 78}, \"high\": {\"unit\": \"fl\", \"value\": 93}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-14\", \"code\": {\"text\": \"MCH\", \"coding\": [{\"code\": \"MCH\", \"display\": \"MCH\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_MCH\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"pg\", \"value\": 37}, \"interpretation\": [{\"coding\": [{\"code\": \"H\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"pg\", \"value\": 26.0}, \"high\": {\"unit\": \"pg\", \"value\": 32.5}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-15\", \"code\": {\"text\": \"MCHC\", \"coding\": [{\"code\": \"MCHC\", \"display\": \"MCHC\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_MCHC\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"g/l Ery\", \"value\": 359}, \"referenceRange\": [{\"low\": {\"unit\": \"g/l Ery\", \"value\": 315}, \"high\": {\"unit\": \"g/l Ery\", \"value\": 360}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-16\", \"code\": {\"text\": \"Thrombozyten\", \"coding\": [{\"code\": \"THRO\", \"display\": \"Thrombozyten\"}]}, \"status\": \"preliminary\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_THRO\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"G/l\", \"value\": 891}, \"interpretation\": [{\"coding\": [{\"code\": \"H\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"G/l\", \"value\": 170}, \"high\": {\"unit\": \"G/l\", \"value\": 400}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-17\", \"code\": {\"text\": \"Normoblasten\", \"coding\": [{\"code\": \"NRBCa\", \"display\": \"Normoblasten\"}]}, \"status\": \"registered\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_NRBCa\"}], \"valueString\": \"folgt\", \"resourceType\": \"Observation\", \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299\"}], \"resourceType\": \"DiagnosticReport\", \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}","fhir_obs":"[{\"id\": \"#embedded-observation-1\", \"code\": {\"text\": \"Natrium\", \"coding\": [{\"code\": \"NA\", \"display\": \"Natrium\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Klinische Chemie\"}], \"contained\": [{\"id\": \"embedded-patient\", \"name\": [{\"given\": [\"Testvorname\"], \"family\": \"Testnachname\"}], \"gender\": \"male\", \"birthDate\": \"1990-06-01\", \"identifier\": [{\"value\": \"599999\"}], \"resourceType\": \"Patient\"}, {\"id\": \"embedded-visit\", \"class\": {\"code\": \"AMB\", \"system\": \"http://terminology.hl7.org/CodeSystem/v3-ActCode\", \"display\": \"ambulatory\"}, \"status\": \"unknown\", \"identifier\": [{\"value\": \"psn-90462334\"}], \"resourceType\": \"Encounter\"}, {\"id\": \"embedded-producer\", \"identifier\": [{\"value\": \"KLIN\"}], \"resourceType\": \"Organization\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_NA\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"mmol/l\", \"value\": 135}, \"referenceRange\": [{\"low\": {\"unit\": \"mmol/l\", \"value\": 134}, \"high\": {\"unit\": \"mmol/l\", \"value\": 143}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}]","effective_date_time":1565738280000,"fhir_version":4,"status_deleted":0,"inserted_when":1589814342169,"modified":1589814342169,"deleted_when":null} diff --git a/dev/lab-data.ndjson b/dev/lab-data.ndjson index 8d4e00f..a9bc60d 100644 --- a/dev/lab-data.ndjson +++ b/dev/lab-data.ndjson @@ -1 +1,3 @@ -3:{"id":3,"fhir":"{\"code\": {\"text\": \"Laborwerte\", \"coding\": [{\"code\": \"11502-2\", \"system\": \"http://loinc.org\", \"display\": \"Laborwerte\"}]}, \"text\": {\"div\": \"
Laborwerte
StatusPARTIAL
NameValueInterpretationReference RangeStatus
Natrium 135 mmol/l 134 mmol/l - 143 mmol/l FINAL
Kalium 4.2 mmol/l 3.3 mmol/l - 4.6 mmol/l FINAL
Calcium 2.39 mmol/l 2.20 mmol/l - 2.66 mmol/l FINAL
AST (GOT) 129 U/l 15 U/l - 41 U/l FINAL
ALT (GPT) 104 U/l 8 U/l - 45 U/l FINAL
Alk. Phosphatase 225 U/l 75 U/l - 363 U/l FINAL
GGT 37 U/l 6 U/l - 30 U/l FINAL
Kreatinin 0.34 mg/dl 0.26 mg/dl - 0.77 mg/dl FINAL
Leukozyten 25.5 G/l 4.5 G/l - 11.4 G/l FINAL
Erythrozyten 2.5 T/l 4.10 T/l - 5.55 T/l FINAL
Hämoglobin 92 g/l 125 g/l - 160 g/l FINAL
Hämatokrit 0.26 l/l 0.37 l/l - 0.48 l/l FINAL
MCV 103 fl 78 fl - 93 fl FINAL
MCH 37 pg 26.0 pg - 32.5 pg FINAL
MCHC 359 g/l Ery 315 g/l Ery - 360 g/l Ery FINAL
Thrombozyten 891 G/l 170 G/l - 400 G/l PRELIMINARY
Normoblasten folgtREGISTERED
\", \"status\": \"generated\"}, \"result\": [{\"reference\": \"#embedded-observation-1\"}, {\"reference\": \"#embedded-observation-2\"}, {\"reference\": \"#embedded-observation-3\"}, {\"reference\": \"#embedded-observation-4\"}, {\"reference\": \"#embedded-observation-5\"}, {\"reference\": \"#embedded-observation-6\"}, {\"reference\": \"#embedded-observation-7\"}, {\"reference\": \"#embedded-observation-8\"}, {\"reference\": \"#embedded-observation-9\"}, {\"reference\": \"#embedded-observation-10\"}, {\"reference\": \"#embedded-observation-11\"}, {\"reference\": \"#embedded-observation-12\"}, {\"reference\": \"#embedded-observation-13\"}, {\"reference\": \"#embedded-observation-14\"}, {\"reference\": \"#embedded-observation-15\"}, {\"reference\": \"#embedded-observation-16\"}, {\"reference\": \"#embedded-observation-17\"}], \"status\": \"partial\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"KLIN\"}], \"contained\": [{\"id\": \"embedded-patient\", \"name\": [{\"given\": [\"Testvorname\"], \"family\": \"Testnachname\"}], \"gender\": \"male\", \"birthDate\": \"1990-06-01\", \"identifier\": [{\"value\": \"599999\"}], \"resourceType\": \"Patient\"}, {\"id\": \"embedded-visit\", \"class\": {\"code\": \"AMB\", \"system\": \"http://terminology.hl7.org/CodeSystem/v3-ActCode\", \"display\": \"ambulatory\"}, \"status\": \"unknown\", \"identifier\": [{\"value\": \"psn-90462334\"}], \"resourceType\": \"Encounter\"}, {\"id\": \"embedded-producer\", \"identifier\": [{\"value\": \"KLIN\"}], \"resourceType\": \"Organization\"}, {\"id\": \"embedded-observation-1\", \"code\": {\"text\": \"Natrium\", \"coding\": [{\"code\": \"NA\", \"display\": \"Natrium\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Klinische Chemie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_NA\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"mmol/l\", \"value\": 135}, \"referenceRange\": [{\"low\": {\"unit\": \"mmol/l\", \"value\": 134}, \"high\": {\"unit\": \"mmol/l\", \"value\": 143}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-2\", \"code\": {\"text\": \"Kalium\", \"coding\": [{\"code\": \"K\", \"display\": \"Kalium\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Klinische Chemie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_K\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"mmol/l\", \"value\": 4.2}, \"referenceRange\": [{\"low\": {\"unit\": \"mmol/l\", \"value\": 3.3}, \"high\": {\"unit\": \"mmol/l\", \"value\": 4.6}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-3\", \"code\": {\"text\": \"Calcium\", \"coding\": [{\"code\": \"CA\", \"display\": \"Calcium\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Klinische Chemie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_CA\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"mmol/l\", \"value\": 2.39}, \"referenceRange\": [{\"low\": {\"unit\": \"mmol/l\", \"value\": 2.20}, \"high\": {\"unit\": \"mmol/l\", \"value\": 2.66}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-4\", \"code\": {\"text\": \"AST (GOT)\", \"coding\": [{\"code\": \"AST\", \"display\": \"AST (GOT)\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Klinische Chemie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_AST\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"U/l\", \"value\": 129}, \"interpretation\": [{\"coding\": [{\"code\": \"H\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"U/l\", \"value\": 15}, \"high\": {\"unit\": \"U/l\", \"value\": 41}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-5\", \"code\": {\"text\": \"ALT (GPT)\", \"coding\": [{\"code\": \"ALT\", \"display\": \"ALT (GPT)\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Klinische Chemie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_ALT\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"U/l\", \"value\": 104}, \"interpretation\": [{\"coding\": [{\"code\": \"H\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"U/l\", \"value\": 8}, \"high\": {\"unit\": \"U/l\", \"value\": 45}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-6\", \"code\": {\"text\": \"Alk. Phosphatase\", \"coding\": [{\"code\": \"AP\", \"display\": \"Alk. Phosphatase\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Klinische Chemie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_AP\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"U/l\", \"value\": 225}, \"referenceRange\": [{\"low\": {\"unit\": \"U/l\", \"value\": 75}, \"high\": {\"unit\": \"U/l\", \"value\": 363}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-7\", \"code\": {\"text\": \"GGT\", \"coding\": [{\"code\": \"GGT\", \"display\": \"GGT\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Klinische Chemie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_GGT\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"U/l\", \"value\": 37}, \"interpretation\": [{\"coding\": [{\"code\": \"H\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"U/l\", \"value\": 6}, \"high\": {\"unit\": \"U/l\", \"value\": 30}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-8\", \"code\": {\"text\": \"Kreatinin\", \"coding\": [{\"code\": \"KREA\", \"display\": \"Kreatinin\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Klinische Chemie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_KREA\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"mg/dl\", \"value\": 0.34}, \"referenceRange\": [{\"low\": {\"unit\": \"mg/dl\", \"value\": 0.26}, \"high\": {\"unit\": \"mg/dl\", \"value\": 0.77}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-9\", \"code\": {\"text\": \"Leukozyten\", \"coding\": [{\"code\": \"LEU\", \"display\": \"Leukozyten\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_LEU\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"G/l\", \"value\": 25.5}, \"interpretation\": [{\"coding\": [{\"code\": \"H\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"G/l\", \"value\": 4.5}, \"high\": {\"unit\": \"G/l\", \"value\": 11.4}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-10\", \"code\": {\"text\": \"Erythrozyten\", \"coding\": [{\"code\": \"ERY\", \"display\": \"Erythrozyten\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_ERY\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"T/l\", \"value\": 2.5}, \"interpretation\": [{\"coding\": [{\"code\": \"L\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"T/l\", \"value\": 4.10}, \"high\": {\"unit\": \"T/l\", \"value\": 5.55}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-11\", \"code\": {\"text\": \"Hämoglobin\", \"coding\": [{\"code\": \"HB\", \"display\": \"Hämoglobin\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_HB\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"g/l\", \"value\": 92}, \"interpretation\": [{\"coding\": [{\"code\": \"L\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"g/l\", \"value\": 125}, \"high\": {\"unit\": \"g/l\", \"value\": 160}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-12\", \"code\": {\"text\": \"Hämatokrit\", \"coding\": [{\"code\": \"HK\", \"display\": \"Hämatokrit\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_HK\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"l/l\", \"value\": 0.26}, \"interpretation\": [{\"coding\": [{\"code\": \"L\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"l/l\", \"value\": 0.37}, \"high\": {\"unit\": \"l/l\", \"value\": 0.48}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-13\", \"code\": {\"text\": \"MCV\", \"coding\": [{\"code\": \"MCV\", \"display\": \"MCV\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_MCV\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"fl\", \"value\": 103}, \"interpretation\": [{\"coding\": [{\"code\": \"H\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"fl\", \"value\": 78}, \"high\": {\"unit\": \"fl\", \"value\": 93}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-14\", \"code\": {\"text\": \"MCH\", \"coding\": [{\"code\": \"MCH\", \"display\": \"MCH\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_MCH\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"pg\", \"value\": 37}, \"interpretation\": [{\"coding\": [{\"code\": \"H\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"pg\", \"value\": 26.0}, \"high\": {\"unit\": \"pg\", \"value\": 32.5}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-15\", \"code\": {\"text\": \"MCHC\", \"coding\": [{\"code\": \"MCHC\", \"display\": \"MCHC\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_MCHC\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"g/l Ery\", \"value\": 359}, \"referenceRange\": [{\"low\": {\"unit\": \"g/l Ery\", \"value\": 315}, \"high\": {\"unit\": \"g/l Ery\", \"value\": 360}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-16\", \"code\": {\"text\": \"Thrombozyten\", \"coding\": [{\"code\": \"THRO\", \"display\": \"Thrombozyten\"}]}, \"status\": \"preliminary\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_THRO\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"G/l\", \"value\": 891}, \"interpretation\": [{\"coding\": [{\"code\": \"H\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"G/l\", \"value\": 170}, \"high\": {\"unit\": \"G/l\", \"value\": 400}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"embedded-observation-17\", \"code\": {\"text\": \"Normoblasten\", \"coding\": [{\"code\": \"NRBCa\", \"display\": \"Normoblasten\"}]}, \"status\": \"registered\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_NRBCa\"}], \"valueString\": \"folgt\", \"resourceType\": \"Observation\", \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299\"}], \"resourceType\": \"DiagnosticReport\", \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}","fhir_obs":"[{\"id\": \"#embedded-observation-1\", \"code\": {\"text\": \"Natrium\", \"coding\": [{\"code\": \"NA\", \"display\": \"Natrium\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Klinische Chemie\"}], \"contained\": [{\"id\": \"embedded-patient\", \"name\": [{\"given\": [\"Testvorname\"], \"family\": \"Testnachname\"}], \"gender\": \"male\", \"birthDate\": \"1990-06-01\", \"identifier\": [{\"value\": \"599999\"}], \"resourceType\": \"Patient\"}, {\"id\": \"embedded-visit\", \"class\": {\"code\": \"AMB\", \"system\": \"http://terminology.hl7.org/CodeSystem/v3-ActCode\", \"display\": \"ambulatory\"}, \"status\": \"unknown\", \"identifier\": [{\"value\": \"psn-90462334\"}], \"resourceType\": \"Encounter\"}, {\"id\": \"embedded-producer\", \"identifier\": [{\"value\": \"KLIN\"}], \"resourceType\": \"Organization\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_NA\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"mmol/l\", \"value\": 135}, \"referenceRange\": [{\"low\": {\"unit\": \"mmol/l\", \"value\": 134}, \"high\": {\"unit\": \"mmol/l\", \"value\": 143}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"#embedded-observation-2\", \"code\": {\"text\": \"Kalium\", \"coding\": [{\"code\": \"K\", \"display\": \"Kalium\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Klinische Chemie\"}], \"contained\": [{\"id\": \"embedded-patient\", \"name\": [{\"given\": [\"Testvorname\"], \"family\": \"Testnachname\"}], \"gender\": \"male\", \"birthDate\": \"1990-06-01\", \"identifier\": [{\"value\": \"599999\"}], \"resourceType\": \"Patient\"}, {\"id\": \"embedded-visit\", \"class\": {\"code\": \"AMB\", \"system\": \"http://terminology.hl7.org/CodeSystem/v3-ActCode\", \"display\": \"ambulatory\"}, \"status\": \"unknown\", \"identifier\": [{\"value\": \"psn-90462334\"}], \"resourceType\": \"Encounter\"}, {\"id\": \"embedded-producer\", \"identifier\": [{\"value\": \"KLIN\"}], \"resourceType\": \"Organization\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_K\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"mmol/l\", \"value\": 4.2}, \"referenceRange\": [{\"low\": {\"unit\": \"mmol/l\", \"value\": 3.3}, \"high\": {\"unit\": \"mmol/l\", \"value\": 4.6}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"#embedded-observation-3\", \"code\": {\"text\": \"Calcium\", \"coding\": [{\"code\": \"CA\", \"display\": \"Calcium\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Klinische Chemie\"}], \"contained\": [{\"id\": \"embedded-patient\", \"name\": [{\"given\": [\"Testvorname\"], \"family\": \"Testnachname\"}], \"gender\": \"male\", \"birthDate\": \"1990-06-01\", \"identifier\": [{\"value\": \"599999\"}], \"resourceType\": \"Patient\"}, {\"id\": \"embedded-visit\", \"class\": {\"code\": \"AMB\", \"system\": \"http://terminology.hl7.org/CodeSystem/v3-ActCode\", \"display\": \"ambulatory\"}, \"status\": \"unknown\", \"identifier\": [{\"value\": \"psn-90462334\"}], \"resourceType\": \"Encounter\"}, {\"id\": \"embedded-producer\", \"identifier\": [{\"value\": \"KLIN\"}], \"resourceType\": \"Organization\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_CA\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"mmol/l\", \"value\": 2.39}, \"referenceRange\": [{\"low\": {\"unit\": \"mmol/l\", \"value\": 2.20}, \"high\": {\"unit\": \"mmol/l\", \"value\": 2.66}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"#embedded-observation-4\", \"code\": {\"text\": \"AST (GOT)\", \"coding\": [{\"code\": \"AST\", \"display\": \"AST (GOT)\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Klinische Chemie\"}], \"contained\": [{\"id\": \"embedded-patient\", \"name\": [{\"given\": [\"Testvorname\"], \"family\": \"Testnachname\"}], \"gender\": \"male\", \"birthDate\": \"1990-06-01\", \"identifier\": [{\"value\": \"599999\"}], \"resourceType\": \"Patient\"}, {\"id\": \"embedded-visit\", \"class\": {\"code\": \"AMB\", \"system\": \"http://terminology.hl7.org/CodeSystem/v3-ActCode\", \"display\": \"ambulatory\"}, \"status\": \"unknown\", \"identifier\": [{\"value\": \"psn-90462334\"}], \"resourceType\": \"Encounter\"}, {\"id\": \"embedded-producer\", \"identifier\": [{\"value\": \"KLIN\"}], \"resourceType\": \"Organization\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_AST\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"U/l\", \"value\": 129}, \"interpretation\": [{\"coding\": [{\"code\": \"H\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"U/l\", \"value\": 15}, \"high\": {\"unit\": \"U/l\", \"value\": 41}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"#embedded-observation-5\", \"code\": {\"text\": \"ALT (GPT)\", \"coding\": [{\"code\": \"ALT\", \"display\": \"ALT (GPT)\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Klinische Chemie\"}], \"contained\": [{\"id\": \"embedded-patient\", \"name\": [{\"given\": [\"Testvorname\"], \"family\": \"Testnachname\"}], \"gender\": \"male\", \"birthDate\": \"1990-06-01\", \"identifier\": [{\"value\": \"599999\"}], \"resourceType\": \"Patient\"}, {\"id\": \"embedded-visit\", \"class\": {\"code\": \"AMB\", \"system\": \"http://terminology.hl7.org/CodeSystem/v3-ActCode\", \"display\": \"ambulatory\"}, \"status\": \"unknown\", \"identifier\": [{\"value\": \"psn-90462334\"}], \"resourceType\": \"Encounter\"}, {\"id\": \"embedded-producer\", \"identifier\": [{\"value\": \"KLIN\"}], \"resourceType\": \"Organization\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_ALT\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"U/l\", \"value\": 104}, \"interpretation\": [{\"coding\": [{\"code\": \"H\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"U/l\", \"value\": 8}, \"high\": {\"unit\": \"U/l\", \"value\": 45}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"#embedded-observation-6\", \"code\": {\"text\": \"Alk. Phosphatase\", \"coding\": [{\"code\": \"AP\", \"display\": \"Alk. Phosphatase\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Klinische Chemie\"}], \"contained\": [{\"id\": \"embedded-patient\", \"name\": [{\"given\": [\"Testvorname\"], \"family\": \"Testnachname\"}], \"gender\": \"male\", \"birthDate\": \"1990-06-01\", \"identifier\": [{\"value\": \"599999\"}], \"resourceType\": \"Patient\"}, {\"id\": \"embedded-visit\", \"class\": {\"code\": \"AMB\", \"system\": \"http://terminology.hl7.org/CodeSystem/v3-ActCode\", \"display\": \"ambulatory\"}, \"status\": \"unknown\", \"identifier\": [{\"value\": \"psn-90462334\"}], \"resourceType\": \"Encounter\"}, {\"id\": \"embedded-producer\", \"identifier\": [{\"value\": \"KLIN\"}], \"resourceType\": \"Organization\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_AP\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"U/l\", \"value\": 225}, \"referenceRange\": [{\"low\": {\"unit\": \"U/l\", \"value\": 75}, \"high\": {\"unit\": \"U/l\", \"value\": 363}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"#embedded-observation-7\", \"code\": {\"text\": \"GGT\", \"coding\": [{\"code\": \"GGT\", \"display\": \"GGT\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Klinische Chemie\"}], \"contained\": [{\"id\": \"embedded-patient\", \"name\": [{\"given\": [\"Testvorname\"], \"family\": \"Testnachname\"}], \"gender\": \"male\", \"birthDate\": \"1990-06-01\", \"identifier\": [{\"value\": \"599999\"}], \"resourceType\": \"Patient\"}, {\"id\": \"embedded-visit\", \"class\": {\"code\": \"AMB\", \"system\": \"http://terminology.hl7.org/CodeSystem/v3-ActCode\", \"display\": \"ambulatory\"}, \"status\": \"unknown\", \"identifier\": [{\"value\": \"psn-90462334\"}], \"resourceType\": \"Encounter\"}, {\"id\": \"embedded-producer\", \"identifier\": [{\"value\": \"KLIN\"}], \"resourceType\": \"Organization\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_GGT\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"U/l\", \"value\": 37}, \"interpretation\": [{\"coding\": [{\"code\": \"H\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"U/l\", \"value\": 6}, \"high\": {\"unit\": \"U/l\", \"value\": 30}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"#embedded-observation-8\", \"code\": {\"text\": \"Kreatinin\", \"coding\": [{\"code\": \"KREA\", \"display\": \"Kreatinin\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Klinische Chemie\"}], \"contained\": [{\"id\": \"embedded-patient\", \"name\": [{\"given\": [\"Testvorname\"], \"family\": \"Testnachname\"}], \"gender\": \"male\", \"birthDate\": \"1990-06-01\", \"identifier\": [{\"value\": \"599999\"}], \"resourceType\": \"Patient\"}, {\"id\": \"embedded-visit\", \"class\": {\"code\": \"AMB\", \"system\": \"http://terminology.hl7.org/CodeSystem/v3-ActCode\", \"display\": \"ambulatory\"}, \"status\": \"unknown\", \"identifier\": [{\"value\": \"psn-90462334\"}], \"resourceType\": \"Encounter\"}, {\"id\": \"embedded-producer\", \"identifier\": [{\"value\": \"KLIN\"}], \"resourceType\": \"Organization\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_KREA\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"mg/dl\", \"value\": 0.34}, \"referenceRange\": [{\"low\": {\"unit\": \"mg/dl\", \"value\": 0.26}, \"high\": {\"unit\": \"mg/dl\", \"value\": 0.77}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"#embedded-observation-9\", \"code\": {\"text\": \"Leukozyten\", \"coding\": [{\"code\": \"LEU\", \"display\": \"Leukozyten\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"contained\": [{\"id\": \"embedded-patient\", \"name\": [{\"given\": [\"Testvorname\"], \"family\": \"Testnachname\"}], \"gender\": \"male\", \"birthDate\": \"1990-06-01\", \"identifier\": [{\"value\": \"599999\"}], \"resourceType\": \"Patient\"}, {\"id\": \"embedded-visit\", \"class\": {\"code\": \"AMB\", \"system\": \"http://terminology.hl7.org/CodeSystem/v3-ActCode\", \"display\": \"ambulatory\"}, \"status\": \"unknown\", \"identifier\": [{\"value\": \"psn-90462334\"}], \"resourceType\": \"Encounter\"}, {\"id\": \"embedded-producer\", \"identifier\": [{\"value\": \"KLIN\"}], \"resourceType\": \"Organization\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_LEU\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"G/l\", \"value\": 25.5}, \"interpretation\": [{\"coding\": [{\"code\": \"H\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"G/l\", \"value\": 4.5}, \"high\": {\"unit\": \"G/l\", \"value\": 11.4}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"#embedded-observation-10\", \"code\": {\"text\": \"Erythrozyten\", \"coding\": [{\"code\": \"ERY\", \"display\": \"Erythrozyten\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"contained\": [{\"id\": \"embedded-patient\", \"name\": [{\"given\": [\"Testvorname\"], \"family\": \"Testnachname\"}], \"gender\": \"male\", \"birthDate\": \"1990-06-01\", \"identifier\": [{\"value\": \"599999\"}], \"resourceType\": \"Patient\"}, {\"id\": \"embedded-visit\", \"class\": {\"code\": \"AMB\", \"system\": \"http://terminology.hl7.org/CodeSystem/v3-ActCode\", \"display\": \"ambulatory\"}, \"status\": \"unknown\", \"identifier\": [{\"value\": \"psn-90462334\"}], \"resourceType\": \"Encounter\"}, {\"id\": \"embedded-producer\", \"identifier\": [{\"value\": \"KLIN\"}], \"resourceType\": \"Organization\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_ERY\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"T/l\", \"value\": 2.5}, \"interpretation\": [{\"coding\": [{\"code\": \"L\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"T/l\", \"value\": 4.10}, \"high\": {\"unit\": \"T/l\", \"value\": 5.55}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"#embedded-observation-11\", \"code\": {\"text\": \"Hämoglobin\", \"coding\": [{\"code\": \"HB\", \"display\": \"Hämoglobin\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"contained\": [{\"id\": \"embedded-patient\", \"name\": [{\"given\": [\"Testvorname\"], \"family\": \"Testnachname\"}], \"gender\": \"male\", \"birthDate\": \"1990-06-01\", \"identifier\": [{\"value\": \"599999\"}], \"resourceType\": \"Patient\"}, {\"id\": \"embedded-visit\", \"class\": {\"code\": \"AMB\", \"system\": \"http://terminology.hl7.org/CodeSystem/v3-ActCode\", \"display\": \"ambulatory\"}, \"status\": \"unknown\", \"identifier\": [{\"value\": \"psn-90462334\"}], \"resourceType\": \"Encounter\"}, {\"id\": \"embedded-producer\", \"identifier\": [{\"value\": \"KLIN\"}], \"resourceType\": \"Organization\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_HB\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"g/l\", \"value\": 92}, \"interpretation\": [{\"coding\": [{\"code\": \"L\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"g/l\", \"value\": 125}, \"high\": {\"unit\": \"g/l\", \"value\": 160}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"#embedded-observation-12\", \"code\": {\"text\": \"Hämatokrit\", \"coding\": [{\"code\": \"HK\", \"display\": \"Hämatokrit\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"contained\": [{\"id\": \"embedded-patient\", \"name\": [{\"given\": [\"Testvorname\"], \"family\": \"Testnachname\"}], \"gender\": \"male\", \"birthDate\": \"1990-06-01\", \"identifier\": [{\"value\": \"599999\"}], \"resourceType\": \"Patient\"}, {\"id\": \"embedded-visit\", \"class\": {\"code\": \"AMB\", \"system\": \"http://terminology.hl7.org/CodeSystem/v3-ActCode\", \"display\": \"ambulatory\"}, \"status\": \"unknown\", \"identifier\": [{\"value\": \"psn-90462334\"}], \"resourceType\": \"Encounter\"}, {\"id\": \"embedded-producer\", \"identifier\": [{\"value\": \"KLIN\"}], \"resourceType\": \"Organization\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_HK\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"l/l\", \"value\": 0.26}, \"interpretation\": [{\"coding\": [{\"code\": \"L\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"l/l\", \"value\": 0.37}, \"high\": {\"unit\": \"l/l\", \"value\": 0.48}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"#embedded-observation-13\", \"code\": {\"text\": \"MCV\", \"coding\": [{\"code\": \"MCV\", \"display\": \"MCV\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"contained\": [{\"id\": \"embedded-patient\", \"name\": [{\"given\": [\"Testvorname\"], \"family\": \"Testnachname\"}], \"gender\": \"male\", \"birthDate\": \"1990-06-01\", \"identifier\": [{\"value\": \"599999\"}], \"resourceType\": \"Patient\"}, {\"id\": \"embedded-visit\", \"class\": {\"code\": \"AMB\", \"system\": \"http://terminology.hl7.org/CodeSystem/v3-ActCode\", \"display\": \"ambulatory\"}, \"status\": \"unknown\", \"identifier\": [{\"value\": \"psn-90462334\"}], \"resourceType\": \"Encounter\"}, {\"id\": \"embedded-producer\", \"identifier\": [{\"value\": \"KLIN\"}], \"resourceType\": \"Organization\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_MCV\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"fl\", \"value\": 103}, \"interpretation\": [{\"coding\": [{\"code\": \"H\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"fl\", \"value\": 78}, \"high\": {\"unit\": \"fl\", \"value\": 93}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"#embedded-observation-14\", \"code\": {\"text\": \"MCH\", \"coding\": [{\"code\": \"MCH\", \"display\": \"MCH\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"contained\": [{\"id\": \"embedded-patient\", \"name\": [{\"given\": [\"Testvorname\"], \"family\": \"Testnachname\"}], \"gender\": \"male\", \"birthDate\": \"1990-06-01\", \"identifier\": [{\"value\": \"599999\"}], \"resourceType\": \"Patient\"}, {\"id\": \"embedded-visit\", \"class\": {\"code\": \"AMB\", \"system\": \"http://terminology.hl7.org/CodeSystem/v3-ActCode\", \"display\": \"ambulatory\"}, \"status\": \"unknown\", \"identifier\": [{\"value\": \"psn-90462334\"}], \"resourceType\": \"Encounter\"}, {\"id\": \"embedded-producer\", \"identifier\": [{\"value\": \"KLIN\"}], \"resourceType\": \"Organization\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_MCH\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"pg\", \"value\": 37}, \"interpretation\": [{\"coding\": [{\"code\": \"H\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"pg\", \"value\": 26.0}, \"high\": {\"unit\": \"pg\", \"value\": 32.5}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"#embedded-observation-15\", \"code\": {\"text\": \"MCHC\", \"coding\": [{\"code\": \"MCHC\", \"display\": \"MCHC\"}]}, \"status\": \"final\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"contained\": [{\"id\": \"embedded-patient\", \"name\": [{\"given\": [\"Testvorname\"], \"family\": \"Testnachname\"}], \"gender\": \"male\", \"birthDate\": \"1990-06-01\", \"identifier\": [{\"value\": \"599999\"}], \"resourceType\": \"Patient\"}, {\"id\": \"embedded-visit\", \"class\": {\"code\": \"AMB\", \"system\": \"http://terminology.hl7.org/CodeSystem/v3-ActCode\", \"display\": \"ambulatory\"}, \"status\": \"unknown\", \"identifier\": [{\"value\": \"psn-90462334\"}], \"resourceType\": \"Encounter\"}, {\"id\": \"embedded-producer\", \"identifier\": [{\"value\": \"KLIN\"}], \"resourceType\": \"Organization\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_MCHC\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"g/l Ery\", \"value\": 359}, \"referenceRange\": [{\"low\": {\"unit\": \"g/l Ery\", \"value\": 315}, \"high\": {\"unit\": \"g/l Ery\", \"value\": 360}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"#embedded-observation-16\", \"code\": {\"text\": \"Thrombozyten\", \"coding\": [{\"code\": \"THRO\", \"display\": \"Thrombozyten\"}]}, \"status\": \"preliminary\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"contained\": [{\"id\": \"embedded-patient\", \"name\": [{\"given\": [\"Testvorname\"], \"family\": \"Testnachname\"}], \"gender\": \"male\", \"birthDate\": \"1990-06-01\", \"identifier\": [{\"value\": \"599999\"}], \"resourceType\": \"Patient\"}, {\"id\": \"embedded-visit\", \"class\": {\"code\": \"AMB\", \"system\": \"http://terminology.hl7.org/CodeSystem/v3-ActCode\", \"display\": \"ambulatory\"}, \"status\": \"unknown\", \"identifier\": [{\"value\": \"psn-90462334\"}], \"resourceType\": \"Encounter\"}, {\"id\": \"embedded-producer\", \"identifier\": [{\"value\": \"KLIN\"}], \"resourceType\": \"Organization\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_THRO\"}], \"resourceType\": \"Observation\", \"valueQuantity\": {\"unit\": \"G/l\", \"value\": 891}, \"interpretation\": [{\"coding\": [{\"code\": \"H\"}]}], \"referenceRange\": [{\"low\": {\"unit\": \"G/l\", \"value\": 170}, \"high\": {\"unit\": \"G/l\", \"value\": 400}}], \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}, {\"id\": \"#embedded-observation-17\", \"code\": {\"text\": \"Normoblasten\", \"coding\": [{\"code\": \"NRBCa\", \"display\": \"Normoblasten\"}]}, \"status\": \"registered\", \"subject\": {\"reference\": \"#embedded-patient\"}, \"category\": [{\"text\": \"Hämatologie\"}], \"contained\": [{\"id\": \"embedded-patient\", \"name\": [{\"given\": [\"Testvorname\"], \"family\": \"Testnachname\"}], \"gender\": \"male\", \"birthDate\": \"1990-06-01\", \"identifier\": [{\"value\": \"599999\"}], \"resourceType\": \"Patient\"}, {\"id\": \"embedded-visit\", \"class\": {\"code\": \"AMB\", \"system\": \"http://terminology.hl7.org/CodeSystem/v3-ActCode\", \"display\": \"ambulatory\"}, \"status\": \"unknown\", \"identifier\": [{\"value\": \"psn-90462334\"}], \"resourceType\": \"Encounter\"}, {\"id\": \"embedded-producer\", \"identifier\": [{\"value\": \"KLIN\"}], \"resourceType\": \"Organization\"}], \"encounter\": {\"reference\": \"#embedded-visit\"}, \"performer\": [{\"reference\": \"#embedded-producer\"}], \"identifier\": [{\"value\": \"24051299_NRBCa\"}], \"valueString\": \"folgt\", \"resourceType\": \"Observation\", \"effectiveDateTime\": \"2019-08-13T23:18:00+02:00\"}]","effective_date_time":1565738280000,"fhir_version":4,"status_deleted":0,"inserted_when":1589814342169,"modified":1589814342169,"deleted_when":null} +1:{"id": 1, "fhir": "{\"id\":\"1\",\"code\":{\"text\":\"Laboratory report\",\"coding\":[{\"code\":\"11502-2\",\"system\":\"http://loinc.org\",\"display\":\"Laboratory report\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"LAB\",\"coding\":[{\"display\":\"LAB\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Testvorname\"],\"family\":\"Testnachname\"}],\"gender\":\"male\",\"birthDate\":\"1981-11-02\",\"identifier\":[{\"value\":\"pid-1\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-1\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"BLUT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20180603_88888888\"}],\"resourceType\":\"DiagnosticReport\",\"effectiveDateTime\":\"2018-06-03T11:35:00+02:00\"}", "fhir_obs": "[{\"id\":\"777777736\",\"code\":{\"text\":\"Quick-Wert (TPZ)\",\"coding\":[{\"code\":\"QUI\",\"display\":\"Quick-Wert (TPZ)\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"Hämostaseologie (TM)\",\"coding\":[{\"display\":\"Hämostaseologie (TM)\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Testvorname\"],\"family\":\"Testnachname\"}],\"gender\":\"male\",\"birthDate\":\"1981-11-02\",\"identifier\":[{\"value\":\"pid-1\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-1\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"BLUT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20180603_88888888_QUI\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"%\",\"value\":99},\"referenceRange\":[{\"low\":{\"unit\":\"%\",\"value\":82},\"high\":{\"unit\":\"%\",\"value\":121}}],\"effectiveDateTime\":\"2018-06-03T11:35:00+02:00\"},{\"id\":\"777777737\",\"code\":{\"text\":\"...INR\",\"coding\":[{\"code\":\"INOR\",\"display\":\"...INR\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"Hämostaseologie (TM)\",\"coding\":[{\"display\":\"Hämostaseologie (TM)\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Testvorname\"],\"family\":\"Testnachname\"}],\"gender\":\"male\",\"birthDate\":\"1981-11-02\",\"identifier\":[{\"value\":\"pid-1\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-1\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"BLUT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20180603_88888888_INOR\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"Ratio\",\"value\":0.99},\"referenceRange\":[{\"low\":{\"unit\":\"Ratio\",\"value\":0.85},\"high\":{\"unit\":\"Ratio\",\"value\":1.15}}],\"effectiveDateTime\":\"2018-06-03T11:35:00+02:00\"},{\"id\":\"777777738\",\"code\":{\"text\":\"aPTT\",\"coding\":[{\"code\":\"APTT\",\"display\":\"aPTT\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"Hämostaseologie (TM)\",\"coding\":[{\"display\":\"Hämostaseologie (TM)\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Testvorname\"],\"family\":\"Testnachname\"}],\"gender\":\"male\",\"birthDate\":\"1981-11-02\",\"identifier\":[{\"value\":\"pid-1\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-1\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"BLUT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20180603_88888888_APTT\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"sec\",\"value\":29},\"referenceRange\":[{\"low\":{\"unit\":\"sec\",\"value\":23},\"high\":{\"unit\":\"sec\",\"value\":38}}],\"effectiveDateTime\":\"2018-06-03T11:35:00+02:00\"},{\"id\":\"777777739\",\"code\":{\"text\":\"Fibrinogen (abgeleitet)\",\"coding\":[{\"code\":\"FBR\",\"display\":\"Fibrinogen (abgeleitet)\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"Hämostaseologie (TM)\",\"coding\":[{\"display\":\"Hämostaseologie (TM)\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Testvorname\"],\"family\":\"Testnachname\"}],\"gender\":\"male\",\"birthDate\":\"1981-11-02\",\"identifier\":[{\"value\":\"pid-1\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-1\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"BLUT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20180603_88888888_FBR\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"g/l\",\"value\":6.0},\"interpretation\":[{\"coding\":[{\"code\":\"H\"}]}],\"referenceRange\":[{\"low\":{\"unit\":\"g/l\",\"value\":1.8},\"high\":{\"unit\":\"g/l\",\"value\":5}}],\"effectiveDateTime\":\"2018-06-03T11:35:00+02:00\"},{\"id\":\"777777740\",\"code\":{\"text\":\"Beurteilung\",\"coding\":[{\"code\":\"BEUR\",\"display\":\"Beurteilung\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"Hämostaseologie (TM)\",\"coding\":[{\"display\":\"Hämostaseologie (TM)\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Testvorname\"],\"family\":\"Testnachname\"}],\"gender\":\"male\",\"birthDate\":\"1981-11-02\",\"identifier\":[{\"value\":\"pid-1\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-1\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"BLUT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20180603_88888888_BEUR\"}],\"resourceType\":\"Observation\",\"effectiveDateTime\":\"2018-06-03T11:35:00+02:00\"}]", "effective_date_time": 1528018500, "fhir_version": 4, "inserted_when": 1528029060, "modified": 1528029060, "version_number": 4, "deleted_when": null} +2:{"id": 2, "fhir": "{\"code\":{\"text\":\"Laboratory report\",\"coding\":[{\"code\":\"11502-2\",\"system\":\"http://loinc.org\",\"display\":\"Laboratory report\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"LAB\",\"coding\":[{\"display\":\"LAB\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Peter\"],\"family\":\"Test\"}],\"gender\":\"male\",\"birthDate\":\"1951-04-12\",\"identifier\":[{\"value\":\"pid-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"POCT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20200807_99999999\"}],\"resourceType\":\"DiagnosticReport\",\"effectiveDateTime\":\"2020-08-07T08:52:00+02:00\"}", "fhir_obs":"[{\"code\":{\"text\":\"Materialtyp POCT\",\"coding\":[{\"code\":\"MTYP-POCT\",\"display\":\"Materialtyp POCT\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"POCT\",\"coding\":[{\"display\":\"POCT\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Peter\"],\"family\":\"Test\"}],\"gender\":\"male\",\"birthDate\":\"1951-04-12\",\"identifier\":[{\"value\":\"pid-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"POCT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20200807_99999999_MTYP-POCT\"}],\"valueString\":\"Arteriell\",\"resourceType\":\"Observation\",\"effectiveDateTime\":\"2020-08-07T08:52:00+02:00\"},{\"code\":{\"text\":\"Sauerstoff-Partialdruck in Blut\",\"coding\":[{\"code\":\"PO2-POCT\",\"display\":\"Sauerstoff-Partialdruck in Blut\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"POCT\",\"coding\":[{\"display\":\"POCT\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Peter\"],\"family\":\"Test\"}],\"gender\":\"male\",\"birthDate\":\"1951-04-12\",\"identifier\":[{\"value\":\"pid-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"POCT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20200807_99999999_PO2-POCT\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"mmHg\",\"value\":82.6},\"effectiveDateTime\":\"2020-08-07T08:52:00+02:00\"},{\"code\":{\"text\":\"pH in Blut bei Patiententemperatur\",\"coding\":[{\"code\":\"NRBC\",\"display\":\"pH in Blut bei Patiententemperatur\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"POCT\",\"coding\":[{\"display\":\"POCT\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Peter\"],\"family\":\"Test\"}],\"gender\":\"male\",\"birthDate\":\"1951-04-12\",\"identifier\":[{\"value\":\"pid-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"POCT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20200807_99999999_PH(T)-POCT\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"value\":7.418},\"effectiveDateTime\":\"2020-08-07T08:52:00+02:00\"},{\"code\":{\"text\":\"Sauerstoff-Partialdruck bei Patiententemperatur\",\"coding\":[{\"code\":\"PO2(T)-POCT\",\"display\":\"Sauerstoff-Partialdruck bei Patiententemperatur\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"POCT\",\"coding\":[{\"display\":\"POCT\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Peter\"],\"family\":\"Test\"}],\"gender\":\"male\",\"birthDate\":\"1951-04-12\",\"identifier\":[{\"value\":\"pid-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"POCT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20200807_99999999_PO2(T)-POCT\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"mmHg\",\"value\":82.6},\"effectiveDateTime\":\"2020-08-07T08:52:00+02:00\"},{\"code\":{\"text\":\"Kohlendioxid-Partialdruck bei Patiententemperatur\",\"coding\":[{\"code\":\"PCO2(T)-POCT\",\"display\":\"Kohlendioxid-Partialdruck bei Patiententemperatur\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"POCT\",\"coding\":[{\"display\":\"POCT\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Peter\"],\"family\":\"Test\"}],\"gender\":\"male\",\"birthDate\":\"1951-04-12\",\"identifier\":[{\"value\":\"pid-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"POCT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20200807_99999999_PCO2(T)-POCT\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"mmHg\",\"value\":33.0},\"effectiveDateTime\":\"2020-08-07T08:52:00+02:00\"},{\"code\":{\"text\":\"Gesamthämoglobin-Konzentration in Blut\",\"coding\":[{\"code\":\"THB-POCT\",\"display\":\"Gesamthämoglobin-Konzentration in Blut\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"POCT\",\"coding\":[{\"display\":\"POCT\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Peter\"],\"family\":\"Test\"}],\"gender\":\"male\",\"birthDate\":\"1951-04-12\",\"identifier\":[{\"value\":\"pid-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"POCT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20200807_99999999_THB-POCT\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"g/dl\",\"value\":11.9},\"effectiveDateTime\":\"2020-08-07T08:52:00+02:00\"},{\"code\":{\"text\":\"Sauerstoff-Sättigung\",\"coding\":[{\"code\":\"SO2-POCT\",\"display\":\"Sauerstoff-Sättigung\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"POCT\",\"coding\":[{\"display\":\"POCT\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Peter\"],\"family\":\"Test\"}],\"gender\":\"male\",\"birthDate\":\"1951-04-12\",\"identifier\":[{\"value\":\"pid-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"POCT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20200807_99999999_SO2-POCT\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"%\",\"value\":94.2},\"effectiveDateTime\":\"2020-08-07T08:52:00+02:00\"},{\"code\":{\"text\":\"Carboxyhämoglobin-Fraktion des Gesamthämoglobins in Blut\",\"coding\":[{\"code\":\"COHB-POCT\",\"display\":\"Carboxyhämoglobin-Fraktion des Gesamthämoglobins in Blut\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"POCT\",\"coding\":[{\"display\":\"POCT\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Peter\"],\"family\":\"Test\"}],\"gender\":\"male\",\"birthDate\":\"1951-04-12\",\"identifier\":[{\"value\":\"pid-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"POCT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20200807_99999999_COHB-POCT\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"%\",\"value\":0.3},\"effectiveDateTime\":\"2020-08-07T08:52:00+02:00\"},{\"code\":{\"text\":\"Methämoglobin-Fraktion des Gesamthämoglobins in Blut\",\"coding\":[{\"code\":\"METHB-POCT\",\"display\":\"Methämoglobin-Fraktion des Gesamthämoglobins in Blut\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"POCT\",\"coding\":[{\"display\":\"POCT\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Peter\"],\"family\":\"Test\"}],\"gender\":\"male\",\"birthDate\":\"1951-04-12\",\"identifier\":[{\"value\":\"pid-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"POCT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20200807_99999999_METHB-POCT\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"%\",\"value\":0.8},\"effectiveDateTime\":\"2020-08-07T08:52:00+02:00\"},{\"code\":{\"text\":\"Hämatokrit\",\"coding\":[{\"code\":\"HCT-POCT\",\"display\":\"Hämatokrit\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"POCT\",\"coding\":[{\"display\":\"POCT\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Peter\"],\"family\":\"Test\"}],\"gender\":\"male\",\"birthDate\":\"1951-04-12\",\"identifier\":[{\"value\":\"pid-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"POCT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20200807_99999999_HCT-POCT\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"%\",\"value\":36.8},\"effectiveDateTime\":\"2020-08-07T08:52:00+02:00\"},{\"code\":{\"text\":\"Kalium-Ionen-Konz. in Plasma\",\"coding\":[{\"code\":\"K+-POCT\",\"display\":\"Kalium-Ionen-Konz. in Plasma\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"POCT\",\"coding\":[{\"display\":\"POCT\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Peter\"],\"family\":\"Test\"}],\"gender\":\"male\",\"birthDate\":\"1951-04-12\",\"identifier\":[{\"value\":\"pid-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"POCT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20200807_99999999_K+-POCT\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"mmol/L\",\"value\":3.9},\"effectiveDateTime\":\"2020-08-07T08:52:00+02:00\"},{\"code\":{\"text\":\"Natrium-Ionen-Konz. in Plasma\",\"coding\":[{\"code\":\"NA+-POCT\",\"display\":\"Natrium-Ionen-Konz. in Plasma\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"POCT\",\"coding\":[{\"display\":\"POCT\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Peter\"],\"family\":\"Test\"}],\"gender\":\"male\",\"birthDate\":\"1951-04-12\",\"identifier\":[{\"value\":\"pid-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"POCT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20200807_99999999_NA+-POCT\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"mmol/L\",\"value\":139},\"effectiveDateTime\":\"2020-08-07T08:52:00+02:00\"},{\"code\":{\"text\":\"Calcium-Ionen-Konz. in Plasma\",\"coding\":[{\"code\":\"CA++-POCT\",\"display\":\"Calcium-Ionen-Konz. in Plasma\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"POCT\",\"coding\":[{\"display\":\"POCT\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Peter\"],\"family\":\"Test\"}],\"gender\":\"male\",\"birthDate\":\"1951-04-12\",\"identifier\":[{\"value\":\"pid-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"POCT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20200807_99999999_CA++-POCT\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"mmol/L\",\"value\":1.11},\"effectiveDateTime\":\"2020-08-07T08:52:00+02:00\"},{\"code\":{\"text\":\"Chlorid-Ionen-Konz. in Plasma\",\"coding\":[{\"code\":\"CL--POCT\",\"display\":\"Chlorid-Ionen-Konz. in Plasma\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"POCT\",\"coding\":[{\"display\":\"POCT\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Peter\"],\"family\":\"Test\"}],\"gender\":\"male\",\"birthDate\":\"1951-04-12\",\"identifier\":[{\"value\":\"pid-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"POCT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20200807_99999999_CL--POCT\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"mmol/L\",\"value\":110},\"effectiveDateTime\":\"2020-08-07T08:52:00+02:00\"},{\"code\":{\"text\":\"D-Glucose-Konz. in Plasma\",\"coding\":[{\"code\":\"GLU-POCT\",\"display\":\"D-Glucose-Konz. in Plasma\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"POCT\",\"coding\":[{\"display\":\"POCT\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Peter\"],\"family\":\"Test\"}],\"gender\":\"male\",\"birthDate\":\"1951-04-12\",\"identifier\":[{\"value\":\"pid-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"POCT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20200807_99999999_GLU-POCT\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"mg/dl\",\"value\":119},\"effectiveDateTime\":\"2020-08-07T08:52:00+02:00\"},{\"code\":{\"text\":\"L-Lactat-Konz. in Plasma\",\"coding\":[{\"code\":\"LAC-POCT\",\"display\":\"L-Lactat-Konz. in Plasma\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"POCT\",\"coding\":[{\"display\":\"POCT\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Peter\"],\"family\":\"Test\"}],\"gender\":\"male\",\"birthDate\":\"1951-04-12\",\"identifier\":[{\"value\":\"pid-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"POCT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20200807_99999999_LAC-POCT\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"mmol/L\",\"value\":1.0},\"effectiveDateTime\":\"2020-08-07T08:52:00+02:00\"},{\"code\":{\"text\":\"Aktueller Basenüberschuss\",\"coding\":[{\"code\":\"ABE-POCT\",\"display\":\"Aktueller Basenüberschuss\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"POCT\",\"coding\":[{\"display\":\"POCT\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Peter\"],\"family\":\"Test\"}],\"gender\":\"male\",\"birthDate\":\"1951-04-12\",\"identifier\":[{\"value\":\"pid-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"POCT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20200807_99999999_ABE-POCT\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"mmol/L\",\"value\":-2.5},\"effectiveDateTime\":\"2020-08-07T08:52:00+02:00\"},{\"code\":{\"text\":\"Standard-Basenüberschuss\",\"coding\":[{\"code\":\"SBE-POCT\",\"display\":\"Standard-Basenüberschuss\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"POCT\",\"coding\":[{\"display\":\"POCT\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Peter\"],\"family\":\"Test\"}],\"gender\":\"male\",\"birthDate\":\"1951-04-12\",\"identifier\":[{\"value\":\"pid-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"POCT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20200807_99999999_SBE-POCT\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"mmol/L\",\"value\":-2.9},\"effectiveDateTime\":\"2020-08-07T08:52:00+02:00\"},{\"code\":{\"text\":\"Standard-Bikarbonat\",\"coding\":[{\"code\":\"SBC-POCT\",\"display\":\"Standard-Bikarbonat\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"POCT\",\"coding\":[{\"display\":\"POCT\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Peter\"],\"family\":\"Test\"}],\"gender\":\"male\",\"birthDate\":\"1951-04-12\",\"identifier\":[{\"value\":\"pid-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"POCT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20200807_99999999_SBC-POCT\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"mmol/L\",\"value\":22.3},\"effectiveDateTime\":\"2020-08-07T08:52:00+02:00\"},{\"code\":{\"text\":\"Gesamt-Sauerstoff-Konz. in Blut\",\"coding\":[{\"code\":\"TO2-POCT\",\"display\":\"Gesamt-Sauerstoff-Konz. in Blut\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"POCT\",\"coding\":[{\"display\":\"POCT\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Peter\"],\"family\":\"Test\"}],\"gender\":\"male\",\"birthDate\":\"1951-04-12\",\"identifier\":[{\"value\":\"pid-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"POCT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20200807_99999999_TO2-POCT\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"Vol%\",\"value\":15.7},\"effectiveDateTime\":\"2020-08-07T08:52:00+02:00\"},{\"code\":{\"text\":\"Sauerstoff-Partialdruck in Blut bei Halbsättigung (50 %)\",\"coding\":[{\"code\":\"P50(ACT)-POCT\",\"display\":\"Sauerstoff-Partialdruck in Blut bei Halbsättigung (50 %)\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"POCT\",\"coding\":[{\"display\":\"POCT\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Peter\"],\"family\":\"Test\"}],\"gender\":\"male\",\"birthDate\":\"1951-04-12\",\"identifier\":[{\"value\":\"pid-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-2\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"POCT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20200807_99999999_P50(ACT)-POCT\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"mmHg\",\"value\":31.45},\"effectiveDateTime\":\"2020-08-07T08:52:00+02:00\"}]", "effective_date_time": 1596783120, "fhir_version": 4,"inserted_when": 1596784380, "modified": 1596784380, "version_number":1, "deleted_when": null} +3:{"id": 3, "fhir": "{\"id\":\"3\",\"code\":{\"text\":\"Laboratory report\",\"coding\":[{\"code\":\"11502-2\",\"system\":\"http://loinc.org\",\"display\":\"Laboratory report\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"LAB\",\"coding\":[{\"display\":\"LAB\"}]}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Gundula\"],\"family\":\"Testing\"}],\"gender\":\"female\",\"birthDate\":\"1991-01-14\",\"identifier\":[{\"value\":\"pid-3\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"U\",\"display\":\"Unknown\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-3\",\"system\":\"urn:oid:1.3.6.1.4.1.24930.29.238.0\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"BLUT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20221212_11111111\"}],\"resourceType\":\"DiagnosticReport\",\"effectiveDateTime\":\"2022-12-12T05:06:00+01:00\"}","fhir_obs": "[{\"id\":\"#4\",\"code\":{\"text\":\"Quick-Wert (TPZ)\",\"coding\":[{\"code\":\"QUI\",\"display\":\"Quick-Wert (TPZ)\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"Hämostaseologie (TM)\"}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Gundula\"],\"family\":\"Testing\"}],\"gender\":\"female\",\"birthDate\":\"1991-01-14\",\"identifier\":[{\"value\":\"pid-3\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"AMB\",\"system\":\"http://terminology.hl7.org/CodeSystem/v3-ActCode\",\"display\":\"ambulatory\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-3\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"BLUT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20221212_11111111_QUI\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"%\",\"value\":64},\"interpretation\":[{\"coding\":[{\"code\":\"L\"}]}],\"referenceRange\":[{\"low\":{\"unit\":\"%\",\"value\":82},\"high\":{\"unit\":\"%\",\"value\":121}}],\"effectiveDateTime\":\"2022-12-12T05:13:00+01:00\"},{\"id\":\"#5\",\"code\":{\"text\":\"...INR\",\"coding\":[{\"code\":\"INOR\",\"display\":\"...INR\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"Hämostaseologie (TM)\"}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Gundula\"],\"family\":\"Testing\"}],\"gender\":\"female\",\"birthDate\":\"1991-01-14\",\"identifier\":[{\"value\":\"pid-3\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"AMB\",\"system\":\"http://terminology.hl7.org/CodeSystem/v3-ActCode\",\"display\":\"ambulatory\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-3\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"BLUT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20221212_11111111_INOR\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"Ratio\",\"value\":1.3},\"interpretation\":[{\"coding\":[{\"code\":\"H\"}]}],\"referenceRange\":[{\"low\":{\"unit\":\"Ratio\",\"value\":0.85},\"high\":{\"unit\":\"Ratio\",\"value\":1.15}}],\"effectiveDateTime\":\"2022-12-12T05:13:00+01:00\"},{\"id\":\"#6\",\"code\":{\"text\":\"aPTT\",\"coding\":[{\"code\":\"APTT\",\"display\":\"aPTT\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"Hämostaseologie (TM)\"}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Gundula\"],\"family\":\"Testing\"}],\"gender\":\"female\",\"birthDate\":\"1991-01-14\",\"identifier\":[{\"value\":\"pid-3\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"AMB\",\"system\":\"http://terminology.hl7.org/CodeSystem/v3-ActCode\",\"display\":\"ambulatory\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-3\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"BLUT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20221212_11111111_APTT\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"sec\",\"value\":46},\"interpretation\":[{\"coding\":[{\"code\":\"H\"}]}],\"referenceRange\":[{\"low\":{\"unit\":\"sec\",\"value\":23},\"high\":{\"unit\":\"sec\",\"value\":38}}],\"effectiveDateTime\":\"2022-12-12T05:13:00+01:00\"},{\"id\":\"#7\",\"code\":{\"text\":\"Fibrinogen (abgeleitet)\",\"coding\":[{\"code\":\"FBR\",\"display\":\"Fibrinogen (abgeleitet)\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"Hämostaseologie (TM)\"}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Gundula\"],\"family\":\"Testing\"}],\"gender\":\"female\",\"birthDate\":\"1991-01-14\",\"identifier\":[{\"value\":\"pid-3\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"AMB\",\"system\":\"http://terminology.hl7.org/CodeSystem/v3-ActCode\",\"display\":\"ambulatory\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-3\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"BLUT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20221212_11111111_FBR\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"g/l\",\"value\":2.9},\"referenceRange\":[{\"low\":{\"unit\":\"g/l\",\"value\":1.8},\"high\":{\"unit\":\"g/l\",\"value\":5}}],\"effectiveDateTime\":\"2022-12-12T05:13:00+01:00\"},{\"id\":\"#8\",\"code\":{\"text\":\"Fibrinogen nach Clauss\",\"coding\":[{\"code\":\"FIBC\",\"display\":\"Fibrinogen nach Clauss\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"Hämostaseologie (TM)\"}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Gundula\"],\"family\":\"Testing\"}],\"gender\":\"female\",\"birthDate\":\"1991-01-14\",\"identifier\":[{\"value\":\"pid-3\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"AMB\",\"system\":\"http://terminology.hl7.org/CodeSystem/v3-ActCode\",\"display\":\"ambulatory\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-3\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"BLUT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20221212_11111111_FIBC\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"g/L\",\"value\":1.5},\"interpretation\":[{\"coding\":[{\"code\":\"L\"}]}],\"referenceRange\":[{\"low\":{\"unit\":\"g/L\",\"value\":1.8},\"high\":{\"unit\":\"g/L\",\"value\":3.5}}],\"effectiveDateTime\":\"2022-12-12T05:13:00+01:00\"},{\"id\":\"#9\",\"code\":{\"text\":\"Rotationsthrombelastogramm\",\"coding\":[{\"code\":\"RTG\",\"display\":\"Rotationsthrombelastogramm\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"Hämostaseologie (TM)\"}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Gundula\"],\"family\":\"Testing\"}],\"gender\":\"female\",\"birthDate\":\"1991-01-14\",\"identifier\":[{\"value\":\"pid-3\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"AMB\",\"system\":\"http://terminology.hl7.org/CodeSystem/v3-ActCode\",\"display\":\"ambulatory\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-3\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"BLUT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20221212_11111111_RTG\"}],\"valueString\":\"s. Text\",\"resourceType\":\"Observation\",\"interpretation\":[{\"coding\":[{\"code\":\"A\"}]}],\"referenceRange\":[{\"text\":\"normal\"}],\"effectiveDateTime\":\"2022-12-12T05:13:00+01:00\"},{\"id\":\"#10\",\"code\":{\"text\":\"RoTEM Extem CT nach 2-5 Min.\",\"coding\":[{\"code\":\"EXTEM_CT5\",\"display\":\"RoTEM Extem CT nach 2-5 Min.\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"Hämostaseologie (TM)\"}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Gundula\"],\"family\":\"Testing\"}],\"gender\":\"female\",\"birthDate\":\"1991-01-14\",\"identifier\":[{\"value\":\"pid-3\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"AMB\",\"system\":\"http://terminology.hl7.org/CodeSystem/v3-ActCode\",\"display\":\"ambulatory\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-3\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"BLUT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20221212_11111111_EXTEM_CT5\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"sec\",\"value\":98},\"interpretation\":[{\"coding\":[{\"code\":\"H\"}]}],\"referenceRange\":[{\"text\":\"- 90\"}],\"effectiveDateTime\":\"2022-12-12T05:13:00+01:00\"},{\"id\":\"#11\",\"code\":{\"text\":\"RoTEM Extem MCF nach 15 min.\",\"coding\":[{\"code\":\"EXTEM_MCF15\",\"display\":\"RoTEM Extem MCF nach 15 min.\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"Hämostaseologie (TM)\"}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Gundula\"],\"family\":\"Testing\"}],\"gender\":\"female\",\"birthDate\":\"1991-01-14\",\"identifier\":[{\"value\":\"pid-3\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"AMB\",\"system\":\"http://terminology.hl7.org/CodeSystem/v3-ActCode\",\"display\":\"ambulatory\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-3\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"BLUT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20221212_11111111_EXTEM_MCF15\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"mm\",\"value\":49},\"interpretation\":[{\"coding\":[{\"code\":\"L\"}]}],\"referenceRange\":[{\"text\":\"55\"}],\"effectiveDateTime\":\"2022-12-12T05:13:00+01:00\"},{\"id\":\"#12\",\"code\":{\"text\":\"RoTEM Extem ML nach 60 min.\",\"coding\":[{\"code\":\"EXTEM_ML60\",\"display\":\"RoTEM Extem ML nach 60 min.\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"Hämostaseologie (TM)\"}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Gundula\"],\"family\":\"Testing\"}],\"gender\":\"female\",\"birthDate\":\"1991-01-14\",\"identifier\":[{\"value\":\"pid-3\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"AMB\",\"system\":\"http://terminology.hl7.org/CodeSystem/v3-ActCode\",\"display\":\"ambulatory\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-3\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"BLUT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20221212_11111111_EXTEM_ML60\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"%\",\"value\":4},\"referenceRange\":[{\"text\":\"- 15\"}],\"effectiveDateTime\":\"2022-12-12T05:13:00+01:00\"},{\"id\":\"#13\",\"code\":{\"text\":\"RoTEM Intem CT nach 5 min.\",\"coding\":[{\"code\":\"INTEM_CT5\",\"display\":\"RoTEM Intem CT nach 5 min.\"}]},\"note\":[{\"text\":\"Test-Notiz\"}],\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"Hämostaseologie (TM)\"}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Gundula\"],\"family\":\"Testing\"}],\"gender\":\"female\",\"birthDate\":\"1991-01-14\",\"identifier\":[{\"value\":\"pid-3\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"AMB\",\"system\":\"http://terminology.hl7.org/CodeSystem/v3-ActCode\",\"display\":\"ambulatory\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-3\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"BLUT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20221212_11111111_INTEM_CT5\"}],\"valueString\":\"s.Bem.\",\"resourceType\":\"Observation\",\"referenceRange\":[{\"text\":\"- 230\"}],\"effectiveDateTime\":\"2022-12-12T05:13:00+01:00\"},{\"id\":\"#14\",\"code\":{\"text\":\"RoTEM Fibtem MCF nach 15 min.\",\"coding\":[{\"code\":\"FIBTEM_MCF15\",\"display\":\"RoTEM Fibtem MCF nach 15 min.\"}]},\"status\":\"final\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"Hämostaseologie (TM)\"}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Gundula\"],\"family\":\"Testing\"}],\"gender\":\"female\",\"birthDate\":\"1991-01-14\",\"identifier\":[{\"value\":\"pid-3\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"AMB\",\"system\":\"http://terminology.hl7.org/CodeSystem/v3-ActCode\",\"display\":\"ambulatory\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-3\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"BLUT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20221212_11111111_FIBTEM_MCF15\"}],\"resourceType\":\"Observation\",\"valueQuantity\":{\"unit\":\"mm\",\"value\":5},\"interpretation\":[{\"coding\":[{\"code\":\"L\"}]}],\"referenceRange\":[{\"text\":\"9\"}],\"effectiveDateTime\":\"2022-12-12T05:13:00+01:00\"},{\"id\":\"#15\",\"code\":{\"text\":\"Beurteilung\",\"coding\":[{\"code\":\"BEUR\",\"display\":\"Beurteilung\"}]},\"note\":[{\"text\":\"Test-Beurteilung\"}],\"status\":\"registered\",\"subject\":{\"reference\":\"#1\"},\"category\":[{\"text\":\"Hämostaseologie (TM)\"}],\"contained\":[{\"id\":\"1\",\"name\":[{\"given\":[\"Gundula\"],\"family\":\"Testing\"}],\"gender\":\"female\",\"birthDate\":\"1991-01-14\",\"identifier\":[{\"value\":\"pid-3\"}],\"resourceType\":\"Patient\"},{\"id\":\"2\",\"class\":{\"code\":\"AMB\",\"system\":\"http://terminology.hl7.org/CodeSystem/v3-ActCode\",\"display\":\"ambulatory\"},\"status\":\"unknown\",\"identifier\":[{\"value\":\"encounter-3\"}],\"resourceType\":\"Encounter\"},{\"id\":\"3\",\"identifier\":[{\"value\":\"BLUT\"}],\"resourceType\":\"Organization\"}],\"encounter\":{\"reference\":\"#2\"},\"performer\":[{\"reference\":\"#3\"}],\"identifier\":[{\"value\":\"SWISSLAB_20221212_11111111_BEUR\"}],\"resourceType\":\"Observation\",\"effectiveDateTime\":\"2022-12-12T05:13:00+01:00\"}]", "effective_date_time": 1670818380, "fhir_version": 4, "inserted_when": 1670820300, "modified": 1670820540, "version_number": 6, "deleted_when": null} diff --git a/src/main/java/de/unimarburg/diz/labtofhir/LabRunner.java b/src/main/java/de/unimarburg/diz/labtofhir/LabRunner.java index cab63cf..d792e6e 100644 --- a/src/main/java/de/unimarburg/diz/labtofhir/LabRunner.java +++ b/src/main/java/de/unimarburg/diz/labtofhir/LabRunner.java @@ -28,6 +28,7 @@ @Retryable public class LabRunner implements ApplicationRunner { + static final long RETRY_BACKOFF_PERIOD = 2_000L; private static final Logger LOG = LoggerFactory.getLogger(LabRunner.class); private final BindingsEndpoint endpoint; private final AdminClientProvider kafkaAdmin; @@ -35,9 +36,6 @@ public class LabRunner implements ApplicationRunner { private final String updateGroup; private final RetryTemplate retryTemplate; - - static final long RETRY_BACKOFF_PERIOD = 2_000L; - @SuppressWarnings("checkstyle:LineLength") public LabRunner(BindingsEndpoint endpoint, AdminClientProvider kafkaAdmin, @Nullable MappingInfo mappingInfo, @Value( @@ -60,18 +58,13 @@ public void run(ApplicationArguments args) throws Exception { // reset update consumer try (var client = createAdminClient()) { - var offsets = client - .listConsumerGroupOffsets(updateGroup) - .partitionsToOffsetAndMetadata() - .get(); + var offsets = client.listConsumerGroupOffsets(updateGroup) + .partitionsToOffsetAndMetadata().get(); if (!offsets.isEmpty()) { LOG.info("Starting mapping update from {} to {}", - mappingInfo - .update() - .getOldVersion(), mappingInfo - .update() - .getVersion()); + mappingInfo.update().getOldVersion(), + mappingInfo.update().getVersion()); // start update at the beginning // delete consumer group @@ -94,9 +87,7 @@ private Admin createAdminClient() { RetryTemplate setupRetryTemplate() { - return RetryTemplate - .builder() - .customPolicy(new AlwaysRetryPolicy()) + return RetryTemplate.builder().customPolicy(new AlwaysRetryPolicy()) .fixedBackoff(RETRY_BACKOFF_PERIOD) .withListener(new RetryListener() { @Override @@ -105,14 +96,11 @@ public void onError( Throwable throwable) { LOG.debug( "Delete Consumer group failed: {}. " + "Retrying {}", - Optional - .ofNullable(throwable.getCause()) - .orElse(throwable) - .getMessage(), context.getRetryCount()); + Optional.ofNullable(throwable.getCause()) + .orElse(throwable).getMessage(), + context.getRetryCount()); } - }) - .retryOn(ExecutionException.class) - .build(); + }).retryOn(ExecutionException.class).build(); } public void stopAndDeleteUpdateConsumer() throws Exception { @@ -131,8 +119,7 @@ public void stopAndDeleteUpdateConsumer() throws Exception { private void deleteUpdateConsumerGroup() throws Exception { try (var client = createAdminClient()) { retryTemplate.execute(ctx -> client - .deleteConsumerGroups(Collections.singleton(updateGroup)) - .all() + .deleteConsumerGroups(Collections.singleton(updateGroup)).all() .get()); } } diff --git a/src/main/java/de/unimarburg/diz/labtofhir/configuration/KafkaConfiguration.java b/src/main/java/de/unimarburg/diz/labtofhir/configuration/KafkaConfiguration.java index 7e1e255..7a28b96 100644 --- a/src/main/java/de/unimarburg/diz/labtofhir/configuration/KafkaConfiguration.java +++ b/src/main/java/de/unimarburg/diz/labtofhir/configuration/KafkaConfiguration.java @@ -41,9 +41,10 @@ @EnableRetry public class KafkaConfiguration { - private static final Logger LOG = LoggerFactory.getLogger( - KafkaConfiguration.class); - private static final String USE_TYPE_INFO_HEADERS = "spring.cloud.stream.kafka.streams.binder.configuration.spring.json.use.type.headers"; + private static final Logger LOG = + LoggerFactory.getLogger(KafkaConfiguration.class); + private static final String USE_TYPE_INFO_HEADERS = + "spring.cloud.stream.kafka.streams.binder.configuration.spring.json.use.type.headers"; @Bean public StreamsBuilderFactoryBeanConfigurer streamsBuilderCustomizer() { @@ -67,32 +68,23 @@ public AdminClientProvider clientProvider(KafkaAdmin kafkaAdmin) { @Bean public LabOffsets getOffsets(AdminClientProvider kafkaAdmin, - @Value("${spring.cloud.stream.kafka.streams.binder.functions.process.applicationId}") String processGroup, - @Value("${spring.cloud.stream.kafka.streams.binder.functions.update.applicationId}") String updateGroup) - throws ExecutionException, InterruptedException { + @Value("${spring.cloud.stream.kafka.streams.binder.functions.process.applicationId}") + String processGroup, + @Value("${spring.cloud.stream.kafka.streams.binder.functions.update.applicationId}") + String updateGroup) throws ExecutionException, InterruptedException { // get current offsets try (var client = kafkaAdmin.createClient()) { - var processOffsets = client - .listConsumerGroupOffsets(processGroup) - .partitionsToOffsetAndMetadata() - .get(); - - var updateOffsets = client - .listConsumerGroupOffsets(updateGroup) - .partitionsToOffsetAndMetadata() - .get(); - - return new LabOffsets(processOffsets - .entrySet() - .stream() - .collect(Collectors.toMap(e -> e - .getKey() - .partition(), Map.Entry::getValue)), updateOffsets - .entrySet() - .stream() - .collect(Collectors.toMap(e -> e - .getKey() - .partition(), Map.Entry::getValue))); + var processOffsets = client.listConsumerGroupOffsets(processGroup) + .partitionsToOffsetAndMetadata().get(); + + var updateOffsets = client.listConsumerGroupOffsets(updateGroup) + .partitionsToOffsetAndMetadata().get(); + + return new LabOffsets(processOffsets.entrySet().stream().collect( + Collectors.toMap(e -> e.getKey().partition(), + Map.Entry::getValue)), updateOffsets.entrySet().stream() + .collect(Collectors.toMap(e -> e.getKey().partition(), + Map.Entry::getValue))); } } @@ -111,11 +103,9 @@ public Producer createUpdateProducer( @Bean public NewTopic mappingTopic( - @Value("${spring.cloud.stream.kafka.streams.binder.replicationFactor}") int replicas) { - return TopicBuilder - .name("mapping") - .partitions(1) - .replicas(replicas) + @Value("${spring.cloud.stream.kafka.streams.binder.replicationFactor}") + int replicas) { + return TopicBuilder.name("mapping").partitions(1).replicas(replicas) .build(); } @@ -125,7 +115,7 @@ public Consumer createUpdateConsumer( @Value("${" + USE_TYPE_INFO_HEADERS + "}") boolean useTypeHeaders) { var props = new HashMap<>(cf.getConfigurationProperties()); - props.put(ConsumerConfig.GROUP_ID_CONFIG, "mapping-update"); + props.put(ConsumerConfig.GROUP_ID_CONFIG, "lab-mapping"); props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, diff --git a/src/main/java/de/unimarburg/diz/labtofhir/mapper/MiiLabReportMapper.java b/src/main/java/de/unimarburg/diz/labtofhir/mapper/MiiLabReportMapper.java index fb26c0e..0266c61 100644 --- a/src/main/java/de/unimarburg/diz/labtofhir/mapper/MiiLabReportMapper.java +++ b/src/main/java/de/unimarburg/diz/labtofhir/mapper/MiiLabReportMapper.java @@ -164,7 +164,7 @@ public Bundle apply(LaboratoryReport report) { return null; } - LOG.debug("Mapped successfully to FHIR bundle: id:{}, order number{}", + LOG.debug("Mapped successfully to FHIR bundle: id:{}, order number:{}", report.getId(), report.getReportIdentifierValue()); LOG.trace("FHIR bundle: {}", fhirParser.encodeResourceToString(bundle)); diff --git a/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMapEntry.java b/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMapEntry.java index 204ff89..064a9bf 100644 --- a/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMapEntry.java +++ b/src/main/java/de/unimarburg/diz/labtofhir/model/LoincMapEntry.java @@ -58,9 +58,9 @@ public int hashCode() { @JsonPOJOBuilder public static class Builder { - @JsonProperty("CODE") + @JsonProperty("SWL_CODE") private String code; - @JsonProperty("LOINC") + @JsonProperty("CODE_VALUE") private String loinc; @JsonProperty("UCUM_WERT") private String ucum; diff --git a/src/main/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessor.java b/src/main/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessor.java index adf8e77..d6332d5 100644 --- a/src/main/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessor.java +++ b/src/main/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessor.java @@ -25,12 +25,12 @@ @Service public class LabUpdateProcessor { - private static final Logger LOG = LoggerFactory.getLogger( - LabUpdateProcessor.class); - private MappingUpdate mappingVersion; + private static final Logger LOG = + LoggerFactory.getLogger(LabUpdateProcessor.class); private final MiiLabReportMapper reportMapper; private final ConcurrentHashMap offsetState; private final ApplicationEventPublisher eventPublisher; + private MappingUpdate mappingVersion; public LabUpdateProcessor(MiiLabReportMapper reportMapper, @@ -42,13 +42,10 @@ public LabUpdateProcessor(MiiLabReportMapper reportMapper, this.mappingVersion = mappingInfo.update(); } - this.offsetState = new ConcurrentHashMap<>(offsets - .processOffsets() - .entrySet() - .stream() - .collect(Collectors.toMap(Entry::getKey, e -> new OffsetTarget(e - .getValue() - .offset(), false)))); + this.offsetState = new ConcurrentHashMap<>( + offsets.processOffsets().entrySet().stream().collect( + Collectors.toMap(Entry::getKey, + e -> new OffsetTarget(e.getValue().offset(), false)))); } @SuppressWarnings("checkstyle:LineLength") @@ -61,63 +58,70 @@ public Function, KStream> upda @Override public void process(Record record) { - var currentPartition = context() - .recordMetadata() - .orElseThrow() - .partition(); - var currentOffset = context() - .recordMetadata() - .orElseThrow() - .offset(); + var currentPartition = + context().recordMetadata().orElseThrow().partition(); + var currentOffset = + context().recordMetadata().orElseThrow().offset(); // check partitions and offsets var partitionState = offsetState.get(currentPartition); - if (currentOffset > partitionState.offset()) { - if (!partitionState.done()) { - - offsetState.computeIfPresent(currentPartition, - (partition, target) -> new OffsetTarget( - target.offset(), true)); - } + if (currentOffset >= partitionState.offset() + && checkCompleted(offsetState, currentPartition)) { // all done - if (offsetState - .values() - .stream() - .allMatch(s -> s.done)) { - - // send completed event - eventPublisher.publishEvent( - new UpdateCompleted(this)); - } - return; } // filter for update codes - if (record - .value() - .getObservations() - .stream() - .anyMatch(o -> o - .getCode() - .getCoding() - .stream() - .anyMatch(c -> mappingVersion - .getUpdates() + if (record.value().getObservations().stream().anyMatch( + o -> o.getCode().getCoding().stream().anyMatch( + c -> mappingVersion.getUpdates() .contains(c.getCode())))) { // map var bundle = reportMapper.apply(record.value()); - context().forward(record.withValue(bundle)); } + + // check completed for current offset +1 + if (currentOffset + 1 >= partitionState.offset()) { + checkCompleted(offsetState, currentPartition); + } } }) // filter .filter((k, v) -> v != null); } + private boolean checkCompleted( + ConcurrentHashMap offsetState, + int currentPartition) { + var partitionState = offsetState.get(currentPartition); + if (partitionState.done()) { + // already done; just return + return true; + } else { + this.offsetState.computeIfPresent(currentPartition, + (partition, target) -> new OffsetTarget(target.offset(), true)); + + } + + // all done + if (this.offsetState.values().stream().allMatch(s -> s.done)) { + + LOG.info("Update done with offset state {}", offsetState); + // send completed event + sendCompleted(); + return true; + } + + return false; + } + + private void sendCompleted() { + eventPublisher.publishEvent(new UpdateCompleted(this)); + } + private record OffsetTarget(long offset, boolean done) { } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b376fd9..8b1ecd9 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -58,7 +58,7 @@ fhir: mapping: loinc: - version: "2.0.2" + version: "3.0.1" credentials: user: password: diff --git a/src/main/resources/mapping-swl-loinc.zip b/src/main/resources/mapping-swl-loinc.zip index c41003df470aa95af50fc57f55908271cd5fcc5f..68d6dc799b03a2c3e1ced090f450a85495aa8825 100644 GIT binary patch delta 3585 zcmai1eQX=$8Sm?aq;(Pkr8Eho?P16om-piP@_kfVi{m(niJhe9q#r4$T%0%d&Gub5 zpVKB4w37yG0!?%`&1j=rB{WR}#OO}LD=HIf(Nv^Oj7f;WnAC}BV$(h(R-uiJZO@&Z zCMi_-kIUt~=bra@f6w#$p5HzH<<@g&TDvF5nwqz4@P6;_J9Y5uzdkVe-X*l*y-TOM zsUw*KsUz`;p#=6WNA|~3(+NBspH3e+kWi=azUf3NmB53EOng*LBnPL*M}qWNG8V%! zBMG#G4{O%!&guIjr4+`oF)BxL1600J8laL!xl+`Pvk&l_x?|&0SdwIgW-(Y5G{ZKR zmuzCVPi}gy>6X-?w1zo_6FHj4EVE0Aq#UZFro>zTJR`6igwfbW@9st8 z&V{bKwl5N^RI!VCHcuJETqtT~TQ6txA@H6$H16!_zTJ7dtF!MqW{ptm)~e^w`KWud z)7ca9-h%sHm+vu}oTtN2yQh1Du^I0>)< zs-Y25)NI`#?zx@MqKy(ObAWrFb2NO1EAKk9W0QJl3P&YbV85}i$Nfd};pRYk+}SpM zo3rPpc6aai*P1$ffINI8IJ=MDU738vfa5+`F|BZLNL__cV(6#`PXoej&bfWJx77c4 z-rg6lCqfeWC_~G>D{4sBic4k920zz#)4p~?r%SCwhGr5e%*$L|&$R!K)Y|lDiSZ#f^%67Bi{L7 z`ngUh4k%b<>+RA{BpDNq zVd28^n6N_FeXjJ{rfpH*9V^L@@6~b`yLi4-B1Pwcqt9?meM3G9rL=01G;&9YMfK0< zMoyoFJ9Bd}*31v-LI;Byp;QZ#1x7+=Q#Hb`jX~r=hjUdH2^XT7s$#bSKvdqxp z8fOV>&Zu1c9yNB=9j2_9rBQiGbWc7;cOLQ~6+{WTcI}I4LM&3Aw=SN~=54Y9DwK<9 z0ls)dvkX!sIIWX%sRApch^GMdc@KS@(z5xo``vH7e`6C-80UwFDCg(*QJo`1f!Z0t zlFW+|M1SQz8jU!2+`kvD58NNRg`Tubn?NIaxf`ix{^9TcvCXOeCjlnip9pUDY<`pT z(aYQ2KfOE}*fp%;C@TtnyJ9@5=knBi5gLw^vw$3>5?e1+iXIiZkyn1)93F>ylV!Qi zl7_9*qsjdg8{w$CsRd4;VmYcmZq91uuygrm-xO1^Ar%-+;rS>F89ILFLy_ZI=$nUN zt5l30QYo!6Po+(x&b2FSYF&u5gcB+~3QQw1K$r~f!#KaRK&))uGE0O?SW7nC`3&ux zzO-ZKM8jBApi}F{=4aTtKe^P|(FQFYWxa~+!_MWmw)XS|lj#vv10(zPsb zjiV`eH%n#mX0||OmW;VgkZa0YG~74-eWmGy?02pg54(=Mu5t4j@x<1}Bm4a8`Qg@) z^ym0#hJy{dFa2*@Q`2B9S%$4d6zK8XcC_ z=jMv!dReO8+k)N<#nlNFOM)b_5FMn|FBp1l$yd8u(Q5?|B`}F4zm8XnDz#Kv$glL1 zA6+o*%NGe|w%kw^Yl`sG_H|7YOs6NtAq)XB)>r-MMzpo9;SJWc>e-Fx&iX>#y!LJ@ zO5ePhRTx2rZ6d(i#}5p~AHvmpdQewD;;OM*(XF*&4;tC54#%Lxp{OL#TEn2u2-QDX zF$%UfpeKhD$#q%_ru?#PNw3ZMOd)`9B?jF*{oTxbh~zlLtEM#O#$iwNQ-NX{TfKMSK9Do5+)M- z>WNvyj>Pb&cdk-^EsU~m%#mZ&kCP~T8`Q3(;Sr{4_1h= ztP=}{b?u#T^hrlu`gjTwB$6+Es((9(g10tAk9Q_dm#ooF{`1;f_n~K+zmRFj9nqVB zbsG#MwW4#NCCx=$t1W6M(A2^C^Ag6W1e&?}$Q&9#QteA~=)ZxkI6Vk7*^o^@`WOeN z60xc3dwFz6?TH);x13NX;vn{ba3NE$6XRFrU!w2gHxmGMB?D* zztc!3Ii_W8f3hqR&8t)qSF)IsB)PX!LQY}jzP z`h#=msT2GFGr)#$kb++*r`vSaER}RSw3`ZPAXhnY_Ucp61KC2kQu3ZDQH7621zDKo z_*q4q6E%%v;a1B6_iNf5mnE{2C1jqRCt_5YQxy1Ak=R+nL%LyQWyij(MW7evJf9o` z{l~zxlcrc^Hyho}hyyGm2vMQ-{>$hj+PL!3RXuec^;RRVpvQx&pI`oB4Ho|zxN-gW kT6Nc}Xg9U)>~Q{@{4cxuqEO4}HxMlSA7QRi6951J delta 920 zcmY*YTWB0*6wcY%`z1H>kS3B&)+{#3O!Cjn&hAX8NV3%uHaC}Tlhk_I*_~|Hba&$J zY+kmp5ek+TY3i@t6lxMwXsZR0AN<{iiiV;lwzd`Vv9@X-qzHW~KBzc}=Annffpg*e z&VlpQ)?8QKaJ5dv8GB3JmK}UPH95cN_-v{MbyKyot*kdTzVBIXWPErmb~e8I(`C^^ z?t3mVWIb@g%ZyC#!+YDh@SWCGe}a=xSde&?3vqa&t-GBQRWTy)vXzhKW{XB?)|lhG zgUONU1b)=oiC<}Ju4nh8v{GMp@c;53yV}XswmF8}=s4$a%M#kgBgL*a9VFEA9<+o- zS?0rBsFEuj$!R$=XA~CjW^X6%?pPxade?T7TD)i{FHOA8;HjjasXV!WUrokv*MTDb z`oL*&;^18yzC3-wtq5vVv3AADL;Ym)&~G+Saphx{FX_c{ey(Vkdal6k8P$sxgI;3G zg={&OE+0*qxeivn(aG#;?dZCe_Hv-TDw6g)m zZPk?~xLJL>8NQ+oEf8;tNs_7}UgG)%gesFmu?23hY#RBL$&DGsyrCUUl}x?BhP-2m zp;(HG3bHjUpdPhC=eq-ADOqL2}gph=RJmLiHKr<55r5{+gQP17>6h9Wzdp>$DCnR;dm zQ$(V~3j&XXq<{u24~P4rDw3ii&2GXXFqS^5tJmR@V_PG1?K*gv`p0Edr8i-Kee6!_ Y*2ZnY{TCKtsa9>e1wm)ymtd9t1?x5|vH$=8 diff --git a/src/test/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessorTests.java b/src/test/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessorTests.java index fc7e907..0490295 100644 --- a/src/test/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessorTests.java +++ b/src/test/java/de/unimarburg/diz/labtofhir/processor/LabUpdateProcessorTests.java @@ -51,13 +51,15 @@ void updateIsProcessed() { var labTopic = createInputTopic(driver); var outputTopic = createOutputTopic(driver); - var inputReports = List.of(createReport(1, "NA"), - createReport(2, "ERY"), createReport(3, "NA"), - createReport(4, "NA")); + // NA values are updated, but update only processes the first two + // because the last one (4) is where the default processor picks up + // according to processOffsets() + var inputReports = + List.of(createReport(1, "NA"), createReport(2, "ERY"), + createReport(3, "NA"), createReport(4, "NA")); // create input records - labTopic.pipeKeyValueList(inputReports - .stream() + labTopic.pipeKeyValueList(inputReports.stream() .map(r -> new KeyValue<>(String.valueOf(r.getId()), r)) .toList()); @@ -65,34 +67,32 @@ void updateIsProcessed() { var outputRecords = outputTopic.readRecordsToList(); // expected keys are: 1, 3 - assertThat(outputRecords - .stream() - .map(TestRecord::getKey) + assertThat(outputRecords.stream().map(TestRecord::getKey) .toList()).isEqualTo(List.of("1", "3")); // assert codes are mapped var obsCodes = getObservationsCodes(outputRecords).toList(); + // all updated observations have LOINC coding for NA assertThat(obsCodes).allMatch( - c -> c.hasCoding("http://loinc.org", "2951-2")); + codes -> codes.hasCoding("http://loinc.org", "2951-2")); } } private LaboratoryReport createReport(int reportId, String labCode) { return createReport(reportId, new Coding() - .setSystem(fhirProperties - .getSystems() - .getLaboratorySystem()) + .setSystem(fhirProperties.getSystems().getLaboratorySystem()) .setCode(labCode)); } @TestConfiguration static class KafkaConfig { + @SuppressWarnings("checkstyle:MagicNumber") @Bean LabOffsets testOffsets() { - // offset target will be 2 on partition 0 - return new LabOffsets(Map.of(0, new OffsetAndMetadata(2L)), + // offset target will be 3 on partition 0 + return new LabOffsets(Map.of(0, new OffsetAndMetadata(3L)), Map.of()); } diff --git a/src/test/resources/mapping-swl-loinc.zip b/src/test/resources/mapping-swl-loinc.zip index c41003df470aa95af50fc57f55908271cd5fcc5f..68d6dc799b03a2c3e1ced090f450a85495aa8825 100644 GIT binary patch delta 3585 zcmai1eQX=$8Sm?aq;(Pkr8Eho?P16om-piP@_kfVi{m(niJhe9q#r4$T%0%d&Gub5 zpVKB4w37yG0!?%`&1j=rB{WR}#OO}LD=HIf(Nv^Oj7f;WnAC}BV$(h(R-uiJZO@&Z zCMi_-kIUt~=bra@f6w#$p5HzH<<@g&TDvF5nwqz4@P6;_J9Y5uzdkVe-X*l*y-TOM zsUw*KsUz`;p#=6WNA|~3(+NBspH3e+kWi=azUf3NmB53EOng*LBnPL*M}qWNG8V%! zBMG#G4{O%!&guIjr4+`oF)BxL1600J8laL!xl+`Pvk&l_x?|&0SdwIgW-(Y5G{ZKR zmuzCVPi}gy>6X-?w1zo_6FHj4EVE0Aq#UZFro>zTJR`6igwfbW@9st8 z&V{bKwl5N^RI!VCHcuJETqtT~TQ6txA@H6$H16!_zTJ7dtF!MqW{ptm)~e^w`KWud z)7ca9-h%sHm+vu}oTtN2yQh1Du^I0>)< zs-Y25)NI`#?zx@MqKy(ObAWrFb2NO1EAKk9W0QJl3P&YbV85}i$Nfd};pRYk+}SpM zo3rPpc6aai*P1$ffINI8IJ=MDU738vfa5+`F|BZLNL__cV(6#`PXoej&bfWJx77c4 z-rg6lCqfeWC_~G>D{4sBic4k920zz#)4p~?r%SCwhGr5e%*$L|&$R!K)Y|lDiSZ#f^%67Bi{L7 z`ngUh4k%b<>+RA{BpDNq zVd28^n6N_FeXjJ{rfpH*9V^L@@6~b`yLi4-B1Pwcqt9?meM3G9rL=01G;&9YMfK0< zMoyoFJ9Bd}*31v-LI;Byp;QZ#1x7+=Q#Hb`jX~r=hjUdH2^XT7s$#bSKvdqxp z8fOV>&Zu1c9yNB=9j2_9rBQiGbWc7;cOLQ~6+{WTcI}I4LM&3Aw=SN~=54Y9DwK<9 z0ls)dvkX!sIIWX%sRApch^GMdc@KS@(z5xo``vH7e`6C-80UwFDCg(*QJo`1f!Z0t zlFW+|M1SQz8jU!2+`kvD58NNRg`Tubn?NIaxf`ix{^9TcvCXOeCjlnip9pUDY<`pT z(aYQ2KfOE}*fp%;C@TtnyJ9@5=knBi5gLw^vw$3>5?e1+iXIiZkyn1)93F>ylV!Qi zl7_9*qsjdg8{w$CsRd4;VmYcmZq91uuygrm-xO1^Ar%-+;rS>F89ILFLy_ZI=$nUN zt5l30QYo!6Po+(x&b2FSYF&u5gcB+~3QQw1K$r~f!#KaRK&))uGE0O?SW7nC`3&ux zzO-ZKM8jBApi}F{=4aTtKe^P|(FQFYWxa~+!_MWmw)XS|lj#vv10(zPsb zjiV`eH%n#mX0||OmW;VgkZa0YG~74-eWmGy?02pg54(=Mu5t4j@x<1}Bm4a8`Qg@) z^ym0#hJy{dFa2*@Q`2B9S%$4d6zK8XcC_ z=jMv!dReO8+k)N<#nlNFOM)b_5FMn|FBp1l$yd8u(Q5?|B`}F4zm8XnDz#Kv$glL1 zA6+o*%NGe|w%kw^Yl`sG_H|7YOs6NtAq)XB)>r-MMzpo9;SJWc>e-Fx&iX>#y!LJ@ zO5ePhRTx2rZ6d(i#}5p~AHvmpdQewD;;OM*(XF*&4;tC54#%Lxp{OL#TEn2u2-QDX zF$%UfpeKhD$#q%_ru?#PNw3ZMOd)`9B?jF*{oTxbh~zlLtEM#O#$iwNQ-NX{TfKMSK9Do5+)M- z>WNvyj>Pb&cdk-^EsU~m%#mZ&kCP~T8`Q3(;Sr{4_1h= ztP=}{b?u#T^hrlu`gjTwB$6+Es((9(g10tAk9Q_dm#ooF{`1;f_n~K+zmRFj9nqVB zbsG#MwW4#NCCx=$t1W6M(A2^C^Ag6W1e&?}$Q&9#QteA~=)ZxkI6Vk7*^o^@`WOeN z60xc3dwFz6?TH);x13NX;vn{ba3NE$6XRFrU!w2gHxmGMB?D* zztc!3Ii_W8f3hqR&8t)qSF)IsB)PX!LQY}jzP z`h#=msT2GFGr)#$kb++*r`vSaER}RSw3`ZPAXhnY_Ucp61KC2kQu3ZDQH7621zDKo z_*q4q6E%%v;a1B6_iNf5mnE{2C1jqRCt_5YQxy1Ak=R+nL%LyQWyij(MW7evJf9o` z{l~zxlcrc^Hyho}hyyGm2vMQ-{>$hj+PL!3RXuec^;RRVpvQx&pI`oB4Ho|zxN-gW kT6Nc}Xg9U)>~Q{@{4cxuqEO4}HxMlSA7QRi6951J delta 920 zcmY*YTWB0*6wcY%`z1H>kS3B&)+{#3O!Cjn&hAX8NV3%uHaC}Tlhk_I*_~|Hba&$J zY+kmp5ek+TY3i@t6lxMwXsZR0AN<{iiiV;lwzd`Vv9@X-qzHW~KBzc}=Annffpg*e z&VlpQ)?8QKaJ5dv8GB3JmK}UPH95cN_-v{MbyKyot*kdTzVBIXWPErmb~e8I(`C^^ z?t3mVWIb@g%ZyC#!+YDh@SWCGe}a=xSde&?3vqa&t-GBQRWTy)vXzhKW{XB?)|lhG zgUONU1b)=oiC<}Ju4nh8v{GMp@c;53yV}XswmF8}=s4$a%M#kgBgL*a9VFEA9<+o- zS?0rBsFEuj$!R$=XA~CjW^X6%?pPxade?T7TD)i{FHOA8;HjjasXV!WUrokv*MTDb z`oL*&;^18yzC3-wtq5vVv3AADL;Ym)&~G+Saphx{FX_c{ey(Vkdal6k8P$sxgI;3G zg={&OE+0*qxeivn(aG#;?dZCe_Hv-TDw6g)m zZPk?~xLJL>8NQ+oEf8;tNs_7}UgG)%gesFmu?23hY#RBL$&DGsyrCUUl}x?BhP-2m zp;(HG3bHjUpdPhC=eq-ADOqL2}gph=RJmLiHKr<55r5{+gQP17>6h9Wzdp>$DCnR;dm zQ$(V~3j&XXq<{u24~P4rDw3ii&2GXXFqS^5tJmR@V_PG1?K*gv`p0Edr8i-Kee6!_ Y*2ZnY{TCKtsa9>e1wm)ymtd9t1?x5|vH$=8 From fc61cd7cabdd7c2af1f5427e0393374d328e57c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20St=C3=B6cker?= Date: Fri, 17 May 2024 11:24:38 +0200 Subject: [PATCH 20/21] docs: add Mapping updates section in the README --- README.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0193668..6a4df50 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ The following environment variables can be set: | SSL_TRUST_STORE_PASSWORD | | Truststore password (if using `SECURITY_PROTOCOL=SSL`) | | INPUT_TOPIC | aim-lab | Topic to read from | | OUTPUT_TOPIC | lab-fhir | Topic to store result bundles | -| MAPPING_LOINC_VERSION | 2.0.1 | LOINC mapping package version: [Package Registry · mapping / loinc-mapping](https://gitlab.diz.uni-marburg.de/mapping/loinc-mapping/-/packages/)) | +| MAPPING_LOINC_VERSION | 3.0.1 | LOINC mapping package version: [Package Registry · mapping / loinc-mapping](https://gitlab.diz.uni-marburg.de/mapping/loinc-mapping/-/packages/)) | | MAPPING_LOINC_CREDENTIALS_USER | | LOINC mapping package registry user | | MAPPING_LOINC_CREDENTIALS_PASSWORD | | LOINC mapping package registry password | | MAPPING_LOINC_PROXY | | Proxy server to use when pulling the package | @@ -53,6 +53,24 @@ The following environment variables can be set: Additional application properties can be set by overriding values form the [application.yml](src/main/resources/application.yml) by using environment variables. +## Mapping updates + +In addition to the regular Kafka processor this application uses a separate +update processor to apply mapping updates to all records up until the +current offset state of the regular processor. + +The update processor is a separate Kafka consumer and keeps its own offset +state in order to be able to resume unfinished updates. On completion, the +update consumer group is deleted. + +On startup, the application checks the configured mapping version and +determines a diff between the mappings of the current and the last used +mapping version. This data is stored in the Kafka topic `mapping` with the key +`lab-update`. + +In case there are no changes or the mapping versions used are equal, the +update processor is not started. + ## Tests This project includes unit and integration tests. From 4f5328136866ec9d127be62b91299faff0621a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20St=C3=B6cker?= Date: Fri, 17 May 2024 12:55:08 +0200 Subject: [PATCH 21/21] ci: fix markdown linting --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 6a4df50..43435e9 100644 --- a/README.md +++ b/README.md @@ -55,20 +55,20 @@ Additional application properties can be set by overriding values form the [appl ## Mapping updates -In addition to the regular Kafka processor this application uses a separate -update processor to apply mapping updates to all records up until the +In addition to the regular Kafka processor this application uses a separate +update processor to apply mapping updates to all records up until the current offset state of the regular processor. -The update processor is a separate Kafka consumer and keeps its own offset -state in order to be able to resume unfinished updates. On completion, the +The update processor is a separate Kafka consumer and keeps its own offset +state in order to be able to resume unfinished updates. On completion, the update consumer group is deleted. -On startup, the application checks the configured mapping version and -determines a diff between the mappings of the current and the last used +On startup, the application checks the configured mapping version and +determines a diff between the mappings of the current and the last used mapping version. This data is stored in the Kafka topic `mapping` with the key `lab-update`. -In case there are no changes or the mapping versions used are equal, the +In case there are no changes or the mapping versions used are equal, the update processor is not started. ## Tests