Skip to content

Commit 7d0c5ae

Browse files
committed
Support Nested Lists
closes #499
1 parent 9ad7a69 commit 7d0c5ae

31 files changed

+974
-308
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package de.medizininformatikinitiative.torch.model.crtdl;
2+
3+
import static java.util.Objects.requireNonNull;
4+
5+
public record FieldCondition(String fieldName, String condition) {
6+
7+
public FieldCondition {
8+
requireNonNull(fieldName, "fieldName must not be null");
9+
requireNonNull(condition, "condition must not be null");
10+
}
11+
12+
13+
public String fhirPath() {
14+
return fieldName + condition;
15+
}
16+
}
Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
package de.medizininformatikinitiative.torch.model.crtdl.annotated;
22

33
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import de.medizininformatikinitiative.torch.model.crtdl.FieldCondition;
45

6+
import java.util.ArrayList;
57
import java.util.List;
68

79
import static java.util.Objects.requireNonNull;
810

911
@JsonIgnoreProperties(ignoreUnknown = true)
1012
public record AnnotatedAttribute(String attributeRef, String fhirPath,
11-
String terserPath,
1213
boolean mustHave,
1314
List<String> linkedGroups) {
1415

1516
public AnnotatedAttribute {
1617
requireNonNull(attributeRef);
1718
requireNonNull(fhirPath);
18-
requireNonNull(terserPath);
1919
linkedGroups = linkedGroups == null ? List.of() : List.copyOf(linkedGroups);
2020
}
2121

@@ -24,9 +24,58 @@ public record AnnotatedAttribute(String attributeRef, String fhirPath,
2424
*/
2525
public AnnotatedAttribute(String attributeRef,
2626
String fhirPath,
27-
String terserPath,
2827
boolean mustHave
2928
) {
30-
this(attributeRef, fhirPath, terserPath, mustHave, List.of()); // Default value for includeReferenceOnly
29+
this(attributeRef, fhirPath, mustHave, List.of()); // Default value for includeReferenceOnly
3130
}
31+
32+
/**
33+
* Splits the FHIR path into sub-paths according to the following rules:
34+
* Splits after each {@code where} clause (including the where clause itself)
35+
* Splits at each dot ({@code .}) when there is no where clause
36+
* This method is useful for processing complex FHIR path expressions by breaking them
37+
* into manageable segments that can be evaluated sequentially.
38+
*
39+
* @return a list of sub-paths in the order they appear in the original FHIR path.
40+
* The list is never {@code null} and contains at least one element.
41+
*/
42+
private static FieldCondition parseSegment(String segment) {
43+
int whereIndex = segment.indexOf(".where(");
44+
if (whereIndex >= 0) {
45+
// Keep the full `.where(...)` in the condition
46+
String field = segment.substring(0, whereIndex).trim();
47+
String cond = segment.substring(whereIndex).trim(); // include '.where(...)'
48+
return new FieldCondition(field, cond);
49+
} else {
50+
return new FieldCondition(segment.trim(), "");
51+
}
52+
}
53+
54+
/**
55+
* Parse this attribute's FHIRPath into field + condition objects
56+
*/
57+
public List<FieldCondition> splitFhirPath() {
58+
List<FieldCondition> result = new ArrayList<>();
59+
int start = 0;
60+
int parenDepth = 0;
61+
62+
for (int i = 0; i < fhirPath.length(); i++) {
63+
char c = fhirPath.charAt(i);
64+
if (c == '(') parenDepth++;
65+
else if (c == ')') parenDepth--;
66+
else if (c == '.' && parenDepth == 0 && !fhirPath.startsWith("where(", i + 1)) {
67+
result.add(parseSegment(fhirPath.substring(start, i)));
68+
start = i + 1;
69+
}
70+
}
71+
72+
if (start < fhirPath.length()) {
73+
result.add(parseSegment(fhirPath.substring(start)));
74+
}
75+
76+
return result;
77+
}
78+
3279
}
80+
81+

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package de.medizininformatikinitiative.torch.model.crtdl.annotated;
22

33

4+
import de.medizininformatikinitiative.torch.model.crtdl.FieldCondition;
45
import de.medizininformatikinitiative.torch.model.crtdl.Filter;
56
import de.medizininformatikinitiative.torch.model.fhir.Query;
67
import de.medizininformatikinitiative.torch.model.fhir.QueryParams;
8+
import de.medizininformatikinitiative.torch.model.management.CopyTreeNode;
79
import de.medizininformatikinitiative.torch.model.mapping.DseMappingTreeBase;
810
import org.hl7.fhir.r4.model.Resource;
911

@@ -105,4 +107,17 @@ public List<AnnotatedAttribute> refAttributes() {
105107
return attributes.stream().filter(annotatedAttribute -> !annotatedAttribute.linkedGroups().isEmpty()).toList();
106108
}
107109

110+
public CopyTreeNode buildTree() {
111+
CopyTreeNode root = new CopyTreeNode(resourceType());
112+
113+
for (AnnotatedAttribute attr : attributes) {
114+
List<FieldCondition> parts = attr.splitFhirPath();
115+
CopyTreeNode current = root;
116+
for (int i = 1; i < parts.size(); i++) {
117+
current = current.getOrCreateChild(parts.get(i));
118+
}
119+
}
120+
return root;
121+
}
122+
108123
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package de.medizininformatikinitiative.torch.model.management;
2+
3+
4+
import de.medizininformatikinitiative.torch.model.crtdl.FieldCondition;
5+
6+
import java.util.ArrayList;
7+
import java.util.List;
8+
import java.util.Objects;
9+
import java.util.Optional;
10+
import java.util.stream.Stream;
11+
12+
public record CopyTreeNode(FieldCondition fieldCondition,
13+
List<CopyTreeNode> children) {
14+
15+
public CopyTreeNode(String fieldName) {
16+
this(new FieldCondition(fieldName, ""), new ArrayList<>());
17+
}
18+
19+
public CopyTreeNode(String fieldName, String condition, List<CopyTreeNode> children) {
20+
this(new FieldCondition(fieldName, condition), children);
21+
}
22+
23+
public CopyTreeNode(FieldCondition fieldCondition) {
24+
this(fieldCondition, new ArrayList<>());
25+
}
26+
27+
public CopyTreeNode {
28+
Objects.requireNonNull(fieldCondition);
29+
children = new ArrayList<>(children); // mutable copy
30+
}
31+
32+
public boolean isConditional() {
33+
return !fieldCondition.condition().isEmpty();
34+
}
35+
36+
public String fhirPath() {
37+
return fieldCondition.fhirPath();
38+
}
39+
40+
public String condition() {
41+
return fieldCondition.condition();
42+
}
43+
44+
public String fieldName() {
45+
return fieldCondition.fieldName();
46+
}
47+
48+
public CopyTreeNode getOrCreateChild(FieldCondition fieldCondition) {
49+
return getChild(fieldCondition).orElseGet(() -> {
50+
CopyTreeNode child = new CopyTreeNode(fieldCondition);
51+
children.add(child);
52+
return child;
53+
});
54+
}
55+
56+
public Optional<CopyTreeNode> getChild(FieldCondition fieldCondition) {
57+
return children.stream()
58+
.filter(c -> c.fieldCondition.equals(fieldCondition))
59+
.findFirst();
60+
}
61+
62+
/**
63+
* Merges this {@code CopyTreeNode} with another node, combining children while respecting
64+
* unconditional and conditional branches.
65+
* <p>
66+
* Merge rules:
67+
* <ul>
68+
* <li>Unconditional children (condition == "") are merged recursively.</li>
69+
* <li>Conditional children (condition != "") are kept unless fully covered by an
70+
* unconditional child with empty children.</li>
71+
* <li>Conditional children that are partially covered by an unconditional child
72+
* will keep only the remaining, uncovered children.</li>
73+
* <li>This merge is applied recursively for all child nodes.</li>
74+
* </ul>
75+
*
76+
* <p>Example:</p>
77+
* <pre>
78+
* Node a: identifier -> [] (unconditional, empty children)
79+
* Node b: identifier.where(type='official') -> [value]
80+
* Merged result: identifier -> [] (conditional branch removed)
81+
* </pre>
82+
*
83+
* @param other the other {@code CopyTreeNode} to merge with
84+
* @return a new {@code CopyTreeNode} containing the merged structure
85+
*/
86+
public CopyTreeNode merged(CopyTreeNode other) {
87+
// Step 0: combine unconditional children first
88+
List<CopyTreeNode> allChildren = new ArrayList<>();
89+
90+
// Collect all unconditional children from both trees
91+
List<CopyTreeNode> uncondThis = this.children().stream()
92+
.filter(c -> c.fieldCondition.condition().isEmpty())
93+
.toList();
94+
List<CopyTreeNode> uncondOther = other.children().stream()
95+
.filter(c -> c.fieldCondition.condition().isEmpty())
96+
.toList();
97+
98+
// Merge unconditional children by fieldName recursively
99+
List<CopyTreeNode> mergedUncond = new ArrayList<>();
100+
for (CopyTreeNode c : uncondThis) {
101+
CopyTreeNode match = uncondOther.stream()
102+
.filter(o -> o.fieldCondition.fieldName().equals(c.fieldCondition.fieldName()))
103+
.findFirst()
104+
.orElse(null);
105+
106+
if (match != null) {
107+
mergedUncond.add(c.merged(match));
108+
} else {
109+
mergedUncond.add(c);
110+
}
111+
}
112+
113+
// Add unconditional children from other that were not in this
114+
uncondOther.stream()
115+
.filter(o -> mergedUncond.stream().noneMatch(m -> m.fieldCondition.fieldName().equals(o.fieldCondition.fieldName())))
116+
.forEach(mergedUncond::add);
117+
118+
allChildren.addAll(mergedUncond);
119+
120+
// Step 1: handle conditional children
121+
List<CopyTreeNode> condChildren = Stream.concat(
122+
this.children().stream().filter(c -> !c.fieldCondition.condition().isEmpty()),
123+
other.children().stream().filter(c -> !c.fieldCondition.condition().isEmpty())
124+
).toList();
125+
126+
for (CopyTreeNode c : condChildren) {
127+
// Find corresponding unconditional child with same fieldName
128+
CopyTreeNode uncondMatch = mergedUncond.stream()
129+
.filter(u -> u.fieldCondition.fieldName().equals(c.fieldCondition.fieldName()))
130+
.findFirst().orElse(null);
131+
132+
List<CopyTreeNode> remainingChildren;
133+
if (uncondMatch != null) {
134+
// Remove any children that are already in the unconditional branch
135+
remainingChildren = c.children().stream()
136+
.filter(ch -> uncondMatch.children().stream().noneMatch(uCh -> uCh.equals(ch)))
137+
.toList();
138+
} else {
139+
remainingChildren = new ArrayList<>(c.children());
140+
}
141+
142+
if (!remainingChildren.isEmpty()) {
143+
allChildren.add(new CopyTreeNode(c.fieldCondition, remainingChildren));
144+
}
145+
}
146+
147+
return new CopyTreeNode(this.fieldCondition, allChildren);
148+
}
149+
150+
151+
}

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

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,8 @@
55
import de.medizininformatikinitiative.torch.model.consent.PatientBatchWithConsent;
66
import de.medizininformatikinitiative.torch.model.crtdl.annotated.AnnotatedAttribute;
77
import de.medizininformatikinitiative.torch.model.crtdl.annotated.AnnotatedAttributeGroup;
8-
import de.medizininformatikinitiative.torch.model.management.ExtractionRedactionWrapper;
9-
import de.medizininformatikinitiative.torch.model.management.PatientResourceBundle;
10-
import de.medizininformatikinitiative.torch.model.management.ProfileAttributeCollection;
8+
import de.medizininformatikinitiative.torch.model.management.*;
119
import de.medizininformatikinitiative.torch.model.management.ResourceBundle;
12-
import de.medizininformatikinitiative.torch.model.management.ResourceGroup;
1310
import de.medizininformatikinitiative.torch.util.ElementCopier;
1411
import de.medizininformatikinitiative.torch.util.Redaction;
1512
import de.medizininformatikinitiative.torch.util.ResourceUtils;
@@ -18,11 +15,7 @@
1815
import org.slf4j.Logger;
1916
import org.slf4j.LoggerFactory;
2017

21-
import java.util.HashMap;
22-
import java.util.HashSet;
23-
import java.util.Map;
24-
import java.util.Optional;
25-
import java.util.Set;
18+
import java.util.*;
2619
import java.util.stream.Collectors;
2720

2821
import static java.util.Objects.requireNonNull;
@@ -128,7 +121,7 @@ public PatientResourceBundle transform(PatientResourceBundle patientResourceBund
128121

129122
Resource transformed = transform(wrapper);
130123

131-
bundle.remove(ResourceUtils.getRelativeURL(transformed));
124+
bundle.remove(resourceId);
132125
bundle.put(transformed);
133126

134127

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ private AnnotatedAttributeGroup annotateGroup(AttributeGroup attributeGroup, Com
104104
}
105105

106106
String[] fhirTerser = FhirPathBuilder.handleSlicingForFhirPath(attribute.attributeRef(), definition);
107-
annotatedAttributes.add(new AnnotatedAttribute(attribute.attributeRef(), fhirTerser[0], fhirTerser[1], attribute.mustHave(), attribute.linkedGroups()));
107+
annotatedAttributes.add(new AnnotatedAttribute(attribute.attributeRef(), fhirTerser[0], attribute.mustHave(), attribute.linkedGroups()));
108108
}
109109

110110
AnnotatedAttributeGroup group = attributeGenerator

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,16 @@ public AnnotatedAttributeGroup generate(AttributeGroup attributeGroup, String pa
4242

4343
String resourceType = definition.type();
4444
String id = resourceType + ".id";
45-
tempAttributes.add(new AnnotatedAttribute(id, id, id, false));
45+
tempAttributes.add(new AnnotatedAttribute(id, id, false));
4646
String profile = resourceType + ".meta.profile";
47-
tempAttributes.add(new AnnotatedAttribute(profile, profile, profile, false));
47+
tempAttributes.add(new AnnotatedAttribute(profile, profile, false));
4848

4949

5050
if (compartmentManager.isInCompartment(resourceType)) {
5151
for (String field : patientRefFields) {
5252
String fieldString = resourceType + "." + field;
5353
if (definition.elementDefinitionById(fieldString).isPresent()) {
54-
tempAttributes.add(new AnnotatedAttribute(fieldString, fieldString, fieldString, false, List.of(patientGroupId)));
54+
tempAttributes.add(new AnnotatedAttribute(fieldString, fieldString, false, List.of(patientGroupId)));
5555
}
5656
}
5757
}

0 commit comments

Comments
 (0)