1
0
mirror of https://github.com/pcvolkmer/mv64e-etl-processor synced 2025-09-13 17:02:52 +00:00

1 Commits

Author SHA1 Message Date
328bf019af chore: bump version 2025-09-03 22:17:48 +02:00
8 changed files with 214 additions and 264 deletions

View File

@@ -7,22 +7,22 @@ plugins {
war
id("org.springframework.boot") version "3.5.5"
id("io.spring.dependency-management") version "1.1.7"
kotlin("jvm") version "2.2.10"
kotlin("plugin.spring") version "2.2.10"
kotlin("jvm") version "1.9.25"
kotlin("plugin.spring") version "1.9.25"
jacoco
}
group = "dev.dnpm"
version = "0.12.0-SNAPSHOT"
version = "0.11.1"
var versions = mapOf(
"mtb-dto" to "0.1.0-SNAPSHOT",
"hapi-fhir" to "8.4.0",
"mockito-kotlin" to "6.0.0",
"archunit" to "1.4.1",
"hapi-fhir" to "7.6.1",
"mockito-kotlin" to "5.4.0",
"archunit" to "1.3.0",
// Webjars
"webjars-locator" to "0.52",
"echarts" to "6.0.0",
"echarts" to "5.4.3",
"htmx.org" to "1.9.12"
)

View File

@@ -39,8 +39,6 @@ public class GicsConsentService implements IConsentService {
private final RestTemplate restTemplate;
private final FhirContext fhirContext;
private final GIcsConfigProperties gIcsConfigProperties;
private final String BROAD_CONSENT_PROFILE_URI = "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung";
private final String BROAD_CONSENT_POLICY = "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1791";
public GicsConsentService(
GIcsConfigProperties gIcsConfigProperties,
@@ -304,36 +302,6 @@ public class GicsConsentService implements IConsentService {
@Override
public Bundle getConsent(String patientId, Date requestDate, ConsentDomain consentDomain) {
Bundle gIcsResultBundle = currentConsentForPersonAndTemplate(patientId, consentDomain, requestDate);
if (ConsentDomain.BROAD_CONSENT == consentDomain) {
return convertGicsResultToMiiBroadConsent(gIcsResultBundle);
}
return gIcsResultBundle;
}
protected Bundle convertGicsResultToMiiBroadConsent(Bundle gIcsResultBundle) {
if (gIcsResultBundle == null
|| gIcsResultBundle.getEntry().isEmpty()
|| !(gIcsResultBundle.getEntry().getFirst().getResource() instanceof Consent))
return gIcsResultBundle;
Bundle.BundleEntryComponent bundleEntryComponent = gIcsResultBundle.getEntry().getFirst();
var consentAsOne = (Consent) bundleEntryComponent.getResource();
if (consentAsOne.getPolicy().stream().noneMatch(p -> p.getUri().equals(BROAD_CONSENT_POLICY))) {
consentAsOne.addPolicy(new Consent.ConsentPolicyComponent().setUri(BROAD_CONSENT_POLICY));
}
if (consentAsOne.getMeta().getProfile().stream().noneMatch(p -> p.getValue().equals(BROAD_CONSENT_PROFILE_URI))) {
consentAsOne.getMeta().addProfile(BROAD_CONSENT_PROFILE_URI);
}
consentAsOne.setPolicyRule(null);
gIcsResultBundle.getEntry().stream().skip(1).forEach(c -> consentAsOne.getProvision().addProvision(((Consent) c.getResource()).getProvision().getProvisionFirstRep()));
gIcsResultBundle.getEntry().clear();
gIcsResultBundle.addEntry(bundleEntryComponent);
return gIcsResultBundle;
return currentConsentForPersonAndTemplate(patientId, consentDomain, requestDate);
}
}

View File

@@ -23,6 +23,8 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.IParser;
import dev.dnpm.etl.processor.config.AppFhirConfig;
import dev.dnpm.etl.processor.config.GPasConfigProperties;
import java.net.URI;
import java.net.URISyntaxException;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.StringUtils;
import org.apache.hc.core5.net.URIBuilder;
@@ -37,11 +39,9 @@ import org.springframework.http.*;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.web.client.HttpClientErrorException.BadRequest;
import org.springframework.web.client.HttpClientErrorException.Unauthorized;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import java.net.URI;
import java.net.URISyntaxException;
public class GpasPseudonymGenerator implements Generator {
private final FhirContext r4Context;
@@ -52,10 +52,10 @@ public class GpasPseudonymGenerator implements Generator {
private final RestTemplate restTemplate;
private final @NotNull String genomDeTanDomain;
private final @NotNull String pidPsnDomain;
protected static final String CREATE_OR_GET_PSN = "$pseudonymizeAllowCreate";
protected static final String CREATE_MULTI_DOMAIN_PSN = "$pseudonymize-secondary";
private static final String SINGLE_PSN_PART_NAME = "pseudonym";
private static final String MULTI_PSN_PART_NAME = "value";
protected final static String createOrGetPsn = "$pseudonymizeAllowCreate";
protected final static String createMultiDomainPsn = "$pseudonymize-secondary";
private final static String SINGLE_PSN_PART_NAME = "pseudonym";
private final static String MULTI_PSN_PART_NAME = "value";
public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate,
RestTemplate restTemplate, AppFhirConfig appFhirConfig) {
@@ -85,7 +85,7 @@ public class GpasPseudonymGenerator implements Generator {
switch (domainType) {
case SINGLE_PSN_DOMAIN -> {
final var requestBody = createSinglePsnRequestBody(id, pidPsnDomain);
final var responseEntity = getGpasPseudonym(requestBody, CREATE_OR_GET_PSN);
final var responseEntity = getGpasPseudonym(requestBody, createOrGetPsn);
final var gPasPseudonymResult = (Parameters) r4Context.newJsonParser()
.parseResource(responseEntity.getBody());
@@ -93,7 +93,7 @@ public class GpasPseudonymGenerator implements Generator {
}
case MULTI_PSN_DOMAIN -> {
final var requestBody = createMultiPsnRequestBody(id, genomDeTanDomain);
final var responseEntity = getGpasPseudonym(requestBody, CREATE_MULTI_DOMAIN_PSN);
final var responseEntity = getGpasPseudonym(requestBody, createMultiDomainPsn);
final var gPasPseudonymResult = (Parameters) r4Context.newJsonParser()
.parseResource(responseEntity.getBody());
@@ -150,22 +150,23 @@ public class GpasPseudonymGenerator implements Generator {
log.debug("API request succeeded. Response: {}", responseEntity.getStatusCode());
return responseEntity;
}
} catch (BadRequest e) {
} catch (RestClientException rce) {
if (rce instanceof BadRequest) {
String msg = "gPas or request configuration is incorrect. Please check both."
+ e.getMessage();
log.error(msg);
throw new PseudonymRequestFailed(msg, e);
} catch (Unauthorized e) {
var msg = "gPas access credentials are invalid check your configuration. msg: '%s"
.formatted(e.getMessage());
log.error(msg);
throw new PseudonymRequestFailed(msg, e);
+ rce.getMessage();
log.debug(
msg);
throw new PseudonymRequestFailed(msg, rce);
}
catch (Exception unexpected) {
if (rce instanceof Unauthorized) {
var msg = "gPas access credentials are invalid check your configuration. msg: '%s".formatted(
rce.getMessage());
log.error(msg);
throw new PseudonymRequestFailed(msg, rce);
}
} catch (Exception unexpected) {
throw new PseudonymRequestFailed(
"API request due unexpected error unsuccessful gPas unsuccessful.",
unexpected
);
"API request due unexpected error unsuccessful gPas unsuccessful.", unexpected);
}
throw new PseudonymRequestFailed(
"API request due unexpected error unsuccessful gPas unsuccessful.");

View File

@@ -30,7 +30,6 @@ import org.springframework.data.relational.core.mapping.Table
import org.springframework.data.repository.CrudRepository
import org.springframework.data.repository.PagingAndSortingRepository
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.*
@Table("request")
@@ -66,12 +65,6 @@ data class Request(
processedAt: Instant
) :
this(null, uuid, patientPseudonym, pid, fingerprint, type, status, processedAt)
fun isPendingUnknown(): Boolean {
return this.status == RequestStatus.UNKNOWN && this.processedAt.isBefore(
Instant.now().minus(10, ChronoUnit.MINUTES)
)
}
}
@JvmRecord
@@ -97,23 +90,19 @@ interface RequestRepository : CrudRepository<Request, Long>, PagingAndSortingRep
@Query("SELECT count(*) AS count, status FROM request WHERE type = 'MTB_FILE' GROUP BY status ORDER BY status, count DESC;")
fun countStates(): List<CountedState>
@Query(
"SELECT count(*) AS count, status FROM (" +
@Query("SELECT count(*) AS count, status FROM (" +
"SELECT status, rank() OVER (PARTITION BY patient_pseudonym ORDER BY processed_at DESC) AS rank FROM request " +
"WHERE type = 'MTB_FILE' AND status NOT IN ('DUPLICATION') " +
") rank WHERE rank = 1 GROUP BY status ORDER BY status, count DESC;"
)
") rank WHERE rank = 1 GROUP BY status ORDER BY status, count DESC;")
fun findPatientUniqueStates(): List<CountedState>
@Query("SELECT count(*) AS count, status FROM request WHERE type = 'DELETE' GROUP BY status ORDER BY status, count DESC;")
fun countDeleteStates(): List<CountedState>
@Query(
"SELECT count(*) AS count, status FROM (" +
@Query("SELECT count(*) AS count, status FROM (" +
"SELECT status, rank() OVER (PARTITION BY patient_pseudonym ORDER BY processed_at DESC) AS rank FROM request " +
"WHERE type = 'DELETE'" +
") rank WHERE rank = 1 GROUP BY status ORDER BY status, count DESC;"
)
") rank WHERE rank = 1 GROUP BY status ORDER BY status, count DESC;")
fun findPatientUniqueDeleteStates(): List<CountedState>
}

View File

@@ -137,7 +137,15 @@ class ConsentProcessor(
}
val provisionComponent: ProvisionComponent = provisions.first()
val provisionCode = getProvisionCode(provisionComponent)
var provisionCode: String? = null
if (provisionComponent.code != null && provisionComponent.code.isNotEmpty()) {
val codableConcept: CodeableConcept = provisionComponent.code.first()
if (codableConcept.coding != null && codableConcept.coding.isNotEmpty()) {
provisionCode = codableConcept.coding.first().code
}
}
if (provisionCode != null) {
try {
val modelProjectConsentPurpose =
@@ -169,17 +177,6 @@ class ConsentProcessor(
}
}
private fun getProvisionCode(provisionComponent: ProvisionComponent): String? {
var provisionCode: String? = null
if (provisionComponent.code != null && provisionComponent.code.isNotEmpty()) {
val codableConcept: CodeableConcept = provisionComponent.code.first()
if (codableConcept.coding != null && codableConcept.coding.isNotEmpty()) {
provisionCode = codableConcept.coding.first().code
}
}
return provisionCode
}
private fun setGenomDeSubmissionType(mtbFile: Mtb) {
if (appConfigProperties.genomDeTestSubmission) {
mtbFile.metadata.type = MvhSubmissionType.TEST
@@ -241,9 +238,9 @@ class ConsentProcessor(
consent.provision.provision.filter { subProvision ->
isRequestDateInRange(requestDate, subProvision.period)
// search coding entries of current provision for code and system
subProvision.code.map { c -> c.coding }.flatten().any { code ->
subProvision.code.map { c -> c.coding }.flatten().firstOrNull { code ->
targetCode.equals(code.code) && targetSystem.equals(code.system)
}
} != null
}.map { subProvision ->
subProvision
}
@@ -260,11 +257,11 @@ class ConsentProcessor(
researchAllowedPolicySystem: String?,
policyRules: Collection<Coding>
): Boolean {
return policyRules.any { code ->
return policyRules.find { code ->
researchAllowedPolicySystem.equals(code.getSystem()) && (researchAllowedPolicyOid.equals(
code.getCode()
))
}
} != null
}
fun isRequestDateInRange(requestDate: Date?, provPeriod: Period): Boolean {

View File

@@ -52,8 +52,7 @@
<td th:if="${request.status.value.contains('success')}" class="bg-green"><small>[[ ${request.status} ]]</small></td>
<td th:if="${request.status.value.contains('warning')}" class="bg-yellow"><small>[[ ${request.status} ]]</small></td>
<td th:if="${request.status.value.contains('error')}" class="bg-red"><small>[[ ${request.status} ]]</small></td>
<td th:if="${request.status.value == 'unknown' and not request.isPendingUnknown()}" class="bg-gray"><small>[[ ${request.status} ]]</small></td>
<td th:if="${request.status.value == 'unknown' and request.isPendingUnknown()}" class="bg-yellow"><small>⏰ [[ ${request.status} ]] ⏰</small></td>
<td th:if="${request.status.value == 'unknown'}" class="bg-gray"><small>[[ ${request.status} ]]</small></td>
<td th:if="${request.status.value == 'duplication'}" class="bg-gray"><small>[[ ${request.status} ]]</small></td>
<td th:if="${request.status.value == 'no-consent'}" class="bg-blue"><small>[[ ${request.status} ]]</small></td>
<td th:style="${request.type.value == 'delete'} ? 'color: red;'"><small>[[ ${request.type} ]]</small></td>

View File

@@ -24,7 +24,6 @@ import java.time.Instant;
import java.util.Date;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withServerError;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
@@ -178,8 +177,5 @@ class GicsConsentServiceTest {
);
}
@Test
public void convertGicsResultToMiiBroadConsent() {
fail("todo: implement Test gicsConsentService.convertGicsResultToMiiBroadConsent");
}
}

View File

@@ -80,7 +80,7 @@ class ConsentProcessorTest {
val checkResult = consentProcessor.consentGatedCheckAndTryEmbedding(inputMtb)
assertThat(checkResult).isTrue
assertThat(inputMtb.metadata.researchConsents).isNotEmpty
assertThat(inputMtb.metadata.researchConsents).hasSize(26)
}
companion object {