Skip to content

Commit dd9e932

Browse files
committed
Redact Primitive Extensions
1 parent 629b64a commit dd9e932

File tree

8 files changed

+389
-128
lines changed

8 files changed

+389
-128
lines changed

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

Lines changed: 116 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import java.util.List;
1919
import java.util.Map;
2020
import java.util.NoSuchElementException;
21-
import java.util.Objects;
2221
import java.util.Optional;
2322
import java.util.Set;
2423
import java.util.stream.Collectors;
@@ -32,7 +31,10 @@
3231
public class Redaction {
3332

3433
private static final Logger logger = LoggerFactory.getLogger(Redaction.class);
35-
private static final Extension ABSENT_REASON_EXTENSION = createAbsentReasonExtension("masked");
34+
private static final String MASKED = "masked";
35+
private static final Extension ABSENT_REASON_EXTENSION = createAbsentReasonExtension(MASKED);
36+
private static final String EXTENSION = "extension";
37+
private static final String REFERENCE = "reference";
3638

3739
private final StructureDefinitionHandler structureDefinitionHandler;
3840

@@ -48,11 +50,11 @@ public Redaction(StructureDefinitionHandler structureDefinitionHandler) {
4850
private static void handleReference(Property child, Set<String> references) {
4951
child.getValues().forEach(referenceValue -> {
5052
if (((Reference) referenceValue).hasReference() && !references.contains(((Reference) referenceValue).getReference())) {
51-
referenceValue.setProperty("reference", HapiFactory.create("string").addExtension(ABSENT_REASON_EXTENSION));
53+
referenceValue.setProperty(REFERENCE, HapiFactory.create("string").addExtension(ABSENT_REASON_EXTENSION));
5254
}
5355
referenceValue.children().forEach(childValue -> {
5456
String name = childValue.getName();
55-
if (!"reference".equals(name) && !"extension".equals(name) && childValue.hasValues()) {
57+
if (!REFERENCE.equals(name) && !EXTENSION.equals(name) && childValue.hasValues()) {
5658
childValue.getValues().forEach(value -> referenceValue.removeChild(name, value));
5759
}
5860
});
@@ -74,29 +76,23 @@ public Base redact(ExtractionRedactionWrapper wrapper) {
7476
if (!resource.getResourceType().toString().equals("Patient")) {
7577
// Convert resource profiles to a list of strings
7678
resourceProfiles = meta.getProfile().stream().filter(profile -> wrapper.profiles().stream().anyMatch(wrapperProfile -> profile.toString().contains(wrapperProfile))).toList();
77-
78-
7979
List<CanonicalType> finalResourceProfiles = resourceProfiles;
8080
Set<String> validProfiles = wrapper.profiles().stream().filter(profile -> finalResourceProfiles.stream().anyMatch(resourceProfile -> resourceProfile.toString().contains(profile))).collect(Collectors.toSet());
8181

8282
if (!validProfiles.equals(wrapper.profiles())) {
8383
logger.error("Missing Profiles in Resource {} {}: {} for requested profiles {}", resource.getResourceType(), resource.getId(), resourceProfiles, wrapper.profiles());
8484
throw new RuntimeException("Resource is missing required profiles: " + resourceProfiles);
8585
}
86-
8786
} else {
8887
resourceProfiles = wrapper.profiles().stream().map(CanonicalType::new).toList();
8988
}
90-
9189
Optional<CompiledStructureDefinition> definition = structureDefinitionHandler.getDefinition(wrapper.profiles());
9290
if (definition.isEmpty()) {
9391
logger.error("Unknown Profile in Resource {} {}", resource.getResourceType(), resource.getId());
9492
throw new RuntimeException("Trying to handle unknown profiles: " + wrapper.profiles());
9593
}
96-
9794
meta.setProfile(resourceProfiles);
98-
99-
return this.redact(resource, String.valueOf(resource.getResourceType()), 0, definition.get(), references);
95+
return this.redact(resource, String.valueOf(resource.getResourceType()), definition.get(), references);
10096
}
10197
throw new RuntimeException("Trying to redact Resource without Meta");
10298
}
@@ -105,90 +101,139 @@ public Base redact(ExtractionRedactionWrapper wrapper) {
105101
* Executes redaction operation on the given base element recursively.
106102
*
107103
* @param base Base to be redacted (e.g. a Resource or an Element)
108-
* @param elementId Element IDs of parent currently handled; initially the resource type"
109-
* @param recursion recursion depth (for debug purposes)
104+
* @param elementId Element IDs of parent currently handled; initially the resource type
110105
* @param definition Structure definition of the Resource.
111106
* @param references Allowed references
112107
* @return redacted Base
113108
*/
114-
private Base redact(Base base, String elementId, int recursion, CompiledStructureDefinition definition, Map<String, Set<String>> references) {
115-
ElementDefinition elementDefinition = definition.elementDefinitionById(elementId);
116-
if (elementDefinition == null) {
117-
throw new NoSuchElementException("Definition unknown for " + base.fhirType() + " in Element ID " + elementId + " in StructureDefinition " + definition.structureDefinition().getUrl());
118-
}
109+
private Base redact(Base base, String elementId, CompiledStructureDefinition definition, Map<String, Set<String>> references) {
110+
ElementDefinition elementDefinition = getElementDefinition(definition, elementId, base);
119111

120-
String finalElementId;
121-
if (elementDefinition.hasSlicing()) {
122-
ElementDefinition slicedElementDefinition = Slicing.checkSlicing(base, elementId, definition);
112+
redactUnknownExtensions(base, elementId, definition);
123113

124-
/* Slicing could not be resolved, but all elements should be sliced */
125-
if (slicedElementDefinition == null) {
126-
base.children().forEach(child -> child.getValues().forEach(value -> base.removeChild(child.getName(), value)));
114+
if (base.isPrimitive()) {
115+
return base;
116+
}
117+
// Handle slicing and early exit when unknown slice
118+
if (!(base instanceof Extension) && elementDefinition.hasSlicing()) {
119+
ElementDefinition sliced = Slicing.checkSlicing(base, elementId, definition);
120+
if (sliced == null) {
121+
removeAllChildren(base);
127122
if (elementDefinition.getMin() > 0) {
128-
base.setProperty("extension", ABSENT_REASON_EXTENSION);
123+
base.setProperty(EXTENSION, createAbsentReasonExtension(MASKED));
129124
}
130-
131125
return base;
132126
}
133-
finalElementId = slicedElementDefinition.getId();
134-
} else {
135-
finalElementId = elementDefinition.getId();
127+
elementId = sliced.getId();
136128
}
137129

138-
base.children().forEach(child -> {
139-
String childId = finalElementId + "." + child.getName();
130+
redactChildren(base, elementId, definition, references);
140131

141-
ElementDefinition childDefinition = definition.elementDefinitionById(childId);
142-
String type;
143-
int min;
132+
return base;
133+
}
144134

145-
if (childDefinition == null) {
146-
type = child.getTypeCode();
147-
min = child.getMinCardinality();
148-
} else {
149-
type = childDefinition.getType().stream()
150-
.map(ElementDefinition.TypeRefComponent::getWorkingCode)
151-
.findFirst().orElse(child.getTypeCode());
152-
min = childDefinition.getMin();
153-
}
135+
private ElementDefinition getElementDefinition(CompiledStructureDefinition definition, String elementId, Base base) {
136+
ElementDefinition elementDefinition = definition.elementDefinitionById(elementId);
137+
if (elementDefinition == null) {
138+
throw new NoSuchElementException("Definition unknown for " + base.fhirType() + " in Element ID " + elementId + " in StructureDefinition " + definition.structureDefinition().getUrl());
139+
}
140+
return elementDefinition;
141+
}
142+
143+
/**
144+
* Checks for extension legality via slicing and explicit test for data-absent-reason system.
145+
*
146+
* @param base to be handled
147+
* @param elementId of base to be handled
148+
* @param definition applied to the base
149+
*/
150+
private void redactUnknownExtensions(Base base, String elementId, CompiledStructureDefinition definition) {
151+
getExtensions(base).stream().filter(extension -> shouldRedactExtension(extension, elementId, definition)).forEach(extension -> base.removeChild(EXTENSION, extension));
152+
}
153+
154+
private List<Extension> getExtensions(Base base) {
155+
return switch (base) {
156+
case Element element when element.hasExtension() -> List.copyOf(element.getExtension());
157+
case DomainResource domainResource when domainResource.hasExtension() ->
158+
List.copyOf(domainResource.getExtension());
159+
default -> List.of();
160+
};
161+
}
154162

155-
if (child.hasValues() && childDefinition != null) {
163+
private boolean shouldRedactExtension(Extension extension, String elementId, CompiledStructureDefinition definition) {
164+
return !"http://hl7.org/fhir/StructureDefinition/data-absent-reason".equals(extension.getUrl()) && !isKnownExtension(extension, elementId, definition);
165+
}
166+
167+
private boolean isKnownExtension(Extension extension, String elementId, CompiledStructureDefinition definition) {
168+
ElementDefinition sliced = Slicing.checkSlicing(extension, elementId + ".extension", definition);
169+
return sliced != null;
170+
}
171+
172+
private void removeAllChildren(Base base) {
173+
base.children().forEach(child -> child.getValues().forEach(value -> base.removeChild(child.getName(), value)));
174+
}
175+
176+
/**
177+
* @param base base whose children should be redacted
178+
* @param baseId ElementId of the base
179+
* @param definition StructureDefinition to be applied
180+
* @param references Allowed references
181+
*/
182+
private void redactChildren(Base base, String baseId, CompiledStructureDefinition definition, Map<String, Set<String>> references) {
183+
base.children().forEach(child -> {
184+
String childId = baseId + "." + child.getName();
185+
ElementDefinition childDef = definition.elementDefinitionById(childId);
186+
187+
String type = getChildType(child, childDef);
188+
int min = (childDef != null) ? childDef.getMin() : child.getMinCardinality();
189+
190+
if (child.hasValues() && childDef != null) {
156191
if ("Reference".equals(type)) {
157-
Set<String> found = references.get(childId);
158-
handleReference(child, found == null ? Set.of() : found);
192+
Set<String> allowedRefs = references.getOrDefault(childId, Set.of());
193+
handleReference(child, allowedRefs);
159194
}
160195

161-
child.getValues().forEach(value -> {
196+
for (Base value : child.getValues()) {
162197
if (value.isEmpty() && min > 0) {
163-
Element element = HapiFactory.create(type).addExtension(ABSENT_REASON_EXTENSION);
164-
base.setProperty(child.getName(), element);
165-
} else if (!value.isPrimitive()) {
166-
redact(value, childId, recursion + 1, definition, references);
167-
}
168-
});
169-
170-
} else {
171-
if (min > 0 && !Objects.equals(child.getTypeCode(), "Extension")) {
172-
/*
173-
TODO find a good way to only work with reflection here?
174-
Potential issue is primitive types don't have addExtension, somehow generic setField results in dem being set.
175-
E.g. RecordedDate in Condition
176-
*/
177-
if (Objects.equals(type, "BackboneElement")) {
178-
String fieldName = child.getName();
179-
ResourceUtils.setField(base, fieldName, ABSENT_REASON_EXTENSION);
198+
Element absent = HapiFactory.create(type).addExtension(createAbsentReasonExtension(MASKED));
199+
base.setProperty(child.getName(), absent);
180200
} else {
181-
try {
182-
Element element = HapiFactory.create(type).addExtension(ABSENT_REASON_EXTENSION);
183-
base.setProperty(child.getName(), element);
184-
} catch (FHIRException e) {
185-
logger.warn("Unresolvable elementID {} in field {} Standard Type {} with cardinality {} ", finalElementId, child.getName(), type, min);
186-
}
201+
redact(value, childId, definition, references);
187202
}
188203
}
204+
} else {
205+
if (min > 0 && !"Extension".equals(child.getTypeCode())) {
206+
addDataAbsentReason(base, child, type, baseId);
207+
}
208+
189209
}
190210
});
211+
}
191212

192-
return base;
213+
private String getChildType(Property child, ElementDefinition def) {
214+
if (def == null) return child.getTypeCode();
215+
return def.getType().stream().map(ElementDefinition.TypeRefComponent::getWorkingCode).findFirst().orElse(child.getTypeCode());
216+
}
217+
218+
/**
219+
* Adds a DataAbsentReason for a child property of a base.
220+
*
221+
* @param base the parent of the child
222+
* @param child property without values to be checked
223+
* @param type type of the child to be handled
224+
* @param parentId elementId of the parent
225+
*/
226+
private void addDataAbsentReason(Base base, Property child, String type, String parentId) {
227+
try {
228+
if ("BackboneElement".equals(type)) {
229+
ResourceUtils.setField(base, child.getName(), createAbsentReasonExtension(MASKED));
230+
} else {
231+
Element element = HapiFactory.create(type).addExtension(createAbsentReasonExtension(MASKED));
232+
base.setProperty(child.getName(), element);
233+
}
234+
} catch (FHIRException e) {
235+
logger.warn("Unresolvable elementID {} in field {} Type {} ", parentId, child.getName(), type);
236+
}
193237
}
238+
194239
}

0 commit comments

Comments
 (0)