diff options
5 files changed, 69 insertions, 55 deletions
diff --git a/build.gradle.kts b/build.gradle.kts index e9a2804..1aac3a0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,22 +7,22 @@ plugins { war id("org.springframework.boot") version "3.5.6" id("io.spring.dependency-management") version "1.1.7" - kotlin("jvm") version "1.9.25" - kotlin("plugin.spring") version "1.9.25" + kotlin("jvm") version "2.2.10" + kotlin("plugin.spring") version "2.2.10" jacoco } group = "dev.dnpm" -version = "0.11.1" +version = "0.12.0-SNAPSHOT" var versions = mapOf( "mtb-dto" to "0.1.0-SNAPSHOT", - "hapi-fhir" to "7.6.1", - "mockito-kotlin" to "5.4.0", - "archunit" to "1.3.0", + "hapi-fhir" to "8.4.0", + "mockito-kotlin" to "6.0.0", + "archunit" to "1.4.1", // Webjars "webjars-locator" to "0.52", - "echarts" to "5.4.3", + "echarts" to "6.0.0", "htmx.org" to "1.9.12" ) diff --git a/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java b/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java index 6a2b947..ea0d20f 100644 --- a/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java +++ b/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java @@ -23,8 +23,6 @@ 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; @@ -39,9 +37,11 @@ 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 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"; + 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"; 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, createOrGetPsn); + final var responseEntity = getGpasPseudonym(requestBody, CREATE_OR_GET_PSN); 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, createMultiDomainPsn); + final var responseEntity = getGpasPseudonym(requestBody, CREATE_MULTI_DOMAIN_PSN); final var gPasPseudonymResult = (Parameters) r4Context.newJsonParser() .parseResource(responseEntity.getBody()); @@ -150,23 +150,22 @@ public class GpasPseudonymGenerator implements Generator { log.debug("API request succeeded. Response: {}", responseEntity.getStatusCode()); return responseEntity; } - } catch (RestClientException rce) { - if (rce instanceof BadRequest) { - String msg = "gPas or request configuration is incorrect. Please check both." - + rce.getMessage(); - log.debug( - msg); - throw new PseudonymRequestFailed(msg, rce); - } - 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) { + } catch (BadRequest e) { + 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); + } + 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."); diff --git a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt index 36c9705..f2509dd 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt @@ -30,6 +30,7 @@ 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") @@ -65,6 +66,12 @@ 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 @@ -90,19 +97,23 @@ 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 (" + - "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;") + @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;" + ) 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 (" + - "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;") + @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;" + ) fun findPatientUniqueDeleteStates(): List<CountedState> -}
\ No newline at end of file +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt index 2ed21eb..381f38a 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt @@ -137,15 +137,7 @@ class ConsentProcessor( } val provisionComponent: ProvisionComponent = provisions.first() - - 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 - } - } - + val provisionCode = getProvisionCode(provisionComponent) if (provisionCode != null) { try { val modelProjectConsentPurpose = @@ -177,6 +169,17 @@ 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 @@ -238,9 +241,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().firstOrNull { code -> + subProvision.code.map { c -> c.coding }.flatten().any { code -> targetCode.equals(code.code) && targetSystem.equals(code.system) - } != null + } }.map { subProvision -> subProvision } @@ -257,11 +260,11 @@ class ConsentProcessor( researchAllowedPolicySystem: String?, policyRules: Collection<Coding> ): Boolean { - return policyRules.find { code -> + return policyRules.any { code -> researchAllowedPolicySystem.equals(code.getSystem()) && (researchAllowedPolicyOid.equals( code.getCode() )) - } != null + } } fun isRequestDateInRange(requestDate: Date?, provPeriod: Period): Boolean { diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index f48e3dc..a419dda 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -52,7 +52,8 @@ <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'}" class="bg-gray"><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 == '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> |
