Skip to content

Commit b2dfa3a

Browse files
committed
Move StructureDefinition Mapping To Start
1 parent 04d4b62 commit b2dfa3a

32 files changed

+384
-403
lines changed

src/main/java/de/medizininformatikinitiative/torch/config/AppConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ public FhirContext fhirContext() {
365365

366366
@Bean
367367
public StructureDefinitionHandler cdsStructureDefinitionHandler(ResourceReader resourceReader) {
368-
return new StructureDefinitionHandler(torchProperties.profile().dir(), resourceReader);
368+
return new StructureDefinitionHandler(new File(torchProperties.profile().dir()), resourceReader);
369369
}
370370

371371
@Bean
Lines changed: 65 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package de.medizininformatikinitiative.torch.management;
22

3+
import de.medizininformatikinitiative.torch.util.CompiledStructureDefinition;
34
import de.medizininformatikinitiative.torch.util.ResourceReader;
5+
import jakarta.annotation.PostConstruct;
46
import org.hl7.fhir.r4.model.StructureDefinition;
5-
import org.springframework.stereotype.Component;
67

78
import java.io.File;
89
import java.io.IOException;
@@ -12,92 +13,101 @@
1213
import java.util.Optional;
1314
import java.util.Set;
1415

16+
import static java.util.Objects.requireNonNull;
17+
1518
/**
16-
* Handler for loading and serving structure definitions.
19+
* Handler for loading and serving FHIR structure definitions.
1720
*/
18-
@Component
1921
public class StructureDefinitionHandler {
2022

21-
private final Map<String, StructureDefinition> definitions = new HashMap<>();
23+
private final Map<String, CompiledStructureDefinition> definitions = new HashMap<>();
2224
private final ResourceReader resourceReader;
25+
private final File directory;
2326

24-
public StructureDefinitionHandler(String fileDirectory, ResourceReader resourceReader) {
25-
try {
26-
this.resourceReader = resourceReader;
27-
processDirectory(fileDirectory);
28-
} catch (IOException e) {
29-
throw new RuntimeException(e);
30-
}
27+
/**
28+
* Creates a new StructureDefinitionHandler.
29+
*
30+
* @param directory the directory containing JSON structure definition files
31+
* @param resourceReader utility for reading and parsing FHIR resources
32+
*/
33+
public StructureDefinitionHandler(File directory, ResourceReader resourceReader) {
34+
this.resourceReader = requireNonNull(resourceReader);
35+
this.directory = requireNonNull(directory);
3136
}
3237

3338
/**
34-
* @param profile to check if known
35-
* @return returns if profile is known.
39+
* Reads all JSON files in a directory and stores their StructureDefinitions.
40+
*
41+
* @throws IOException if any file cannot be read or parsed
3642
*/
37-
public Boolean known(String profile) {
38-
return definitions.containsKey(profile);
43+
@PostConstruct
44+
public void processDirectory() throws IOException {
45+
if (directory.isDirectory()) {
46+
File[] files = directory.listFiles((dir, name) -> name.toLowerCase().endsWith(".json"));
47+
if (files != null) {
48+
for (File file : files) {
49+
readStructureDefinition(file.getAbsolutePath());
50+
}
51+
}
52+
}
3953
}
4054

4155
/**
42-
* Reads a StructureDefinition from a file and stores it in the definitionsMap
56+
* Checks if a profile URL is known to this handler.
57+
*
58+
* @param profile the profile URL to check
59+
* @return true if the profile is known, false otherwise
4360
*/
44-
public void readStructureDefinition(String filePath) throws IOException {
45-
StructureDefinition structureDefinition = (StructureDefinition) resourceReader.readResource(filePath);
46-
definitions.put(structureDefinition.getUrl(), structureDefinition);
61+
public boolean known(String profile) {
62+
return definitions.containsKey(stripVersion(profile));
4763
}
4864

4965
/**
5066
* Returns the StructureDefinition with the given URL.
5167
* Handles versioned URLs by splitting on the '|' character.
5268
*
53-
* @param url The URL of the StructureDefinition, possibly including a version.
54-
* @return The StructureDefinition corresponding to the base URL (ignores version).
69+
* @param url The URL of the StructureDefinition, possibly including a version
70+
* @return The StructureDefinition corresponding to the base URL (ignores version)
5571
*/
56-
public StructureDefinition getDefinition(String url) {
57-
String[] versionSplit = url.split("\\|");
58-
return definitions.get(versionSplit[0]);
72+
public Optional<CompiledStructureDefinition> getDefinition(String url) {
73+
return getDefinition(Set.of(url));
5974
}
6075

6176
/**
62-
* Returns the first non-null StructureDefinition from a list of URLs.
63-
* <p>
77+
* Returns the first found StructureDefinition from a list of URLs.
6478
* Iterates over the list of URLs, returning the first valid StructureDefinition.
79+
* Removes version flags making the handling version agnostic.
6580
*
66-
* @param urls a list of URLs for which to find the corresponding StructureDefinition.
67-
* @return The first non-null StructureDefinition found, or empty if none are found.
81+
* @param urls a list of URLs for which to find the corresponding StructureDefinition
82+
* @return the first StructureDefinition found, or empty if none are found
6883
*/
69-
public Optional<StructureDefinition> getDefinition(Set<String> urls) {
70-
return urls.stream().map(definitions::get).filter(Objects::nonNull).findFirst();
84+
public Optional<CompiledStructureDefinition> getDefinition(Set<String> urls) {
85+
return urls.stream()
86+
.map(this::stripVersion)
87+
.map(definitions::get)
88+
.filter(Objects::nonNull)
89+
.findFirst();
7190
}
7291

73-
public StructureDefinition.StructureDefinitionSnapshotComponent getSnapshot(String url) {
74-
if (definitions.get(url) != null) {
75-
return (definitions.get(url)).getSnapshot();
76-
} else {
77-
throw new IllegalArgumentException("Unknown Profile: " + url);
78-
}
79-
}
80-
81-
public String getResourceType(String url) {
82-
if (definitions.get(url) != null) {
83-
return (definitions.get(url)).getType();
84-
} else {
85-
throw new IllegalArgumentException("Unknown Profile: " + url);
86-
}
92+
/**
93+
* Reads a StructureDefinition from a file and stores it in the definitions map.
94+
*
95+
* @param filePath the absolute path to the JSON file
96+
* @throws IOException if the file cannot be read or parsed
97+
*/
98+
private void readStructureDefinition(String filePath) throws IOException {
99+
StructureDefinition structureDefinition = (StructureDefinition) resourceReader.readResource(filePath);
100+
definitions.put(structureDefinition.getUrl(), CompiledStructureDefinition.fromStructureDefinition(structureDefinition));
87101
}
88102

89103
/**
90-
* Reads all JSON files in a directory and stores their StructureDefinitions in the definitionsMap
104+
* Strips version information from a FHIR canonical URL.
105+
*
106+
* @param url the potentially versioned URL
107+
* @return the URL with version information removed
91108
*/
92-
private void processDirectory(String directoryPath) throws IOException {
93-
File directory = new File(directoryPath);
94-
if (directory.isDirectory()) {
95-
File[] files = directory.listFiles((dir, name) -> name.toLowerCase().endsWith(".json"));
96-
if (files != null) {
97-
for (File file : files) {
98-
readStructureDefinition(file.getAbsolutePath());
99-
}
100-
}
101-
}
109+
private String stripVersion(String url) {
110+
int pipeIndex = url.indexOf('|');
111+
return pipeIndex == -1 ? url : url.substring(0, pipeIndex);
102112
}
103113
}

src/main/java/de/medizininformatikinitiative/torch/model/crtdl/Attribute.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ public record Attribute(@JsonProperty(required = true) String attributeRef,
1414

1515
public Attribute {
1616
requireNonNull(attributeRef);
17+
if (attributeRef.isEmpty()) {
18+
throw new IllegalArgumentException("attributeRef must not be empty");
19+
}
1720
linkedGroups = linkedGroups == null ? List.of() : List.copyOf(linkedGroups);
1821
}
1922

src/main/java/de/medizininformatikinitiative/torch/model/crtdl/annotated/AnnotatedAttributeGroup.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
public record AnnotatedAttributeGroup(
1919
String name,
2020
String id,
21-
String groupReference,
21+
String resourceType, String groupReference,
2222
List<AnnotatedAttribute> attributes,
2323
List<Filter> filter,
2424
Predicate<Resource> compiledFilter,
@@ -27,12 +27,12 @@ public record AnnotatedAttributeGroup(
2727

2828
public static final String PATIENT = "Patient";
2929

30-
public AnnotatedAttributeGroup(String id,
30+
public AnnotatedAttributeGroup(String id, String resourceType,
3131
String groupReference,
3232
List<AnnotatedAttribute> attributes,
3333
List<Filter> filter,
3434
Predicate<Resource> compiledFilter) {
35-
this("", id, groupReference, attributes, filter, compiledFilter, false); // Default value for includeReferenceOnly
35+
this("", id, resourceType, groupReference, attributes, filter, compiledFilter, false); // Default value for includeReferenceOnly
3636
}
3737

3838

@@ -89,11 +89,11 @@ public AnnotatedAttributeGroup addAttributes(List<AnnotatedAttribute> newAttribu
8989

9090
List<AnnotatedAttribute> tempAttributes = new ArrayList<>(attributes);
9191
tempAttributes.addAll(newAttributes);
92-
return new AnnotatedAttributeGroup(name, id, groupReference, tempAttributes, filter, compiledFilter, includeReferenceOnly);
92+
return new AnnotatedAttributeGroup(name, id, resourceType, groupReference, tempAttributes, filter, compiledFilter, includeReferenceOnly);
9393
}
9494

9595
public AnnotatedAttributeGroup setCompiledFilter(Predicate<Resource> compiledFilter) {
96-
return new AnnotatedAttributeGroup(name, id, groupReference, attributes, filter, compiledFilter, includeReferenceOnly);
96+
return new AnnotatedAttributeGroup(name, id, resourceType, groupReference, attributes, filter, compiledFilter, includeReferenceOnly);
9797
}
9898

9999
public String resourceType() {

src/main/java/de/medizininformatikinitiative/torch/service/CrtdlValidatorService.java

Lines changed: 27 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,18 @@
99
import de.medizininformatikinitiative.torch.model.crtdl.annotated.AnnotatedAttributeGroup;
1010
import de.medizininformatikinitiative.torch.model.crtdl.annotated.AnnotatedCrtdl;
1111
import de.medizininformatikinitiative.torch.model.crtdl.annotated.AnnotatedDataExtraction;
12+
import de.medizininformatikinitiative.torch.util.CompiledStructureDefinition;
1213
import de.medizininformatikinitiative.torch.util.FhirPathBuilder;
1314
import org.hl7.fhir.r4.model.ElementDefinition;
1415
import org.hl7.fhir.r4.model.Resource;
15-
import org.hl7.fhir.r4.model.StructureDefinition;
1616
import org.slf4j.Logger;
1717
import org.slf4j.LoggerFactory;
1818

19-
import java.io.IOException;
20-
import java.util.*;
19+
import java.util.ArrayList;
20+
import java.util.HashSet;
21+
import java.util.List;
22+
import java.util.Objects;
23+
import java.util.Set;
2124
import java.util.function.Predicate;
2225

2326
public class CrtdlValidatorService {
@@ -29,7 +32,7 @@ public class CrtdlValidatorService {
2932
private final FilterService filterService;
3033

3134

32-
public CrtdlValidatorService(StructureDefinitionHandler profileHandler, StandardAttributeGenerator attributeGenerator, FilterService filterService) throws IOException {
35+
public CrtdlValidatorService(StructureDefinitionHandler profileHandler, StandardAttributeGenerator attributeGenerator, FilterService filterService) {
3336
this.profileHandler = profileHandler;
3437
this.attributeGenerator = attributeGenerator;
3538
this.filterService = filterService;
@@ -49,17 +52,15 @@ public AnnotatedCrtdl validate(Crtdl crtdl) throws ValidationException {
4952
String patientAttributeGroupId = "";
5053

5154
for (AttributeGroup attributeGroup : crtdl.dataExtraction().attributeGroups()) {
52-
StructureDefinition definition = profileHandler.getDefinition(attributeGroup.groupReference());
53-
if (definition != null) {
54-
if (Objects.equals(definition.getType(), "Patient")) {
55-
if (exactlyOnePatientGroup) {
56-
throw new ValidationException(" More than one Patient Attribute Group");
57-
} else {
58-
exactlyOnePatientGroup = true;
59-
patientAttributeGroupId = attributeGroup.id();
60-
logger.debug("Found Patient Attribute Group {}", patientAttributeGroupId);
61-
}
62-
55+
CompiledStructureDefinition definition = profileHandler.getDefinition(attributeGroup.groupReference())
56+
.orElseThrow(() -> new ValidationException("Unknown Profile: " + attributeGroup.groupReference()));
57+
if (Objects.equals(definition.type(), "Patient")) {
58+
if (exactlyOnePatientGroup) {
59+
throw new ValidationException(" More than one Patient Attribute Group");
60+
} else {
61+
exactlyOnePatientGroup = true;
62+
patientAttributeGroupId = attributeGroup.id();
63+
logger.debug("Found Patient Attribute Group {}", patientAttributeGroupId);
6364
}
6465
}
6566
}
@@ -68,19 +69,15 @@ public AnnotatedCrtdl validate(Crtdl crtdl) throws ValidationException {
6869
}
6970

7071
for (AttributeGroup attributeGroup : crtdl.dataExtraction().attributeGroups()) {
71-
StructureDefinition definition = profileHandler.getDefinition(attributeGroup.groupReference());
72-
if (definition != null) {
73-
74-
for (Attribute attribute : attributeGroup.attributes()) {
75-
linkedGroups.addAll(attribute.linkedGroups());
76-
}
77-
annotatedAttributeGroups.add(annotateGroup(attributeGroup, definition, patientAttributeGroupId));
78-
successfullyAnnotatedGroups.add(attributeGroup.id());
72+
CompiledStructureDefinition definition = profileHandler.getDefinition(attributeGroup.groupReference())
73+
.orElseThrow(() -> new ValidationException("Unknown Profile: " + attributeGroup.groupReference()));
74+
for (Attribute attribute : attributeGroup.attributes()) {
75+
linkedGroups.addAll(attribute.linkedGroups());
76+
}
77+
annotatedAttributeGroups.add(annotateGroup(attributeGroup, definition, patientAttributeGroupId));
78+
successfullyAnnotatedGroups.add(attributeGroup.id());
7979

8080

81-
} else {
82-
throw new ValidationException("Unknown Profile: " + attributeGroup.groupReference());
83-
}
8481
}
8582

8683
linkedGroups.removeAll(successfullyAnnotatedGroups);
@@ -92,12 +89,12 @@ public AnnotatedCrtdl validate(Crtdl crtdl) throws ValidationException {
9289
return new AnnotatedCrtdl(crtdl.cohortDefinition(), new AnnotatedDataExtraction(annotatedAttributeGroups));
9390
}
9491

95-
private AnnotatedAttributeGroup annotateGroup(AttributeGroup attributeGroup, StructureDefinition
92+
private AnnotatedAttributeGroup annotateGroup(AttributeGroup attributeGroup, CompiledStructureDefinition
9693
definition, String patientGroupId) throws ValidationException {
9794
List<AnnotatedAttribute> annotatedAttributes = new ArrayList<>();
9895

9996
for (Attribute attribute : attributeGroup.attributes()) {
100-
ElementDefinition elementDefinition = definition.getSnapshot().getElementById(attribute.attributeRef());
97+
ElementDefinition elementDefinition = definition.elementDefinitionById(attribute.attributeRef());
10198
if (elementDefinition == null) {
10299
throw new ValidationException("Unknown Attribute " + attribute.attributeRef() + " in group " + attributeGroup.id());
103100
}
@@ -110,7 +107,7 @@ private AnnotatedAttributeGroup annotateGroup(AttributeGroup attributeGroup, Str
110107
throw new ValidationException("Typeless Attribute " + attribute.attributeRef() + " in group " + attributeGroup.id());
111108
}
112109

113-
String[] fhirTerser = FhirPathBuilder.handleSlicingForFhirPath(attribute.attributeRef(), definition.getSnapshot());
110+
String[] fhirTerser = FhirPathBuilder.handleSlicingForFhirPath(attribute.attributeRef(), definition);
114111
annotatedAttributes.add(new AnnotatedAttribute(attribute.attributeRef(), fhirTerser[0], fhirTerser[1], attribute.mustHave(), attribute.linkedGroups()));
115112
}
116113

@@ -120,7 +117,7 @@ private AnnotatedAttributeGroup annotateGroup(AttributeGroup attributeGroup, Str
120117

121118
if (!attributeGroup.filter().isEmpty()) {
122119
try {
123-
Predicate<Resource> filter = filterService.compileFilter(attributeGroup.filter(), definition.getType());
120+
Predicate<Resource> filter = filterService.compileFilter(attributeGroup.filter(), definition.type());
124121
return group.setCompiledFilter(filter);
125122
} catch (Exception e) {
126123
throw new RuntimeException(e);

src/main/java/de/medizininformatikinitiative/torch/service/DirectResourceLoader.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ private Mono<PatientBatchWithConsent> processBatchWithConsent(List<AnnotatedAttr
7878
}
7979

8080
private Flux<Query> groupQueries(AnnotatedAttributeGroup group) {
81-
return Flux.fromIterable(group.queries(dseMappingTreeBase, structureDefinitionsHandler.getResourceType(group.groupReference())));
81+
return Flux.fromIterable(group.queries(dseMappingTreeBase, group.resourceType()));
8282
}
8383

8484
Flux<DomainResource> executeQueryWithBatch(PatientBatch batch, Query query) {
@@ -181,6 +181,7 @@ private Mono<PatientBatchWithConsent> processPatientSingleAttributeGroup(Annotat
181181
if (profileMustHaveChecker.fulfilled(tuple.resource, group)) {
182182

183183
safeGroup.add(tuple.patientId);
184+
System.out.println("Ref" + group.groupReference() + "Type" + group.resourceType() + " ID " + group.id());
184185
bundle.put(tuple.resource, group.id(), true);
185186
} else {
186187
bundle.put(tuple.resource, group.id(), false);

0 commit comments

Comments
 (0)