Skip to content

Commit 02e6db7

Browse files
committed
Support Multiple Profiles in Redaction
1 parent e5b129e commit 02e6db7

26 files changed

+1064
-187
lines changed

src/main/java/de/medizininformatikinitiative/torch/management/StructureDefinitionHandler.java

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.io.File;
99
import java.io.IOException;
1010
import java.util.HashMap;
11+
import java.util.List;
1112
import java.util.Map;
1213
import java.util.Objects;
1314
import java.util.Optional;
@@ -63,30 +64,28 @@ public boolean known(String profile) {
6364
}
6465

6566
/**
66-
* Returns the StructureDefinition with the given URL.
67+
* Returns the CompiledStructureDefinition with the given URL.
6768
* Handles versioned URLs by splitting on the '|' character.
6869
*
6970
* @param url The URL of the StructureDefinition, possibly including a version
70-
* @return The StructureDefinition corresponding to the base URL (ignores version)
71+
* @return CompiledStructureDefinition corresponding to the base URL (ignores version)
7172
*/
7273
public Optional<CompiledStructureDefinition> getDefinition(String url) {
73-
return getDefinition(Set.of(url));
74+
return Optional.ofNullable(definitions.get(stripVersion(url)));
7475
}
7576

7677
/**
77-
* Returns the first found StructureDefinition from a list of URLs.
78-
* Iterates over the list of URLs, returning the first valid StructureDefinition.
78+
* Returns the all CompiledStructureDefinition from a list of URLs.
7979
* Removes version flags making the handling version agnostic.
8080
*
8181
* @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
82+
* @return all StructureDefinitions found, or empty if none are found
8383
*/
84-
public Optional<CompiledStructureDefinition> getDefinition(Set<String> urls) {
84+
public List<CompiledStructureDefinition> getDefinitions(Set<String> urls) {
8585
return urls.stream()
8686
.map(this::stripVersion)
8787
.map(definitions::get)
88-
.filter(Objects::nonNull)
89-
.findFirst();
88+
.filter(Objects::nonNull).toList();
9089
}
9190

9291
/**
@@ -110,4 +109,9 @@ private String stripVersion(String url) {
110109
int pipeIndex = url.indexOf('|');
111110
return pipeIndex == -1 ? url : url.substring(0, pipeIndex);
112111
}
112+
113+
@Override
114+
public String toString() {
115+
return definitions.keySet().toString();
116+
}
113117
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package de.medizininformatikinitiative.torch.model.management;
2+
3+
import de.medizininformatikinitiative.torch.util.CompiledStructureDefinition;
4+
import de.medizininformatikinitiative.torch.util.Slicing;
5+
import org.hl7.fhir.r4.model.Base;
6+
import org.hl7.fhir.r4.model.ElementDefinition;
7+
8+
import java.util.Optional;
9+
10+
import static java.util.Objects.requireNonNull;
11+
12+
public record ElementContext(String elementId, CompiledStructureDefinition definition) {
13+
14+
public ElementContext {
15+
requireNonNull(elementId);
16+
requireNonNull(definition);
17+
}
18+
19+
Optional<ElementDefinition> elementDefinition() {
20+
return definition.elementDefinitionById(elementId);
21+
}
22+
23+
/**
24+
* Creates a new ElementContext by descending to the child with {@code childName}.
25+
* <p>
26+
*
27+
* @param childName name of the field to which next should be descended.
28+
* @return ElementContext with elementIds updated by one step.
29+
*/
30+
ElementContext descend(String childName) {
31+
return new ElementContext(elementId + "." + childName, definition);
32+
}
33+
34+
/**
35+
* Checks if the element definition exists and has a slicing defined.
36+
*
37+
* @return true if element definition present and has slicing, false otherwise.
38+
*/
39+
public boolean hasSlicing() {
40+
return elementDefinition().map(ElementDefinition::hasSlicing).orElse(false);
41+
}
42+
43+
public Optional<ElementContext> matchingSlice(Base dataElement) {
44+
Optional<ElementDefinition> slice = Slicing.resolveSlicing(dataElement, elementId, definition);
45+
return slice.map(elementDefinition -> new ElementContext(elementDefinition.getId(), definition));
46+
}
47+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package de.medizininformatikinitiative.torch.model.management;
2+
3+
import de.medizininformatikinitiative.torch.util.CompiledStructureDefinition;
4+
import org.hl7.fhir.r4.model.Base;
5+
import org.hl7.fhir.r4.model.ElementDefinition;
6+
import org.hl7.fhir.r4.model.Extension;
7+
8+
import java.util.List;
9+
import java.util.Map;
10+
import java.util.Optional;
11+
import java.util.Set;
12+
import java.util.function.Predicate;
13+
import java.util.stream.Collectors;
14+
import java.util.stream.Stream;
15+
16+
public record MultiElementContext(List<ElementContext> contexts, Map<String, Set<String>> references) {
17+
18+
public MultiElementContext {
19+
contexts = List.copyOf(contexts);
20+
references = Map.copyOf(references);
21+
}
22+
23+
public MultiElementContext(String elementId, List<CompiledStructureDefinition> definitions, Map<String, Set<String>> references) {
24+
this(definitions.stream().map(compiledStructureDefinition -> new ElementContext(elementId, compiledStructureDefinition)).toList(), references);
25+
}
26+
27+
/**
28+
* Attempts to resolve slice contexts for the given data element.
29+
* <p>
30+
* If matching slices are found, they are passed to the provided {@code sliceConsumer}.
31+
* If the consumer returns {@code false}, a new {@code MultiElementContext} is returned
32+
* that includes the matching slices and the existing non-sliced contexts.
33+
* If the consumer returns {@code true}, no context is returned.
34+
*
35+
* @param dataElement the element to match against slice definitions
36+
* @param sliceConsumer a predicate to inspect or process matching slices; if it returns {@code true}, result is discarded
37+
* @return an {@code Optional} containing the updated {@code MultiElementContext}, or empty if suppressed by the consumer
38+
*/
39+
public Optional<MultiElementContext> resolveSlices(Base dataElement, Predicate<List<ElementContext>> sliceConsumer) {
40+
List<ElementContext> slices = contexts.stream().filter(ElementContext::hasSlicing)
41+
.flatMap(ctx -> ctx.matchingSlice(dataElement).stream())
42+
.toList();
43+
return sliceConsumer.test(slices) ? Optional.empty() : Optional.of(mergeWithSlices(slices));
44+
}
45+
46+
public boolean shouldRedactExtension(Extension extension) {
47+
return !isDataAbsentReason(extension) && !hasSlice(extension);
48+
}
49+
50+
/**
51+
* Checks if the given element is a sliced element.
52+
*
53+
* @param dataElement HAPI Base (Element) which should be checked
54+
* @return true if any slicing found, false otherwise
55+
*/
56+
public boolean hasSlice(Base dataElement) {
57+
return contexts.stream()
58+
.flatMap(context -> context.matchingSlice(dataElement).stream())
59+
.findAny()
60+
.isPresent();
61+
}
62+
63+
public Set<String> allowedReferences() {
64+
return contexts.stream()
65+
.map(ElementContext::elementId)
66+
.map(id -> references().getOrDefault(id, Set.of()))
67+
.flatMap(Set::stream)
68+
.collect(Collectors.toSet());
69+
}
70+
71+
private boolean isDataAbsentReason(Extension extension) {
72+
return "http://hl7.org/fhir/StructureDefinition/data-absent-reason".equals(extension.getUrl());
73+
}
74+
75+
public MultiElementContext mergeWithSlices(List<ElementContext> slices) {
76+
List<ElementContext> all = Stream.concat(
77+
slices.stream(),
78+
contexts.stream().filter(ctx -> !ctx.hasSlicing())
79+
).toList();
80+
return new MultiElementContext(all, references());
81+
}
82+
83+
/**
84+
* Returns a new {@code MultiElementContext} by descending into the specified child element
85+
* for each contained {@code ElementContext}.
86+
* <p>
87+
* This effectively advances all contexts one level deeper along the given {@code childName},
88+
* preserving any associated references.
89+
*
90+
* @param childName the name of the child element to descend into
91+
* @return a new {@code MultiElementContext} with updated {@code ElementContext}s
92+
*/
93+
public MultiElementContext descend(String childName) {
94+
return new MultiElementContext(contexts.stream().map(ctx -> ctx.descend(childName)).toList(), references);
95+
}
96+
97+
/**
98+
* Checks if any element definition has a slicing defined.
99+
*
100+
* @return true if slicing present and false otherwise.
101+
*/
102+
public boolean hasSlicing() {
103+
return elementDefinitions().anyMatch(ElementDefinition::hasSlicing);
104+
}
105+
106+
public Stream<ElementDefinition> elementDefinitions() {
107+
return contexts.stream().flatMap(elementContext -> elementContext.elementDefinition().stream());
108+
}
109+
110+
/**
111+
* Checks if any element definitions has minimum cardinality >0.
112+
*
113+
* @return true if one element definition is required for at least one associated profile other false.
114+
*/
115+
public boolean required() {
116+
return elementDefinitions().anyMatch(elementDefinition -> elementDefinition.getMin() > 0);
117+
}
118+
119+
public List<String> workingCodes() {
120+
return elementDefinitions()
121+
.flatMap(elementDefinition -> elementDefinition.getType().stream()
122+
.map(ElementDefinition.TypeRefComponent::getWorkingCode))
123+
.toList();
124+
}
125+
}

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.HashSet;
2121
import java.util.List;
2222
import java.util.Objects;
23+
import java.util.Optional;
2324
import java.util.Set;
2425
import java.util.function.Predicate;
2526

@@ -94,13 +95,13 @@ private AnnotatedAttributeGroup annotateGroup(AttributeGroup attributeGroup, Com
9495
List<AnnotatedAttribute> annotatedAttributes = new ArrayList<>();
9596

9697
for (Attribute attribute : attributeGroup.attributes()) {
97-
ElementDefinition elementDefinition = definition.elementDefinitionById(attribute.attributeRef());
98-
if (elementDefinition == null) {
98+
Optional<ElementDefinition> elementDefinition = definition.elementDefinitionById(attribute.attributeRef());
99+
if (elementDefinition.isEmpty()) {
99100
throw new ValidationException("Unknown Attribute " + attribute.attributeRef() + " in group " + attributeGroup.id());
100101
}
101102

102-
if (elementDefinition.hasType()) {
103-
if (!elementDefinition.getType("Reference").isEmpty() && elementDefinition.getType().size() == 1 && attribute.linkedGroups().isEmpty()) {
103+
if (elementDefinition.get().hasType()) {
104+
if (!elementDefinition.get().getType("Reference").isEmpty() && elementDefinition.get().getType().size() == 1 && attribute.linkedGroups().isEmpty()) {
104105
throw new ValidationException("Reference Attribute " + attribute.attributeRef() + " without linked Groups in group " + attributeGroup.id());
105106
}
106107
} else {

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import de.medizininformatikinitiative.torch.model.crtdl.annotated.AnnotatedAttribute;
88
import de.medizininformatikinitiative.torch.model.crtdl.annotated.AnnotatedAttributeGroup;
99
import de.medizininformatikinitiative.torch.util.CompiledStructureDefinition;
10-
import org.hl7.fhir.r4.model.ElementDefinition;
1110

1211
import java.util.ArrayList;
1312
import java.util.List;
@@ -51,8 +50,7 @@ public AnnotatedAttributeGroup generate(AttributeGroup attributeGroup, String pa
5150
if (compartmentManager.isInCompartment(resourceType)) {
5251
for (String field : patientRefFields) {
5352
String fieldString = resourceType + "." + field;
54-
ElementDefinition elementDefinition = definition.elementDefinitionById(fieldString);
55-
if (elementDefinition != null) {
53+
if (definition.elementDefinitionById(fieldString).isPresent()) {
5654
tempAttributes.add(new AnnotatedAttribute(fieldString, fieldString, fieldString, false, List.of(patientGroupId)));
5755
}
5856
}

src/main/java/de/medizininformatikinitiative/torch/util/CompiledStructureDefinition.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import java.util.Map;
88
import java.util.Objects;
9+
import java.util.Optional;
910
import java.util.stream.Collectors;
1011
import java.util.stream.Stream;
1112

@@ -30,8 +31,8 @@ public static CompiledStructureDefinition fromStructureDefinition(StructureDefin
3031
return new CompiledStructureDefinition(structureDefinition, structureDefinition.getSnapshot().getElement().stream().collect(Collectors.toMap(ElementDefinition::getId, Functions.identity())));
3132
}
3233

33-
public ElementDefinition elementDefinitionById(String id) {
34-
return elementDefinitions.get(requireNonNull(id));
34+
public Optional<ElementDefinition> elementDefinitionById(String id) {
35+
return Optional.ofNullable(elementDefinitions.get(requireNonNull(id)));
3536
}
3637

3738
Stream<ElementDefinition> elementDefinitionByPath(String path) {

0 commit comments

Comments
 (0)