Skip to content

Commit 1a7791b

Browse files
bastianschafferalexanderkiel
authored andcommitted
Prevent Compiling Empty List of Attribute Group Filters
Fixes: #299
1 parent d20e2a3 commit 1a7791b

File tree

4 files changed

+77
-20
lines changed

4 files changed

+77
-20
lines changed

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

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -114,16 +114,20 @@ private AnnotatedAttributeGroup annotateGroup(AttributeGroup attributeGroup, Str
114114
annotatedAttributes.add(new AnnotatedAttribute(attribute.attributeRef(), fhirTerser[0], fhirTerser[1], attribute.mustHave(), attribute.linkedGroups()));
115115
}
116116

117-
AnnotatedAttributeGroup standardGroup = attributeGenerator.generate(attributeGroup, patientGroupId);
118-
try {
119-
Predicate<Resource> filter = filterService.compileFilter(attributeGroup.filter(), definition.getType());
120-
return standardGroup
121-
.addAttributes(annotatedAttributes)
122-
.setCompiledFilter(filter);
123-
} catch (Exception e) {
124-
throw new RuntimeException(e);
117+
AnnotatedAttributeGroup group = attributeGenerator
118+
.generate(attributeGroup, patientGroupId)
119+
.addAttributes(annotatedAttributes);
120+
121+
if (!attributeGroup.filter().isEmpty()) {
122+
try {
123+
Predicate<Resource> filter = filterService.compileFilter(attributeGroup.filter(), definition.getType());
124+
return group.setCompiledFilter(filter);
125+
} catch (Exception e) {
126+
throw new RuntimeException(e);
127+
}
125128
}
126129

130+
return group;
127131
}
128132

129133

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

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import java.util.function.Predicate;
2424
import java.util.stream.Collectors;
2525

26+
import static java.util.Objects.requireNonNull;
27+
2628
/**
2729
* A Service that handles filter preprocessing.
2830
*/
@@ -66,27 +68,31 @@ public void init() {
6668
* Preprocesses multiple filters to be ready for evaluation on a specific resource type.
6769
* The predicate can be evaluated on as many resources as needed.
6870
*
69-
* @param attributeGroupFilters the filters to be preprocessed
70-
* @param resourceType the resource type of the resource that the filters should later be applied on
71-
* @return the compiled filters containing parsed FHIRPath expressions that are ready to be evaluated
71+
* @param attributeGroupFilters the filters to be preprocessed
72+
* @param resourceType the resource type of the resource that the filters should later be applied on
73+
* @return the compiled filters containing parsed FHIRPath expressions that are ready to be evaluated
74+
* @throws IllegalArgumentException if attributeGroupFilters is null or empty
7275
*/
7376
public Predicate<Resource> compileFilter(List<Filter> attributeGroupFilters, String resourceType) {
77+
if (attributeGroupFilters == null || attributeGroupFilters.isEmpty()) {
78+
throw new IllegalArgumentException("An empty list of filters can't be compiled.");
79+
}
7480
var parsedFilters = attributeGroupFilters.stream()
7581
.map(filter ->
7682
new ParsedFilter(
7783
filter,
7884
parseFhirPath(searchParameters.get(List.of(filter.name(), filter.type(), resourceType)))))
7985
.toList();
8086

81-
if (parsedFilters.isEmpty()) {
82-
logger.warn("Could not find any matching search parameter for filter. This can later result in unexpected false" +
83-
" negative results of the 'test' method of 'CompiledFilter'.");
84-
}
85-
8687
return new CompiledFilter(parsedFilters, fhirPathEngine);
8788
}
8889

8990
private IFhirPath.IParsedExpression parseFhirPath(String path) {
91+
if (path == null || path.isEmpty()) {
92+
throw new IllegalArgumentException("Could not find any matching search parameter for filter. This can later " +
93+
"result in unexpected false negative results of the 'test' method of 'CompiledFilter'.");
94+
}
95+
9096
try {
9197
return fhirPathEngine.parse(path);
9298
} catch (Exception e) {
@@ -122,6 +128,13 @@ private static boolean isValidSearchParam(SearchParameter searchParam) {
122128
private record CompiledFilter(List<ParsedFilter> filters,
123129
IFhirPath fhirPathEngine) implements Predicate<Resource> {
124130

131+
public CompiledFilter {
132+
if (filters.isEmpty()) {
133+
throw new IllegalArgumentException("CompiledFilter must have at least one parsed filter, but got none.");
134+
}
135+
requireNonNull(fhirPathEngine);
136+
}
137+
125138
@Override
126139
public boolean test(Resource resource) {
127140

@@ -213,5 +226,9 @@ private boolean evaluateDate(Base found, Filter filter) {
213226
}
214227

215228
private record ParsedFilter(Filter filter, IFhirPath.IParsedExpression parsedExpression) {
229+
public ParsedFilter {
230+
requireNonNull(filter);
231+
requireNonNull(parsedExpression);
232+
}
216233
}
217234
}

src/test/java/de/medizininformatikinitiative/torch/service/CrtdlValidatorServiceTest.java

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.junit.jupiter.api.Test;
1313

1414
import java.io.IOException;
15+
import java.time.LocalDate;
1516
import java.util.List;
1617

1718
import static org.assertj.core.api.Assertions.assertThat;
@@ -70,23 +71,38 @@ void referenceWithoutLinkedGroups() throws ValidationException {
7071
}
7172

7273
@Test
73-
void validInput() throws ValidationException {
74-
Crtdl crtdl = new Crtdl(node, new DataExtraction(List.of(patientGroup, new AttributeGroup("test", "https://www.medizininformatik-initiative.de/fhir/core/modul-labor/StructureDefinition/ObservationLab", List.of(), List.of(new Filter("token", "code", List.of(new Code("some-system", "some-code"))))))));
74+
void validInput_withoutFilter() throws ValidationException {
75+
Crtdl crtdl = new Crtdl(node, new DataExtraction(List.of(patientGroup)));
7576

76-
assertThat(validatorService.validate(crtdl).dataExtraction().attributeGroups().get(0).attributes()).isEqualTo(
77+
var validatedCrtdl = validatorService.validate(crtdl);
78+
79+
assertThat(validatedCrtdl).isNotNull();
80+
assertThat(validatedCrtdl.dataExtraction().attributeGroups().getFirst().compiledFilter()).isNull();
81+
assertThat(validatedCrtdl.dataExtraction().attributeGroups().getFirst().attributes()).isEqualTo(
7782
List.of(
7883
new AnnotatedAttribute("Patient.id", "Patient.id", "Patient.id", false),
7984
new AnnotatedAttribute("Patient.meta.profile", "Patient.meta.profile", "Patient.meta.profile", false)
8085
));
86+
}
87+
88+
@Test
89+
void validInput_withFilter() throws ValidationException {
90+
Crtdl crtdl = new Crtdl(node, new DataExtraction(List.of(patientGroup,
91+
new AttributeGroup("test", "https://www.medizininformatik-initiative.de/fhir/core/modul-labor/StructureDefinition/ObservationLab",
92+
List.of(), List.of(new Filter("token", "code", List.of(new Code("some-system", "some-code"))))))));
8193

94+
var validatedCrtdl = validatorService.validate(crtdl);
95+
96+
assertThat(validatedCrtdl).isNotNull();
97+
assertThat(validatedCrtdl.dataExtraction().attributeGroups().get(1).compiledFilter()).isNotNull();
8298
assertThat(validatorService.validate(crtdl).dataExtraction().attributeGroups().get(1).attributes()).isEqualTo(
8399
List.of(
84100
new AnnotatedAttribute("Observation.id", "Observation.id", "Observation.id", false),
85101
new AnnotatedAttribute("Observation.meta.profile", "Observation.meta.profile", "Observation.meta.profile", false),
86102
new AnnotatedAttribute("Observation.subject", "Observation.subject", "Observation.subject", false, List.of("patientGroupId"))
87103
));
88-
89104
}
90105

91106

107+
92108
}

src/test/java/de/medizininformatikinitiative/torch/service/FilterServiceTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package de.medizininformatikinitiative.torch.service;
22

33
import ca.uhn.fhir.context.FhirContext;
4+
import ca.uhn.fhir.fhirpath.IFhirPath;
5+
import de.medizininformatikinitiative.torch.exceptions.ReferenceToPatientException;
6+
import de.medizininformatikinitiative.torch.model.crtdl.AttributeGroup;
47
import de.medizininformatikinitiative.torch.model.crtdl.Code;
58
import de.medizininformatikinitiative.torch.model.crtdl.Filter;
69
import org.hl7.fhir.r4.model.AllergyIntolerance;
@@ -20,6 +23,8 @@
2023
import java.time.format.DateTimeFormatter;
2124
import java.util.List;
2225

26+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
27+
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
2328
import static org.junit.jupiter.api.Assertions.assertFalse;
2429
import static org.junit.jupiter.api.Assertions.assertTrue;
2530

@@ -66,6 +71,21 @@ public void test_compile_MultipleResources() {
6671
assertFalse(result_3);
6772
}
6873

74+
@Test
75+
public void testEmptyFilter() {
76+
assertThatThrownBy(() -> filterService.compileFilter(List.of(), OBSERVATION))
77+
.isInstanceOf(IllegalArgumentException.class)
78+
.hasMessageContaining("An empty list of filters can't be compiled.");
79+
}
80+
81+
@Test
82+
public void test_nonExistingSearchParam() {
83+
assertThatThrownBy(() -> filterService.compileFilter(List.of(new Filter("foo", "bar", List.of())), OBSERVATION))
84+
.isInstanceOf(RuntimeException.class)
85+
.hasMessageContaining("Could not find any matching search parameter for filter. This can later " +
86+
"result in unexpected false negative results of the 'test' method of 'CompiledFilter'.");
87+
}
88+
6989
@Nested
7090
class TestCodeableConcept {
7191

0 commit comments

Comments
 (0)