Skip to content
This repository was archived by the owner on Oct 6, 2025. It is now read-only.

Commit bd6d71b

Browse files
committed
better search for new data, read new data and store data implementations
1 parent 510aadc commit bd6d71b

7 files changed

Lines changed: 492 additions & 47 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package de.netzwerk_universitaetsmedizin.codex.processes.data_transfer.client;
2+
3+
import java.nio.file.Path;
4+
import java.util.Arrays;
5+
import java.util.Date;
6+
import java.util.List;
7+
import java.util.UUID;
8+
import java.util.stream.Stream;
9+
10+
import org.hl7.fhir.r4.model.Bundle;
11+
import org.hl7.fhir.r4.model.Condition;
12+
import org.hl7.fhir.r4.model.DomainResource;
13+
import org.hl7.fhir.r4.model.IdType;
14+
import org.hl7.fhir.r4.model.Observation;
15+
import org.hl7.fhir.r4.model.Patient;
16+
import org.hl7.fhir.r4.model.Reference;
17+
import org.slf4j.Logger;
18+
import org.slf4j.LoggerFactory;
19+
20+
import ca.uhn.fhir.context.FhirContext;
21+
import de.netzwerk_universitaetsmedizin.codex.processes.data_transfer.ConstantsDataTransfer;
22+
import de.netzwerk_universitaetsmedizin.codex.processes.data_transfer.domain.DateWithPrecision;
23+
import de.netzwerk_universitaetsmedizin.codex.processes.data_transfer.variables.PseudonymList;
24+
25+
public class FhirClientFactory
26+
{
27+
private static final String condition = "{\"resourceType\":\"Condition\",\"meta\":{\"profile\":[\"https://www.netzwerk-universitaetsmedizin.de/fhir/StructureDefinition/chronic-lung-diseases\"]},\"clinicalStatus\":{\"coding\":[{\"system\":\"http://terminology.hl7.org/CodeSystem/condition-clinical\",\"code\":\"active\",\"display\":\"Active\"}]},\"verificationStatus\":{\"coding\":[{\"system\":\"http://terminology.hl7.org/CodeSystem/condition-ver-status\",\"code\":\"confirmed\",\"display\":\"Confirmed\"},{\"system\":\"http://snomed.info/sct\",\"code\":\"410605003\",\"display\":\"Confirmed present (qualifier value)\"}]},\"category\":[{\"coding\":[{\"system\":\"http://snomed.info/sct\",\"code\":\"418112009\",\"display\":\"Pulmonary medicine\"}]}],\"code\":{\"coding\":[{\"system\":\"http://snomed.info/sct\",\"code\":\"413839001\",\"display\":\"Chronic lung disease\"}]},\"recordedDate\":\"2020-11-10T15:50:41+01:00\"}";
28+
private static final String patient = "{\"resourceType\":\"Patient\",\"meta\":{\"profile\":[\"https://www.netzwerk-universitaetsmedizin.de/fhir/StructureDefinition/Patient\"]},\"extension\":[{\"url\":\"https://www.netzwerk-universitaetsmedizin.de/fhir/StructureDefinition/ethnic-group\",\"valueCoding\":{\"system\":\"http://snomed.info/sct\",\"code\":\"186019001\",\"display\":\"Other ethnic, mixed origin\"}},{\"url\":\"https://www.netzwerk-universitaetsmedizin.de/fhir/StructureDefinition/age\",\"extension\":[{\"url\":\"dateTimeOfDocumentation\",\"valueDateTime\":\"2020-10-01\"},{\"url\":\"age\",\"valueAge\":{\"value\":67,\"unit\":\"years\",\"system\":\"http://unitsofmeasure.org\",\"code\":\"a\"}}]}],\"birthDate\":\"1953-09-30\"}";
29+
private static final String observation = "{\"resourceType\":\"Observation\",\"meta\":{\"profile\":[\"https://www.netzwerk-universitaetsmedizin.de/fhir/StructureDefinition/sars-cov-2-rt-pcr\"]},\"identifier\":[{\"type\":{\"coding\":[{\"system\":\"http://terminology.hl7.org/CodeSystem/v2-0203\",\"code\":\"OBI\"}]}}],\"status\":\"final\",\"category\":[{\"coding\":[{\"system\":\"http://loinc.org\",\"code\":\"26436-6\"},{\"system\":\"http://terminology.hl7.org/CodeSystem/observation-category\",\"code\":\"laboratory\"}]}],\"code\":{\"coding\":[{\"system\":\"http://loinc.org\",\"code\":\"94500-6\",\"display\":\"SARS-CoV-2 (COVID-19) RNA [Presence] in Respiratory specimen by NAA with probe detection\"}],\"text\":\"SARS-CoV-2-RNA (PCR)\"},\"effectiveDateTime\":\"2020-11-10T15:50:41+01:00\",\"valueCodeableConcept\":{\"coding\":[{\"system\":\"http://snomed.info/sct\",\"code\":\"260373001\",\"display\":\"Detected (qualifier value)\"}],\"text\":\"SARS-CoV-2-RNA positiv\"}}";
30+
31+
private static final Logger logger = LoggerFactory.getLogger(FhirClientFactory.class);
32+
33+
private final HapiFhirClientFactory hapiClientFactory;
34+
private final FhirContext fhirContext;
35+
private final Path searchBundleOverride;
36+
private final String localIdentifierValue;
37+
38+
public FhirClientFactory(HapiFhirClientFactory hapiClientFactory, FhirContext fhirContext,
39+
Path searchBundleOverride, String localIdentifierValue)
40+
{
41+
this.hapiClientFactory = hapiClientFactory;
42+
this.fhirContext = fhirContext;
43+
this.searchBundleOverride = searchBundleOverride;
44+
this.localIdentifierValue = localIdentifierValue;
45+
}
46+
47+
public FhirClient getFhirClient()
48+
{
49+
if (hapiClientFactory.isConfigured())
50+
return new FhirClientImpl(hapiClientFactory, fhirContext, searchBundleOverride);
51+
else
52+
return createFhirClientStub();
53+
}
54+
55+
private FhirClient createFhirClientStub()
56+
{
57+
return new FhirClient()
58+
{
59+
@Override
60+
public void storeBundle(Bundle bundle)
61+
{
62+
logger.warn("Ignoring bundle with {} {}", bundle.getEntry().size(),
63+
bundle.getEntry().size() != 1 ? "entries" : "entry");
64+
65+
if (logger.isDebugEnabled())
66+
logger.debug("Ignored bundle: {}", fhirContext.newJsonParser().encodeResourceToString(bundle));
67+
}
68+
69+
@Override
70+
public PseudonymList getPseudonymsWithNewData(DateWithPrecision exportFrom, Date exportTo)
71+
{
72+
logger.warn("Returning demo pseudonyms for {}", localIdentifierValue);
73+
74+
List<String> pseudonyms;
75+
if ("charite-tmptst.de".equals(localIdentifierValue))
76+
pseudonyms = Arrays.asList("dic_berlin/dic_CT6E6", "dic_berlin/dic_9LDA5");
77+
else if ("klinikum.uni-heidelberg.de".equals(localIdentifierValue))
78+
pseudonyms = Arrays.asList("dic_heidelberg/dic_3YKQW", "dic_heidelberg/dic_RPRM3");
79+
else
80+
pseudonyms = Arrays.asList("foo/bar", "baz/qux");
81+
82+
return new PseudonymList(pseudonyms);
83+
}
84+
85+
@Override
86+
public Stream<DomainResource> getNewData(String pseudonym, DateWithPrecision exportFrom, Date exportTo)
87+
{
88+
logger.warn("Returning demo resources for {}", pseudonym);
89+
90+
Patient p = fhirContext.newJsonParser().parseResource(Patient.class, patient);
91+
p.addIdentifier().setSystem(ConstantsDataTransfer.NAMING_SYSTEM_NUM_CODEX_DIC_PSEUDONYM)
92+
.setValue(pseudonym);
93+
p.setIdElement(new IdType("Patient", UUID.randomUUID().toString()));
94+
95+
Condition c = fhirContext.newJsonParser().parseResource(Condition.class, condition);
96+
c.setSubject(new Reference(p.getIdElement()));
97+
98+
Observation o = fhirContext.newJsonParser().parseResource(Observation.class, observation);
99+
o.setSubject(new Reference(p.getIdElement()));
100+
101+
return Stream.of(p, c, o);
102+
}
103+
};
104+
}
105+
}

codex-process-data-transfer/src/main/java/de/netzwerk_universitaetsmedizin/codex/processes/data_transfer/client/FhirClientImpl.java

Lines changed: 108 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import java.util.EnumSet;
1111
import java.util.List;
1212
import java.util.Objects;
13-
import java.util.function.Consumer;
13+
import java.util.function.Function;
1414
import java.util.regex.Matcher;
1515
import java.util.regex.Pattern;
1616
import java.util.stream.Collectors;
@@ -27,6 +27,7 @@
2727

2828
import ca.uhn.fhir.context.FhirContext;
2929
import ca.uhn.fhir.parser.DataFormatException;
30+
import ca.uhn.fhir.rest.api.Constants;
3031
import de.netzwerk_universitaetsmedizin.codex.processes.data_transfer.ConstantsDataTransfer;
3132
import de.netzwerk_universitaetsmedizin.codex.processes.data_transfer.domain.DateWithPrecision;
3233
import de.netzwerk_universitaetsmedizin.codex.processes.data_transfer.variables.PseudonymList;
@@ -104,7 +105,7 @@ public FhirClientImpl(HapiFhirClientFactory clientFactory, FhirContext fhirConte
104105
*/
105106
private Bundle getSearchBundle(DateWithPrecision exportFrom, Date exportTo)
106107
{
107-
return doGetSearchBundle(null, exportFrom, exportTo, true);
108+
return doGetSearchBundle(null, null, exportFrom, exportTo, true);
108109
}
109110

110111
/**
@@ -116,12 +117,30 @@ private Bundle getSearchBundle(DateWithPrecision exportFrom, Date exportTo)
116117
* not <code>null</code>
117118
* @return
118119
*/
119-
private Bundle getSearchBundle(String pseudonym, DateWithPrecision exportFrom, Date exportTo)
120+
private Bundle getSearchBundleWithPseudonym(String pseudonym, DateWithPrecision exportFrom, Date exportTo)
120121
{
121-
return doGetSearchBundle(pseudonym, exportFrom, exportTo, false);
122+
Objects.requireNonNull(pseudonym, "pseudonym");
123+
124+
return doGetSearchBundle(null, pseudonym, exportFrom, exportTo, false);
122125
}
123126

124-
private Bundle doGetSearchBundle(String pseudonym, DateWithPrecision exportFrom, Date exportTo,
127+
/**
128+
* @param patientId
129+
* not <code>null</code>
130+
* @param exportFrom
131+
* may be <code>null</code>
132+
* @param exportTo
133+
* not <code>null</code>
134+
* @return
135+
*/
136+
private Bundle getSearchBundleWithPatientId(String patientId, DateWithPrecision exportFrom, Date exportTo)
137+
{
138+
Objects.requireNonNull(patientId, "patientId");
139+
140+
return doGetSearchBundle(patientId, null, exportFrom, exportTo, false);
141+
}
142+
143+
private Bundle doGetSearchBundle(String patientId, String pseudonym, DateWithPrecision exportFrom, Date exportTo,
125144
boolean includePatient)
126145
{
127146
Objects.requireNonNull(exportTo, "exportTo");
@@ -134,13 +153,17 @@ private Bundle doGetSearchBundle(String pseudonym, DateWithPrecision exportFrom,
134153
throw new RuntimeException("Search-Bundle type not batch or transaction");
135154
}
136155

137-
bundle.getEntry().forEach(modifySearchUrl(pseudonym, exportFrom, exportTo, includePatient));
156+
List<BundleEntryComponent> entries = bundle.getEntry().stream()
157+
.map(modifySearchUrl(patientId, pseudonym, exportFrom, exportTo, includePatient)).filter(e -> e != null)
158+
.collect(Collectors.toList());
159+
160+
bundle.setEntry(entries);
138161

139162
return bundle;
140163
}
141164

142-
private Consumer<? super BundleEntryComponent> modifySearchUrl(String pseudonym, DateWithPrecision exportFrom,
143-
Date exportTo, boolean includePatient)
165+
private Function<BundleEntryComponent, BundleEntryComponent> modifySearchUrl(String patientId, String pseudonym,
166+
DateWithPrecision exportFrom, Date exportTo, boolean includePatient)
144167
{
145168
return entry ->
146169
{
@@ -163,13 +186,20 @@ private Consumer<? super BundleEntryComponent> modifySearchUrl(String pseudonym,
163186

164187
if (RESOURCES_WITH_PATIENT_REF.contains(resource))
165188
{
166-
query += createPatPrefixPseudonymSearchUrlPart(pseudonym);
189+
if (patientId != null)
190+
query += createPatIdSearchUrlPart(patientId);
191+
else
192+
query += createPatPrefixPseudonymSearchUrlPart(pseudonym);
167193

168194
if (includePatient)
169195
query += createIncludeSearchUrlPart(resource);
170196
}
171197
else if ("Patient".equals(resource))
172198
{
199+
// filtering search for patient if patient id known
200+
if (patientId != null)
201+
return null;
202+
173203
query += createPseudonymSearchUrlPart(pseudonym);
174204
}
175205
else
@@ -185,6 +215,8 @@ else if ("Patient".equals(resource))
185215
query += createExportToSearchUrlPart(exportTo);
186216

187217
entry.getRequest().setUrl(resource + query);
218+
219+
return entry;
188220
};
189221
}
190222

@@ -196,6 +228,14 @@ private String createPseudonymSearchUrlPart(String pseudonym)
196228
return "";
197229
}
198230

231+
private String createPatIdSearchUrlPart(String patientId)
232+
{
233+
if (patientId != null && !patientId.isBlank())
234+
return "&patient=" + patientId;
235+
else
236+
return "";
237+
}
238+
199239
private String createPatPrefixPseudonymSearchUrlPart(String pseudonym)
200240
{
201241
if (pseudonym != null && !pseudonym.isBlank())
@@ -279,7 +319,7 @@ private Bundle readSerachBundleTemplate()
279319
{
280320
try (InputStream in = FhirClientImpl.class.getResourceAsStream("/fhir/Bundle/SearchBundle.xml"))
281321
{
282-
logger.warn("Using internal Search-Bundle");
322+
logger.info("Using internal Search-Bundle");
283323
return fhirContext.newXmlParser().parseResource(Bundle.class, in);
284324
}
285325

@@ -301,7 +341,8 @@ public PseudonymList getPseudonymsWithNewData(DateWithPrecision exportFrom, Date
301341
logger.debug("Executing Search-Bundle: {}",
302342
fhirContext.newJsonParser().encodeResourceToString(searchBundle));
303343

304-
Bundle resultBundle = clientFactory.getFhirStoreClient().transaction().withBundle(searchBundle).execute();
344+
Bundle resultBundle = clientFactory.getFhirStoreClient().transaction().withBundle(searchBundle)
345+
.withAdditionalHeader(Constants.HEADER_PREFER, "handling=strict").execute();
305346

306347
if (logger.isDebugEnabled())
307348
logger.debug("Search-Bundle result: {}", fhirContext.newJsonParser().encodeResourceToString(resultBundle));
@@ -352,7 +393,8 @@ private Bundle continueSearch(String url)
352393
if (logger.isDebugEnabled())
353394
logger.debug("Executing search: {}", url);
354395

355-
Bundle resultBundle = (Bundle) clientFactory.getFhirStoreClient().search().byUrl(url).execute();
396+
Bundle resultBundle = (Bundle) clientFactory.getFhirStoreClient().search().byUrl(url)
397+
.withAdditionalHeader(Constants.HEADER_PREFER, "handling=strict").execute();
356398

357399
if (logger.isDebugEnabled())
358400
logger.debug("Search-Bundle result: {}", fhirContext.newJsonParser().encodeResourceToString(resultBundle));
@@ -363,13 +405,23 @@ private Bundle continueSearch(String url)
363405
@Override
364406
public Stream<DomainResource> getNewData(String pseudonym, DateWithPrecision exportFrom, Date exportTo)
365407
{
366-
Bundle searchBundle = getSearchBundle(pseudonym, exportFrom, exportTo);
408+
if (clientFactory.supportsIdentifierReferenceSearch())
409+
return getNewDataWithIdentifierReferenceSupport(pseudonym, exportFrom, exportTo);
410+
else
411+
return getNewDataWithoutIdentifierReferenceSupport(pseudonym, exportFrom, exportTo);
412+
}
413+
414+
private Stream<DomainResource> getNewDataWithIdentifierReferenceSupport(String pseudonym,
415+
DateWithPrecision exportFrom, Date exportTo)
416+
{
417+
Bundle searchBundle = getSearchBundleWithPseudonym(pseudonym, exportFrom, exportTo);
367418

368419
if (logger.isDebugEnabled())
369420
logger.debug("Executing Search-Bundle: {}",
370421
fhirContext.newJsonParser().encodeResourceToString(searchBundle));
371422

372-
Bundle resultBundle = clientFactory.getFhirStoreClient().transaction().withBundle(searchBundle).execute();
423+
Bundle resultBundle = clientFactory.getFhirStoreClient().transaction().withBundle(searchBundle)
424+
.withAdditionalHeader(Constants.HEADER_PREFER, "handling=strict").execute();
373425

374426
if (logger.isDebugEnabled())
375427
logger.debug("Search-Bundle result: {}", fhirContext.newJsonParser().encodeResourceToString(resultBundle));
@@ -378,6 +430,45 @@ public Stream<DomainResource> getNewData(String pseudonym, DateWithPrecision exp
378430
.map(e -> (Bundle) e.getResource()).flatMap(this::getDomainResources);
379431
}
380432

433+
private Stream<DomainResource> getNewDataWithoutIdentifierReferenceSupport(String pseudonym,
434+
DateWithPrecision exportFrom, Date exportTo)
435+
{
436+
Bundle patientBundle = (Bundle) clientFactory.getFhirStoreClient().search().forResource(Patient.class)
437+
.where(Patient.IDENTIFIER.exactly()
438+
.systemAndIdentifier(ConstantsDataTransfer.NAMING_SYSTEM_NUM_CODEX_DIC_PSEUDONYM, pseudonym))
439+
.execute();
440+
441+
if (logger.isDebugEnabled())
442+
logger.debug("Patient search-bundle result: {}",
443+
fhirContext.newJsonParser().encodeResourceToString(patientBundle));
444+
445+
if (patientBundle.getTotal() != 1 || !(patientBundle.getEntryFirstRep().getResource() instanceof Patient))
446+
{
447+
logger.warn(
448+
"Error while retrieving patient for pseudonym {}, result bundle total not 1 or first entry not patient",
449+
pseudonym);
450+
throw new RuntimeException("Error while retrieving patient for pseudonym " + pseudonym);
451+
}
452+
453+
Patient patient = (Patient) patientBundle.getEntryFirstRep().getResource();
454+
455+
Bundle searchBundle = getSearchBundleWithPatientId(patient.getIdElement().getIdPart(), exportFrom, exportTo);
456+
457+
if (logger.isDebugEnabled())
458+
logger.debug("Executing Search-Bundle: {}",
459+
fhirContext.newJsonParser().encodeResourceToString(searchBundle));
460+
461+
Bundle resultBundle = clientFactory.getFhirStoreClient().transaction().withBundle(searchBundle)
462+
.withAdditionalHeader(Constants.HEADER_PREFER, "handling=strict").execute();
463+
464+
if (logger.isDebugEnabled())
465+
logger.debug("Search-Bundle result: {}", fhirContext.newJsonParser().encodeResourceToString(resultBundle));
466+
467+
return Stream.concat(Stream.of(patient),
468+
resultBundle.getEntry().stream().filter(e -> e.hasResource() && e.getResource() instanceof Bundle)
469+
.map(e -> (Bundle) e.getResource()).flatMap(this::getDomainResources));
470+
}
471+
381472
private Stream<DomainResource> getDomainResources(Bundle bundle)
382473
{
383474
Stream<DomainResource> domainResources = getDomainResourcesFromBundle(bundle);
@@ -392,7 +483,7 @@ private Stream<DomainResource> getDomainResources(Bundle bundle)
392483
private Stream<DomainResource> getDomainResourcesFromBundle(Bundle bundle)
393484
{
394485
return bundle.getEntry().stream().filter(e -> e.hasResource() && e.getResource() instanceof DomainResource)
395-
.map(e -> (Patient) e.getResource());
486+
.map(e -> (DomainResource) e.getResource());
396487
}
397488

398489
private Stream<DomainResource> doGetDomainResources(String nextUrl, int subTotal)
@@ -410,6 +501,7 @@ private Stream<DomainResource> doGetDomainResources(String nextUrl, int subTotal
410501
@Override
411502
public void storeBundle(Bundle bundle)
412503
{
413-
clientFactory.getFhirStoreClient().transaction().withBundle(bundle).execute();
504+
clientFactory.getFhirStoreClient().transaction().withBundle(bundle)
505+
.withAdditionalHeader(Constants.HEADER_PREFER, "handling=strict").execute();
414506
}
415507
}

codex-process-data-transfer/src/main/java/de/netzwerk_universitaetsmedizin/codex/processes/data_transfer/client/HapiFhirClientFactory.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public class HapiFhirClientFactory
4545
private final String basicAuthUsername;
4646
private final String basicAuthPassword;
4747
private final String bearerToken;
48+
private final boolean supportsIdentifierReferenceSearch;
4849

4950
/**
5051
* @param fhirContext
@@ -57,9 +58,10 @@ public class HapiFhirClientFactory
5758
* may be <code>null</code>
5859
* @param bearerToken
5960
* may be <code>null</code>
61+
* @param supportsIdentifierReferenceSearch
6062
*/
6163
public HapiFhirClientFactory(FhirContext fhirContext, String serverBase, String basicAuthUsername,
62-
String basicAuthPassword, String bearerToken)
64+
String basicAuthPassword, String bearerToken, boolean supportsIdentifierReferenceSearch)
6365
{
6466
if (fhirContext != null)
6567
this.fhirContext = fhirContext;
@@ -70,9 +72,15 @@ public HapiFhirClientFactory(FhirContext fhirContext, String serverBase, String
7072
this.basicAuthUsername = basicAuthUsername;
7173
this.basicAuthPassword = basicAuthPassword;
7274
this.bearerToken = bearerToken;
75+
this.supportsIdentifierReferenceSearch = supportsIdentifierReferenceSearch;
7376
}
7477

75-
private boolean isConfigured()
78+
public boolean supportsIdentifierReferenceSearch()
79+
{
80+
return supportsIdentifierReferenceSearch;
81+
}
82+
83+
protected boolean isConfigured()
7684
{
7785
return serverBase != null && !serverBase.isEmpty();
7886
}

0 commit comments

Comments
 (0)