summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build.gradle.kts14
-rw-r--r--src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java49
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt29
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt29
-rw-r--r--src/main/resources/templates/index.html3
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>