Skip to content

Commit

Permalink
refactor 'check search bundle' service task implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
wetret committed Dec 4, 2024
1 parent a6e23de commit 4b3fc2f
Show file tree
Hide file tree
Showing 6 changed files with 388 additions and 453 deletions.
6 changes: 0 additions & 6 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,6 @@
<version>${dsf.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.12.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,57 +1,38 @@
package de.medizininformatik_initiative.process.report.service;

import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.ResourceType;
import org.hl7.fhir.r4.model.Task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.MultiValueMap;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.beans.factory.InitializingBean;

import de.medizininformatik_initiative.process.report.ConstantsReport;
import de.medizininformatik_initiative.process.report.util.SearchQueryCheckService;
import dev.dsf.bpe.v1.ProcessPluginApi;
import dev.dsf.bpe.v1.activity.AbstractServiceDelegate;
import dev.dsf.bpe.v1.variables.Target;
import dev.dsf.bpe.v1.variables.Variables;

public class CheckSearchBundle extends AbstractServiceDelegate
public class CheckSearchBundle extends AbstractServiceDelegate implements InitializingBean
{
private static final Logger logger = LoggerFactory.getLogger(CheckSearchBundle.class);

private static final Pattern MODIFIERS = Pattern.compile(":.*");
private static final Pattern YEAR_ONLY = Pattern.compile("\\b20\\d{2}(?!\\S)");
private static final String DATE_EQUALITY_FILTER = "eq";
private final SearchQueryCheckService searchQueryCheckService;

private static final String CAPABILITY_STATEMENT_PATH = "metadata";
private static final String SUMMARY_SEARCH_PARAM = "_summary";
private static final String SUMMARY_SEARCH_PARAM_VALUE_COUNT = "count";
private static final String TYPE_SEARCH_PARAM = "type";

private static final Set<String> ALL_RESOURCE_TYPES = EnumSet.allOf(ResourceType.class).stream()
.map(ResourceType::name).collect(Collectors.toSet());

private static final List<String> DATE_SEARCH_PARAMS = List.of("date", "recorded-date", "onset-date", "effective",
"effective-time", "authored", "collected", "issued", "period", "location-period", "occurrence");
private static final List<String> TOKEN_SEARCH_PARAMS = List.of("code", "ingredient-code", "type");
private static final List<String> OTHER_SEARCH_PARAMS = List.of("_profile", "_summary");
private static final List<String> VALID_SEARCH_PARAMS = Stream
.of(DATE_SEARCH_PARAMS.stream(), TOKEN_SEARCH_PARAMS.stream(), OTHER_SEARCH_PARAMS.stream()).flatMap(s -> s)
.toList();

public CheckSearchBundle(ProcessPluginApi api)
public CheckSearchBundle(ProcessPluginApi api, SearchQueryCheckService searchQueryCheckService)
{
super(api);
this.searchQueryCheckService = searchQueryCheckService;
}

@Override
public void afterPropertiesSet() throws Exception
{
super.afterPropertiesSet();
Objects.requireNonNull(searchQueryCheckService, "searchQueryCheckService");
}

@Override
Expand All @@ -66,15 +47,12 @@ protected void doExecute(DelegateExecution execution, Variables variables)

try
{
List<Bundle.BundleEntryComponent> searches = bundle.getEntry();

testNoResources(searches);
testRequestMethod(searches);
testRequestUrls(searches);
searchQueryCheckService.checkBundle(bundle);

logger.info(
"Search Bundle downloaded from HRP '{}' as part of Task with id '{}' contains only valid requests of type GET and valid search params {}",
target.getOrganizationIdentifierValue(), task.getId(), VALID_SEARCH_PARAMS);
target.getOrganizationIdentifierValue(), task.getId(),
searchQueryCheckService.getValidSearchParams());
}
catch (Exception exception)
{
Expand All @@ -86,155 +64,4 @@ protected void doExecute(DelegateExecution execution, Variables variables)
exception);
}
}

private void testNoResources(List<Bundle.BundleEntryComponent> searches)
{
if (searches.stream().map(Bundle.BundleEntryComponent::getResource).anyMatch(Objects::nonNull))
throw new RuntimeException("Search Bundle contains resources");
}

private void testRequestMethod(List<Bundle.BundleEntryComponent> searches)
{
long searchesCount = searches.size();
long httpGetCount = searches.stream().filter(Bundle.BundleEntryComponent::hasRequest)
.map(Bundle.BundleEntryComponent::getRequest).filter(Bundle.BundleEntryRequestComponent::hasMethod)
.map(Bundle.BundleEntryRequestComponent::getMethod).filter(Bundle.HTTPVerb.GET::equals).count();

if (searchesCount != httpGetCount)
throw new RuntimeException("Search Bundle contains HTTP method other then GET");
}

private void testRequestUrls(List<Bundle.BundleEntryComponent> searches)
{
int searchesCount = searches.size();
List<Bundle.BundleEntryRequestComponent> requests = searches.stream()
.filter(Bundle.BundleEntryComponent::hasRequest).map(Bundle.BundleEntryComponent::getRequest)
.filter(Bundle.BundleEntryRequestComponent::hasUrl).toList();
int requestCount = requests.size();

if (searchesCount != requestCount)
throw new RuntimeException("Search Bundle contains request without url");

List<UriComponents> uriComponents = requests.stream()
.map(r -> UriComponentsBuilder.fromUriString(r.getUrl()).build()).toList();

testContainsOnlyResourcePath(uriComponents);
testContainsValidSummaryCount(uriComponents);
testContainsValidSearchParams(uriComponents);
testContainsValidDateSearchParams(uriComponents);
testContainsValidTokenSearchParams(uriComponents);
}

private void testContainsOnlyResourcePath(List<UriComponents> uriComponents)
{
uriComponents.stream().filter(u -> !CAPABILITY_STATEMENT_PATH.equals(u.getPath())).forEach(this::testPath);
}

private void testPath(UriComponents uriComponents)
{
if (!ALL_RESOURCE_TYPES.contains(uriComponents.getPath()))
{
throw new RuntimeException(
"Search Bundle contains request url with forbidden path - [" + uriComponents.getPath() + "]");
}
}

private void testContainsValidSummaryCount(List<UriComponents> uriComponents)
{
uriComponents.stream().filter(u -> !CAPABILITY_STATEMENT_PATH.equals(u.getPath()))
.map(UriComponents::getQueryParams).forEach(this::testSummaryCount);
}

private void testSummaryCount(MultiValueMap<String, String> queryParams)
{
List<String> summaryParams = queryParams.get(SUMMARY_SEARCH_PARAM);

if (summaryParams == null || summaryParams.isEmpty())
{
throw new RuntimeException("Search Bundle contains request url without _summary parameter");
}

if (summaryParams.size() > 1)
{
throw new RuntimeException("Search Bundle contains request url with more than one _summary parameter");
}

if (!SUMMARY_SEARCH_PARAM_VALUE_COUNT.equals(summaryParams.get(0)))
{
throw new RuntimeException(
"Search Bundle contains request url with unexpected _summary parameter value (expected: count, actual: "
+ summaryParams.get(0) + ")");
}
}

private void testContainsValidSearchParams(List<UriComponents> uriComponents)
{
uriComponents.stream().filter(u -> !CAPABILITY_STATEMENT_PATH.equals(u.getPath()))
.map(UriComponents::getQueryParams).forEach(this::testSearchParamNames);
}

private void testSearchParamNames(MultiValueMap<String, String> queryParams)
{
if (queryParams.keySet().stream().map(s -> MODIFIERS.matcher(s).replaceAll(""))
.anyMatch(s -> !VALID_SEARCH_PARAMS.contains(s)))
throw new RuntimeException("Search Bundle contains invalid search params, only allowed search params are "
+ VALID_SEARCH_PARAMS);
}

private void testContainsValidDateSearchParams(List<UriComponents> uriComponents)
{
uriComponents.stream().filter(u -> !CAPABILITY_STATEMENT_PATH.equals(u.getPath()))
.map(UriComponents::getQueryParams).forEach(this::testSearchParamDateValues);
}

private void testSearchParamDateValues(MultiValueMap<String, String> queryParams)
{
List<Map.Entry<String, String>> dateParams = queryParams.entrySet().stream()
.filter(e -> DATE_SEARCH_PARAMS.contains(MODIFIERS.matcher(e.getKey()).replaceAll("")))
.flatMap(e -> e.getValue().stream().map(v -> Map.entry(e.getKey(), v))).toList();

List<Map.Entry<String, String>> erroneousDateFilters = dateParams.stream()
.filter(e -> !e.getValue().startsWith(DATE_EQUALITY_FILTER)).toList();

if (erroneousDateFilters.size() > 0)
throw new RuntimeException(
"Search Bundle contains date search params not starting with 'eq' - [" + erroneousDateFilters
.stream().map(e -> e.getKey() + ":" + e.getValue()).collect(Collectors.joining(",")) + "]");

List<Map.Entry<String, String>> erroneousDateValues = dateParams.stream()
.filter(e -> !YEAR_ONLY.matcher(e.getValue().replace(DATE_EQUALITY_FILTER, "")).matches()).toList();

if (erroneousDateValues.size() > 0)
throw new RuntimeException(
"Search Bundle contains date search params not limited to a year - [" + erroneousDateValues.stream()
.map(e -> e.getKey() + ":" + e.getValue()).collect(Collectors.joining(",")) + "]");
}

private void testContainsValidTokenSearchParams(List<UriComponents> uriComponents)
{
uriComponents.stream().filter(u -> !CAPABILITY_STATEMENT_PATH.equals(u.getPath()))
.forEach(this::testSearchParamTokenValues);
}

private void testSearchParamTokenValues(UriComponents uriComponents)
{
List<Map.Entry<String, String>> codeParams = uriComponents.getQueryParams().entrySet().stream()
.filter(e -> TOKEN_SEARCH_PARAMS.contains(MODIFIERS.matcher(e.getKey()).replaceAll("")))
.flatMap(e -> e.getValue().stream().map(v -> Map.entry(e.getKey(), v))).toList();

// Exemption for Encounter.type token params
List<Map.Entry<String, String>> erroneousCodeValues = codeParams.stream()
.filter(e -> !e.getValue().endsWith("|"))
.filter(e -> !isEncounterType(uriComponents.getPath(), e.getKey())).toList();

if (erroneousCodeValues.size() > 0)
throw new RuntimeException(
"Search Bundle contains code search params not limited to system - [" + erroneousCodeValues.stream()
.map(e -> e.getKey() + ":" + e.getValue()).collect(Collectors.joining(",")) + "]");
}

private boolean isEncounterType(String path, String paramName)
{
return TYPE_SEARCH_PARAM.equals(paramName) && ResourceType.Encounter.name().equals(path);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import de.medizininformatik_initiative.process.report.service.SetTimer;
import de.medizininformatik_initiative.process.report.service.StoreReceipt;
import de.medizininformatik_initiative.process.report.util.ReportStatusGenerator;
import de.medizininformatik_initiative.process.report.util.SearchQueryCheckService;
import dev.dsf.bpe.v1.ProcessPluginApi;
import dev.dsf.bpe.v1.ProcessPluginDeploymentStateListener;
import dev.dsf.bpe.v1.documentation.ProcessDocumentation;
Expand Down Expand Up @@ -94,7 +95,14 @@ public DownloadSearchBundle downloadSearchBundle()
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public CheckSearchBundle checkSearchBundle()
{
return new CheckSearchBundle(api);
return new CheckSearchBundle(api, searchQueryCheckService());
}

@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public SearchQueryCheckService searchQueryCheckService()
{
return new SearchQueryCheckService();
}

@Bean
Expand Down
Loading

0 comments on commit 4b3fc2f

Please sign in to comment.