18
18
import java .util .List ;
19
19
import java .util .Map ;
20
20
import java .util .NoSuchElementException ;
21
- import java .util .Objects ;
22
21
import java .util .Optional ;
23
22
import java .util .Set ;
24
23
import java .util .stream .Collectors ;
32
31
public class Redaction {
33
32
34
33
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" ;
36
38
37
39
private final StructureDefinitionHandler structureDefinitionHandler ;
38
40
@@ -48,11 +50,11 @@ public Redaction(StructureDefinitionHandler structureDefinitionHandler) {
48
50
private static void handleReference (Property child , Set <String > references ) {
49
51
child .getValues ().forEach (referenceValue -> {
50
52
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 ));
52
54
}
53
55
referenceValue .children ().forEach (childValue -> {
54
56
String name = childValue .getName ();
55
- if (!"reference" .equals (name ) && !"extension" .equals (name ) && childValue .hasValues ()) {
57
+ if (!REFERENCE .equals (name ) && !EXTENSION .equals (name ) && childValue .hasValues ()) {
56
58
childValue .getValues ().forEach (value -> referenceValue .removeChild (name , value ));
57
59
}
58
60
});
@@ -74,29 +76,23 @@ public Base redact(ExtractionRedactionWrapper wrapper) {
74
76
if (!resource .getResourceType ().toString ().equals ("Patient" )) {
75
77
// Convert resource profiles to a list of strings
76
78
resourceProfiles = meta .getProfile ().stream ().filter (profile -> wrapper .profiles ().stream ().anyMatch (wrapperProfile -> profile .toString ().contains (wrapperProfile ))).toList ();
77
-
78
-
79
79
List <CanonicalType > finalResourceProfiles = resourceProfiles ;
80
80
Set <String > validProfiles = wrapper .profiles ().stream ().filter (profile -> finalResourceProfiles .stream ().anyMatch (resourceProfile -> resourceProfile .toString ().contains (profile ))).collect (Collectors .toSet ());
81
81
82
82
if (!validProfiles .equals (wrapper .profiles ())) {
83
83
logger .error ("Missing Profiles in Resource {} {}: {} for requested profiles {}" , resource .getResourceType (), resource .getId (), resourceProfiles , wrapper .profiles ());
84
84
throw new RuntimeException ("Resource is missing required profiles: " + resourceProfiles );
85
85
}
86
-
87
86
} else {
88
87
resourceProfiles = wrapper .profiles ().stream ().map (CanonicalType ::new ).toList ();
89
88
}
90
-
91
89
Optional <CompiledStructureDefinition > definition = structureDefinitionHandler .getDefinition (wrapper .profiles ());
92
90
if (definition .isEmpty ()) {
93
91
logger .error ("Unknown Profile in Resource {} {}" , resource .getResourceType (), resource .getId ());
94
92
throw new RuntimeException ("Trying to handle unknown profiles: " + wrapper .profiles ());
95
93
}
96
-
97
94
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 );
100
96
}
101
97
throw new RuntimeException ("Trying to redact Resource without Meta" );
102
98
}
@@ -105,90 +101,139 @@ public Base redact(ExtractionRedactionWrapper wrapper) {
105
101
* Executes redaction operation on the given base element recursively.
106
102
*
107
103
* @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
110
105
* @param definition Structure definition of the Resource.
111
106
* @param references Allowed references
112
107
* @return redacted Base
113
108
*/
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 );
119
111
120
- String finalElementId ;
121
- if (elementDefinition .hasSlicing ()) {
122
- ElementDefinition slicedElementDefinition = Slicing .checkSlicing (base , elementId , definition );
112
+ redactUnknownExtensions (base , elementId , definition );
123
113
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 );
127
122
if (elementDefinition .getMin () > 0 ) {
128
- base .setProperty ("extension" , ABSENT_REASON_EXTENSION );
123
+ base .setProperty (EXTENSION , createAbsentReasonExtension ( MASKED ) );
129
124
}
130
-
131
125
return base ;
132
126
}
133
- finalElementId = slicedElementDefinition .getId ();
134
- } else {
135
- finalElementId = elementDefinition .getId ();
127
+ elementId = sliced .getId ();
136
128
}
137
129
138
- base .children ().forEach (child -> {
139
- String childId = finalElementId + "." + child .getName ();
130
+ redactChildren (base , elementId , definition , references );
140
131
141
- ElementDefinition childDefinition = definition .elementDefinitionById (childId );
142
- String type ;
143
- int min ;
132
+ return base ;
133
+ }
144
134
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
+ }
154
162
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 ) {
156
191
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 );
159
194
}
160
195
161
- child .getValues (). forEach ( value -> {
196
+ for ( Base value : child .getValues ()) {
162
197
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 );
180
200
} 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 );
187
202
}
188
203
}
204
+ } else {
205
+ if (min > 0 && !"Extension" .equals (child .getTypeCode ())) {
206
+ addDataAbsentReason (base , child , type , baseId );
207
+ }
208
+
189
209
}
190
210
});
211
+ }
191
212
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
+ }
193
237
}
238
+
194
239
}
0 commit comments