summaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'src/main')
-rw-r--r--src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java122
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt120
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppJdbcConfiguration.kt25
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt6
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt5
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt107
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/monitoring/ReportService.kt3
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt38
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt3
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/output/MtbFileSender.kt5
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/security/TokenService.kt (renamed from src/main/kotlin/dev/dnpm/etl/processor/services/TokenService.kt)2
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/security/UserRoleService.kt (renamed from src/main/kotlin/dev/dnpm/etl/processor/services/UserRoleService.kt)5
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt49
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/services/RequestService.kt26
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/services/ResponseProcessor.kt10
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/services/kafka/KafkaResponseProcessor.kt3
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/types.kt41
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt16
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt12
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsRestController.kt14
-rw-r--r--src/main/resources/application-dev.yml35
-rw-r--r--src/main/resources/static/style.css10
-rw-r--r--src/main/resources/templates/configs/gPasConnectionAvailable.html15
-rw-r--r--src/main/resources/templates/configs/outputConnectionAvailable.html42
-rw-r--r--src/main/resources/templates/index.html6
25 files changed, 467 insertions, 253 deletions
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 446bd16..77caa77 100644
--- a/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java
+++ b/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java
@@ -23,41 +23,17 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.IParser;
import dev.dnpm.etl.processor.config.GPasConfigProperties;
import org.apache.commons.lang3.StringUtils;
-import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
-import org.apache.hc.client5.http.impl.classic.HttpClients;
-import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager;
-import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
-import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
-import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
-import org.apache.hc.core5.http.config.Registry;
-import org.apache.hc.core5.http.config.RegistryBuilder;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
import org.hl7.fhir.r4.model.StringType;
import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.*;
-import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.web.client.RestTemplate;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.TrustManagerFactory;
-import java.io.BufferedInputStream;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.security.KeyManagementException;
-import java.security.KeyStore;
-import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
-import java.security.cert.CertificateException;
-import java.security.cert.CertificateFactory;
-import java.security.cert.X509Certificate;
-import java.util.Base64;
-
public class GpasPseudonymGenerator implements Generator {
private final static FhirContext r4Context = FhirContext.forR4();
@@ -69,27 +45,13 @@ public class GpasPseudonymGenerator implements Generator {
private final RestTemplate restTemplate;
- private SSLContext customSslContext;
-
- public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate) {
+ public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate, RestTemplate restTemplate) {
this.retryTemplate = retryTemplate;
- this.restTemplate = getRestTemplete();
-
+ this.restTemplate = restTemplate;
this.gPasUrl = gpasCfg.getUri();
this.psnTargetDomain = gpasCfg.getTarget();
httpHeader = getHttpHeaders(gpasCfg.getUsername(), gpasCfg.getPassword());
- try {
- if (StringUtils.isNotBlank(gpasCfg.getSslCaLocation())) {
- customSslContext = getSslContext(gpasCfg.getSslCaLocation());
- log.warn(String.format("%s has been initialized with SSL certificate %s. This is deprecated in favor of including Root CA.",
- this.getClass().getName(), gpasCfg.getSslCaLocation()));
- }
- } catch (IOException | KeyManagementException | KeyStoreException | CertificateException |
- NoSuchAlgorithmException e) {
- throw new RuntimeException(e);
- }
-
log.debug(String.format("%s has been initialized", this.getClass().getName()));
}
@@ -99,7 +61,7 @@ public class GpasPseudonymGenerator implements Generator {
var gPasRequestBody = getGpasRequestBody(id);
var responseEntity = getGpasPseudonym(gPasRequestBody);
var gPasPseudonymResult = (Parameters) r4Context.newJsonParser()
- .parseResource(responseEntity.getBody());
+ .parseResource(responseEntity.getBody());
return unwrapPseudonym(gPasPseudonymResult);
}
@@ -113,9 +75,9 @@ public class GpasPseudonymGenerator implements Generator {
}
final var identifier = (Identifier) parameters.get().getPart().stream()
- .filter(a -> a.getName().equals("pseudonym"))
- .findFirst()
- .orElseGet(ParametersParameterComponent::new).getValue();
+ .filter(a -> a.getName().equals("pseudonym"))
+ .findFirst()
+ .orElseGet(ParametersParameterComponent::new).getValue();
// pseudonym
return sanitizeValue(identifier.getValue());
@@ -144,8 +106,8 @@ public class GpasPseudonymGenerator implements Generator {
try {
responseEntity = retryTemplate.execute(
- ctx -> restTemplate.exchange(gPasUrl, HttpMethod.POST, requestEntity,
- String.class));
+ ctx -> restTemplate.exchange(gPasUrl, HttpMethod.POST, requestEntity,
+ String.class));
if (responseEntity.getStatusCode().is2xxSuccessful()) {
log.debug("API request succeeded. Response: {}", responseEntity.getStatusCode());
@@ -157,16 +119,16 @@ public class GpasPseudonymGenerator implements Generator {
return responseEntity;
} catch (Exception unexpected) {
throw new PseudonymRequestFailed(
- "API request due unexpected error unsuccessful gPas unsuccessful.", unexpected);
+ "API request due unexpected error unsuccessful gPas unsuccessful.", unexpected);
}
}
protected String getGpasRequestBody(String id) {
var requestParameters = new Parameters();
requestParameters.addParameter().setName("target")
- .setValue(new StringType().setValue(psnTargetDomain));
+ .setValue(new StringType().setValue(psnTargetDomain));
requestParameters.addParameter().setName("original")
- .setValue(new StringType().setValue(id));
+ .setValue(new StringType().setValue(id));
final IParser iParser = r4Context.newJsonParser();
return iParser.encodeResourceToString(requestParameters);
}
@@ -180,67 +142,7 @@ public class GpasPseudonymGenerator implements Generator {
return headers;
}
- String authHeader = gPasUserName + ":" + gPasPassword;
- byte[] authHeaderBytes = authHeader.getBytes();
- byte[] encodedAuthHeaderBytes = Base64.getEncoder().encode(authHeaderBytes);
- String encodedAuthHeader = new String(encodedAuthHeaderBytes);
-
- if (StringUtils.isNotBlank(gPasUserName) && StringUtils.isNotBlank(gPasPassword)) {
- headers.set("Authorization", "Basic " + encodedAuthHeader);
- }
-
+ headers.setBasicAuth(gPasUserName, gPasPassword);
return headers;
}
-
- /**
- * Read SSL root certificate and return SSLContext
- *
- * @param certificateLocation file location to root certificate (PEM)
- * @return initialized SSLContext
- * @throws IOException file cannot be read
- * @throws CertificateException in case we have an invalid certificate of type X.509
- * @throws KeyStoreException keystore cannot be initialized
- * @throws NoSuchAlgorithmException missing trust manager algorithmus
- * @throws KeyManagementException key management failed at init SSLContext
- */
- @Nullable
- protected SSLContext getSslContext(String certificateLocation)
- throws IOException, CertificateException, KeyStoreException, KeyManagementException, NoSuchAlgorithmException {
-
- KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
-
- FileInputStream fis = new FileInputStream(certificateLocation);
- X509Certificate ca = (X509Certificate) CertificateFactory.getInstance("X.509")
- .generateCertificate(new BufferedInputStream(fis));
-
- ks.load(null, null);
- ks.setCertificateEntry(Integer.toString(1), ca);
-
- TrustManagerFactory tmf = TrustManagerFactory.getInstance(
- TrustManagerFactory.getDefaultAlgorithm());
- tmf.init(ks);
-
- SSLContext sslContext = SSLContext.getInstance("TLS");
- sslContext.init(null, tmf.getTrustManagers(), null);
-
- return sslContext;
- }
-
- protected RestTemplate getRestTemplete() {
- if (customSslContext == null) {
- return new RestTemplate();
- }
- final var sslsf = new SSLConnectionSocketFactory(customSslContext);
- final Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
- .register("https", sslsf).register("http", new PlainConnectionSocketFactory()).build();
-
- final BasicHttpClientConnectionManager connectionManager = new BasicHttpClientConnectionManager(
- socketFactoryRegistry);
- final CloseableHttpClient httpClient = HttpClients.custom()
- .setConnectionManager(connectionManager).build();
-
- final HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(
- httpClient);
- return new RestTemplate(requestFactory);
- }
}
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt
index 0ae2c2f..5fc1120 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt
@@ -20,21 +20,32 @@
package dev.dnpm.etl.processor.config
import com.fasterxml.jackson.databind.ObjectMapper
-import dev.dnpm.etl.processor.monitoring.*
+import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult
+import dev.dnpm.etl.processor.monitoring.ConnectionCheckService
+import dev.dnpm.etl.processor.monitoring.GPasConnectionCheckService
+import dev.dnpm.etl.processor.monitoring.ReportService
import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator
import dev.dnpm.etl.processor.pseudonym.Generator
import dev.dnpm.etl.processor.pseudonym.GpasPseudonymGenerator
import dev.dnpm.etl.processor.pseudonym.PseudonymizeService
-import dev.dnpm.etl.processor.services.TokenRepository
-import dev.dnpm.etl.processor.services.TokenService
+import dev.dnpm.etl.processor.security.TokenRepository
+import dev.dnpm.etl.processor.security.TokenService
import dev.dnpm.etl.processor.services.Transformation
import dev.dnpm.etl.processor.services.TransformationService
+import org.apache.hc.client5.http.impl.classic.HttpClients
+import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager
+import org.apache.hc.client5.http.socket.ConnectionSocketFactory
+import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory
+import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory
+import org.apache.hc.core5.http.config.RegistryBuilder
import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
+import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration
+import org.springframework.http.client.HttpComponentsClientHttpRequestFactory
import org.springframework.retry.RetryCallback
import org.springframework.retry.RetryContext
import org.springframework.retry.RetryListener
@@ -46,6 +57,13 @@ import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.provisioning.InMemoryUserDetailsManager
import org.springframework.web.client.RestTemplate
import reactor.core.publisher.Sinks
+import java.io.BufferedInputStream
+import java.io.FileInputStream
+import java.security.KeyStore
+import java.security.cert.CertificateFactory
+import java.security.cert.X509Certificate
+import javax.net.ssl.SSLContext
+import javax.net.ssl.TrustManagerFactory
import kotlin.time.Duration.Companion.seconds
import kotlin.time.toJavaDuration
@@ -70,8 +88,20 @@ class AppConfiguration {
@ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS")
@Bean
- fun gpasPseudonymGenerator(configProperties: GPasConfigProperties, retryTemplate: RetryTemplate): Generator {
- return GpasPseudonymGenerator(configProperties, retryTemplate)
+ fun gpasPseudonymGenerator(configProperties: GPasConfigProperties, retryTemplate: RetryTemplate, restTemplate: RestTemplate): Generator {
+ try {
+ if (!configProperties.sslCaLocation.isNullOrBlank()) {
+ return GpasPseudonymGenerator(
+ configProperties,
+ retryTemplate,
+ createCustomGpasRestTemplate(configProperties)
+ )
+ }
+ } catch (e: Exception) {
+ throw RuntimeException(e)
+ }
+
+ return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate)
}
@ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "BUILDIN", matchIfMissing = true)
@@ -83,8 +113,80 @@ class AppConfiguration {
@ConditionalOnProperty(value = ["app.pseudonymizer"], havingValue = "GPAS")
@ConditionalOnMissingBean
@Bean
- fun gpasPseudonymGeneratorOnDeprecatedProperty(configProperties: GPasConfigProperties, retryTemplate: RetryTemplate): Generator {
- return GpasPseudonymGenerator(configProperties, retryTemplate)
+ fun gpasPseudonymGeneratorOnDeprecatedProperty(configProperties: GPasConfigProperties, retryTemplate: RetryTemplate, restTemplate: RestTemplate): Generator {
+ try {
+ if (!configProperties.sslCaLocation.isNullOrBlank()) {
+ return GpasPseudonymGenerator(
+ configProperties,
+ retryTemplate,
+ createCustomGpasRestTemplate(configProperties)
+ )
+ }
+ } catch (e: Exception) {
+ throw RuntimeException(e)
+ }
+
+ return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate)
+ }
+
+ private fun createCustomGpasRestTemplate(configProperties: GPasConfigProperties): RestTemplate {
+ fun getSslContext(certificateLocation: String): SSLContext? {
+ val ks = KeyStore.getInstance(KeyStore.getDefaultType())
+
+ val fis = FileInputStream(certificateLocation)
+ val ca = CertificateFactory.getInstance("X.509")
+ .generateCertificate(BufferedInputStream(fis)) as X509Certificate
+
+ ks.load(null, null)
+ ks.setCertificateEntry(1.toString(), ca)
+
+ val tmf = TrustManagerFactory.getInstance(
+ TrustManagerFactory.getDefaultAlgorithm()
+ )
+ tmf.init(ks)
+
+ val sslContext = SSLContext.getInstance("TLS")
+ sslContext.init(null, tmf.trustManagers, null)
+
+ return sslContext
+ }
+
+ fun getCustomRestTemplate(customSslContext: SSLContext): RestTemplate {
+ val sslsf = SSLConnectionSocketFactory(customSslContext)
+ val socketFactoryRegistry = RegistryBuilder.create<ConnectionSocketFactory>()
+ .register("https", sslsf).register("http", PlainConnectionSocketFactory()).build()
+
+ val connectionManager = BasicHttpClientConnectionManager(
+ socketFactoryRegistry
+ )
+ val httpClient = HttpClients.custom()
+ .setConnectionManager(connectionManager).build()
+
+ val requestFactory = HttpComponentsClientHttpRequestFactory(
+ httpClient
+ )
+ return RestTemplate(requestFactory)
+ }
+
+ try {
+ if (!configProperties.sslCaLocation.isNullOrBlank()) {
+ val customSslContext = getSslContext(configProperties.sslCaLocation)
+ logger.warn(
+ String.format(
+ "%s has been initialized with SSL certificate %s. This is deprecated in favor of including Root CA.",
+ this.javaClass.name, configProperties.sslCaLocation
+ )
+ )
+
+ if (customSslContext != null) {
+ return getCustomRestTemplate(customSslContext)
+ }
+ }
+ } catch (e: Exception) {
+ throw RuntimeException(e)
+ }
+
+ throw RuntimeException("Custom SSL configuration for gPAS not usable")
}
@ConditionalOnProperty(value = ["app.pseudonymizer"], havingValue = "BUILDIN")
@@ -173,5 +275,9 @@ class AppConfiguration {
return GPasConnectionCheckService(restTemplate, gPasConfigProperties, connectionCheckUpdateProducer)
}
+ @Bean
+ fun jdbcConfiguration(): AbstractJdbcConfiguration {
+ return AppJdbcConfiguration()
+ }
}
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppJdbcConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppJdbcConfiguration.kt
new file mode 100644
index 0000000..898982c
--- /dev/null
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppJdbcConfiguration.kt
@@ -0,0 +1,25 @@
+package dev.dnpm.etl.processor.config
+
+import dev.dnpm.etl.processor.Fingerprint
+import org.springframework.context.annotation.Configuration
+import org.springframework.core.convert.converter.Converter
+import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration
+
+@Configuration
+class AppJdbcConfiguration : AbstractJdbcConfiguration() {
+ override fun userConverters(): MutableList<*> {
+ return mutableListOf(StringToFingerprintConverter(), FingerprintToStringConverter())
+ }
+}
+
+class StringToFingerprintConverter : Converter<String, Fingerprint> {
+ override fun convert(source: String): Fingerprint {
+ return Fingerprint(source)
+ }
+}
+
+class FingerprintToStringConverter : Converter<Fingerprint, String> {
+ override fun convert(source: Fingerprint): String {
+ return source.value
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt
index c377555..6b063bd 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt
@@ -21,7 +21,7 @@ package dev.dnpm.etl.processor.config
import dev.dnpm.etl.processor.security.UserRole
import dev.dnpm.etl.processor.security.UserRoleRepository
-import dev.dnpm.etl.processor.services.UserRoleService
+import dev.dnpm.etl.processor.security.UserRoleService
import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.context.properties.EnableConfigurationProperties
@@ -89,7 +89,7 @@ class AppSecurityConfiguration(
http {
authorizeRequests {
authorize("/configs/**", hasRole("ADMIN"))
- authorize("/mtbfile/**", hasAnyRole("MTBFILE"))
+ authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN", "USER"))
authorize("/report/**", hasAnyRole("ADMIN", "USER"))
authorize("*.css", permitAll)
authorize("*.ico", permitAll)
@@ -147,7 +147,7 @@ class AppSecurityConfiguration(
http {
authorizeRequests {
authorize("/configs/**", hasRole("ADMIN"))
- authorize("/mtbfile/**", hasAnyRole("MTBFILE"))
+ authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN"))
authorize("/report/**", hasRole("ADMIN"))
authorize(anyRequest, permitAll)
}
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt b/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt
index de901ce..b72b1fd 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt
@@ -22,6 +22,7 @@ package dev.dnpm.etl.processor.input
import com.fasterxml.jackson.databind.ObjectMapper
import de.ukw.ccc.bwhc.dto.Consent
import de.ukw.ccc.bwhc.dto.MtbFile
+import dev.dnpm.etl.processor.RequestId
import dev.dnpm.etl.processor.services.RequestProcessor
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.slf4j.LoggerFactory
@@ -37,9 +38,9 @@ class KafkaInputListener(
val mtbFile = objectMapper.readValue(data.value(), MtbFile::class.java)
val firstRequestIdHeader = data.headers().headers("requestId")?.firstOrNull()
val requestId = if (null != firstRequestIdHeader) {
- String(firstRequestIdHeader.value())
+ RequestId(String(firstRequestIdHeader.value()))
} else {
- ""
+ RequestId("")
}
if (mtbFile.consent.status == Consent.Status.ACTIVE) {
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt
index 81ad922..1afaa32 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt
@@ -26,22 +26,18 @@ import jakarta.annotation.PostConstruct
import org.apache.kafka.clients.consumer.Consumer
import org.apache.kafka.common.errors.TimeoutException
import org.springframework.beans.factory.annotation.Qualifier
-import org.springframework.http.HttpEntity
-import org.springframework.http.HttpHeaders
-import org.springframework.http.HttpMethod
-import org.springframework.http.HttpStatus
-import org.springframework.http.MediaType
-import org.springframework.http.RequestEntity
+import org.springframework.http.*
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.web.client.RestTemplate
import org.springframework.web.util.UriComponentsBuilder
import reactor.core.publisher.Sinks
+import java.time.Instant
import kotlin.time.Duration.Companion.seconds
import kotlin.time.toJavaDuration
interface ConnectionCheckService {
- fun connectionAvailable(): Boolean
+ fun connectionAvailable(): ConnectionCheckResult
}
@@ -51,9 +47,27 @@ sealed class ConnectionCheckResult {
abstract val available: Boolean
- data class KafkaConnectionCheckResult(override val available: Boolean) : ConnectionCheckResult()
- data class RestConnectionCheckResult(override val available: Boolean) : ConnectionCheckResult()
- data class GPasConnectionCheckResult(override val available: Boolean) : ConnectionCheckResult()
+ abstract val timestamp: Instant
+
+ abstract val lastChange: Instant
+
+ data class KafkaConnectionCheckResult(
+ override val available: Boolean,
+ override val timestamp: Instant,
+ override val lastChange: Instant
+ ) : ConnectionCheckResult()
+
+ data class RestConnectionCheckResult(
+ override val available: Boolean,
+ override val timestamp: Instant,
+ override val lastChange: Instant
+ ) : ConnectionCheckResult()
+
+ data class GPasConnectionCheckResult(
+ override val available: Boolean,
+ override val timestamp: Instant,
+ override val lastChange: Instant
+ ) : ConnectionCheckResult()
}
class KafkaConnectionCheckService(
@@ -62,25 +76,33 @@ class KafkaConnectionCheckService(
private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
) : OutputConnectionCheckService {
- private var connectionAvailable: Boolean = false
-
+ private var result = ConnectionCheckResult.KafkaConnectionCheckResult(false, Instant.now(), Instant.now())
@PostConstruct
@Scheduled(cron = "0 * * * * *")
fun check() {
- connectionAvailable = try {
- null != consumer.listTopics(5.seconds.toJavaDuration())
+ result = try {
+ val available = null != consumer.listTopics(5.seconds.toJavaDuration())
+ ConnectionCheckResult.KafkaConnectionCheckResult(
+ available,
+ Instant.now(),
+ if (result.available == available) { result.lastChange } else { Instant.now() }
+ )
} catch (e: TimeoutException) {
- false
+ ConnectionCheckResult.KafkaConnectionCheckResult(
+ false,
+ Instant.now(),
+ if (!result.available) { result.lastChange } else { Instant.now() }
+ )
}
connectionCheckUpdateProducer.emitNext(
- ConnectionCheckResult.KafkaConnectionCheckResult(connectionAvailable),
+ result,
Sinks.EmitFailureHandler.FAIL_FAST
)
}
- override fun connectionAvailable(): Boolean {
- return this.connectionAvailable
+ override fun connectionAvailable(): ConnectionCheckResult.KafkaConnectionCheckResult {
+ return this.result
}
}
@@ -92,27 +114,37 @@ class RestConnectionCheckService(
private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
) : OutputConnectionCheckService {
- private var connectionAvailable: Boolean = false
+ private var result = ConnectionCheckResult.RestConnectionCheckResult(false, Instant.now(), Instant.now())
@PostConstruct
@Scheduled(cron = "0 * * * * *")
fun check() {
- connectionAvailable = try {
- restTemplate.getForEntity(
+ result = try {
+ val available = restTemplate.getForEntity(
restTargetProperties.uri?.replace("/etl/api", "").toString(),
String::class.java
).statusCode == HttpStatus.OK
+
+ ConnectionCheckResult.RestConnectionCheckResult(
+ available,
+ Instant.now(),
+ if (result.available == available) { result.lastChange } else { Instant.now() }
+ )
} catch (e: Exception) {
- false
+ ConnectionCheckResult.RestConnectionCheckResult(
+ false,
+ Instant.now(),
+ if (!result.available) { result.lastChange } else { Instant.now() }
+ )
}
connectionCheckUpdateProducer.emitNext(
- ConnectionCheckResult.RestConnectionCheckResult(connectionAvailable),
+ result,
Sinks.EmitFailureHandler.FAIL_FAST
)
}
- override fun connectionAvailable(): Boolean {
- return this.connectionAvailable
+ override fun connectionAvailable(): ConnectionCheckResult.RestConnectionCheckResult {
+ return this.result
}
}
@@ -123,12 +155,12 @@ class GPasConnectionCheckService(
private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
) : ConnectionCheckService {
- private var connectionAvailable: Boolean = false
+ private var result = ConnectionCheckResult.GPasConnectionCheckResult(false, Instant.now(), Instant.now())
@PostConstruct
@Scheduled(cron = "0 * * * * *")
fun check() {
- connectionAvailable = try {
+ result = try {
val uri = UriComponentsBuilder.fromUriString(
gPasConfigProperties.uri?.replace("/\$pseudonymizeAllowCreate", "/\$pseudonymize").toString()
)
@@ -141,22 +173,33 @@ class GPasConnectionCheckService(
if (!gPasConfigProperties.username.isNullOrBlank() && !gPasConfigProperties.password.isNullOrBlank()) {
headers.setBasicAuth(gPasConfigProperties.username, gPasConfigProperties.password)
}
- restTemplate.exchange(
+
+ val available = restTemplate.exchange(
uri,
HttpMethod.GET,
HttpEntity<Void>(headers),
Void::class.java
).statusCode == HttpStatus.OK
+
+ ConnectionCheckResult.GPasConnectionCheckResult(
+ available,
+ Instant.now(),
+ if (result.available == available) { result.lastChange } else { Instant.now() }
+ )
} catch (e: Exception) {
- false
+ ConnectionCheckResult.GPasConnectionCheckResult(
+ false,
+ Instant.now(),
+ if (!result.available) { result.lastChange } else { Instant.now() }
+ )
}
connectionCheckUpdateProducer.emitNext(
- ConnectionCheckResult.GPasConnectionCheckResult(connectionAvailable),
+ result,
Sinks.EmitFailureHandler.FAIL_FAST
)
}
- override fun connectionAvailable(): Boolean {
- return this.connectionAvailable
+ override fun connectionAvailable(): ConnectionCheckResult.GPasConnectionCheckResult {
+ return this.result
}
} \ No newline at end of file
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ReportService.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ReportService.kt
index 97ecd05..062f749 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ReportService.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ReportService.kt
@@ -19,6 +19,7 @@
package dev.dnpm.etl.processor.monitoring
+import com.fasterxml.jackson.annotation.JsonAlias
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonValue
import com.fasterxml.jackson.core.JsonParseException
@@ -54,7 +55,7 @@ class ReportService(
private data class DataQualityReport(val issues: List<Issue>)
@JsonIgnoreProperties(ignoreUnknown = true)
- data class Issue(val severity: Severity, val message: String)
+ data class Issue(val severity: Severity, @JsonAlias("details") val message: String)
enum class Severity(@JsonValue val value: String) {
FATAL("fatal"),
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 028b4a3..9efae4c 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt
@@ -19,10 +19,14 @@
package dev.dnpm.etl.processor.monitoring
+import dev.dnpm.etl.processor.Fingerprint
+import dev.dnpm.etl.processor.randomRequestId
+import dev.dnpm.etl.processor.RequestId
import org.springframework.data.annotation.Id
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.data.jdbc.repository.query.Query
+import org.springframework.data.relational.core.mapping.Column
import org.springframework.data.relational.core.mapping.Embedded
import org.springframework.data.relational.core.mapping.Table
import org.springframework.data.repository.CrudRepository
@@ -30,26 +34,48 @@ import org.springframework.data.repository.PagingAndSortingRepository
import java.time.Instant
import java.util.*
-typealias RequestId = UUID
-
@Table("request")
data class Request(
@Id val id: Long? = null,
- val uuid: String = RequestId.randomUUID().toString(),
+ val uuid: RequestId = randomRequestId(),
val patientId: String,
val pid: String,
- val fingerprint: String,
+ @Column("fingerprint")
+ val fingerprint: Fingerprint,
val type: RequestType,
var status: RequestStatus,
var processedAt: Instant = Instant.now(),
@Embedded.Nullable var report: Report? = null
-)
+) {
+ constructor(
+ uuid: RequestId,
+ patientId: String,
+ pid: String,
+ fingerprint: Fingerprint,
+ type: RequestType,
+ status: RequestStatus
+ ) :
+ this(null, uuid, patientId, pid, fingerprint, type, status, Instant.now())
+
+ constructor(
+ uuid: RequestId,
+ patientId: String,
+ pid: String,
+ fingerprint: Fingerprint,
+ type: RequestType,
+ status: RequestStatus,
+ processedAt: Instant
+ ) :
+ this(null, uuid, patientId, pid, fingerprint, type, status, processedAt)
+}
+@JvmRecord
data class Report(
val description: String,
val dataQualityReport: String = ""
)
+@JvmRecord
data class CountedState(
val count: Int,
val status: RequestStatus,
@@ -59,7 +85,7 @@ interface RequestRepository : CrudRepository<Request, Long>, PagingAndSortingRep
fun findAllByPatientIdOrderByProcessedAtDesc(patientId: String): List<Request>
- fun findByUuidEquals(uuid: String): Optional<Request>
+ fun findByUuidEquals(uuid: RequestId): Optional<Request>
fun findRequestByPatientId(patientId: String, pageable: Pageable): Page<Request>
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt b/src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt
index fc5d617..7b777e8 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt
@@ -22,6 +22,7 @@ package dev.dnpm.etl.processor.output
import com.fasterxml.jackson.databind.ObjectMapper
import de.ukw.ccc.bwhc.dto.Consent
import de.ukw.ccc.bwhc.dto.MtbFile
+import dev.dnpm.etl.processor.RequestId
import dev.dnpm.etl.processor.config.KafkaProperties
import dev.dnpm.etl.processor.monitoring.RequestStatus
import org.slf4j.LoggerFactory
@@ -101,5 +102,5 @@ class KafkaMtbFileSender(
return "{\"pid\": \"${request.patientId}\"}"
}
- data class Data(val requestId: String, val content: MtbFile)
+ data class Data(val requestId: RequestId, val content: MtbFile)
} \ No newline at end of file
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/output/MtbFileSender.kt b/src/main/kotlin/dev/dnpm/etl/processor/output/MtbFileSender.kt
index aca972b..2670f2e 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/output/MtbFileSender.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/output/MtbFileSender.kt
@@ -20,6 +20,7 @@
package dev.dnpm.etl.processor.output
import de.ukw.ccc.bwhc.dto.MtbFile
+import dev.dnpm.etl.processor.RequestId
import dev.dnpm.etl.processor.monitoring.RequestStatus
import org.springframework.http.HttpStatusCode
@@ -32,9 +33,9 @@ interface MtbFileSender {
data class Response(val status: RequestStatus, val body: String = "")
- data class MtbFileRequest(val requestId: String, val mtbFile: MtbFile)
+ data class MtbFileRequest(val requestId: RequestId, val mtbFile: MtbFile)
- data class DeleteRequest(val requestId: String, val patientId: String)
+ data class DeleteRequest(val requestId: RequestId, val patientId: String)
}
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/TokenService.kt b/src/main/kotlin/dev/dnpm/etl/processor/security/TokenService.kt
index f084408..44b04e8 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/services/TokenService.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/security/TokenService.kt
@@ -17,7 +17,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package dev.dnpm.etl.processor.services
+package dev.dnpm.etl.processor.security
import jakarta.annotation.PostConstruct
import org.springframework.data.annotation.Id
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/UserRoleService.kt b/src/main/kotlin/dev/dnpm/etl/processor/security/UserRoleService.kt
index 6649f7d..174f8a9 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/services/UserRoleService.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/security/UserRoleService.kt
@@ -17,11 +17,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package dev.dnpm.etl.processor.services
+package dev.dnpm.etl.processor.security
-import dev.dnpm.etl.processor.security.Role
-import dev.dnpm.etl.processor.security.UserRole
-import dev.dnpm.etl.processor.security.UserRoleRepository
import org.springframework.data.repository.findByIdOrNull
import org.springframework.security.core.session.SessionRegistry
import org.springframework.security.oauth2.core.oidc.user.OidcUser
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt
index bdf07cb..94598ae 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt
@@ -21,6 +21,9 @@ package dev.dnpm.etl.processor.services
import com.fasterxml.jackson.databind.ObjectMapper
import de.ukw.ccc.bwhc.dto.MtbFile
+import dev.dnpm.etl.processor.Fingerprint
+import dev.dnpm.etl.processor.randomRequestId
+import dev.dnpm.etl.processor.RequestId
import dev.dnpm.etl.processor.config.AppConfigProperties
import dev.dnpm.etl.processor.monitoring.Report
import dev.dnpm.etl.processor.monitoring.Request
@@ -49,10 +52,10 @@ class RequestProcessor(
) {
fun processMtbFile(mtbFile: MtbFile) {
- processMtbFile(mtbFile, UUID.randomUUID().toString())
+ processMtbFile(mtbFile, randomRequestId())
}
- fun processMtbFile(mtbFile: MtbFile, requestId: String) {
+ fun processMtbFile(mtbFile: MtbFile, requestId: RequestId) {
val pid = mtbFile.patient.id
mtbFile pseudonymizeWith pseudonymizeService
@@ -62,12 +65,12 @@ class RequestProcessor(
requestService.save(
Request(
- uuid = requestId,
- patientId = request.mtbFile.patient.id,
- pid = pid,
- fingerprint = fingerprint(request.mtbFile),
- status = RequestStatus.UNKNOWN,
- type = RequestType.MTB_FILE
+ requestId,
+ request.mtbFile.patient.id,
+ pid,
+ fingerprint(request.mtbFile),
+ RequestType.MTB_FILE,
+ RequestStatus.UNKNOWN
)
)
@@ -108,21 +111,21 @@ class RequestProcessor(
}
fun processDeletion(patientId: String) {
- processDeletion(patientId, UUID.randomUUID().toString())
+ processDeletion(patientId, randomRequestId())
}
- fun processDeletion(patientId: String, requestId: String) {
+ fun processDeletion(patientId: String, requestId: RequestId) {
try {
val patientPseudonym = pseudonymizeService.patientPseudonym(patientId)
requestService.save(
Request(
- uuid = requestId,
- patientId = patientPseudonym,
- pid = patientId,
- fingerprint = fingerprint(patientPseudonym),
- status = RequestStatus.UNKNOWN,
- type = RequestType.DELETE
+ requestId,
+ patientPseudonym,
+ patientId,
+ fingerprint(patientPseudonym),
+ RequestType.DELETE,
+ RequestStatus.UNKNOWN
)
)
@@ -146,7 +149,7 @@ class RequestProcessor(
uuid = requestId,
patientId = "???",
pid = patientId,
- fingerprint = "",
+ fingerprint = Fingerprint.empty(),
status = RequestStatus.ERROR,
type = RequestType.DELETE,
report = Report("Fehler bei der Pseudonymisierung")
@@ -155,14 +158,16 @@ class RequestProcessor(
}
}
- private fun fingerprint(mtbFile: MtbFile): String {
+ private fun fingerprint(mtbFile: MtbFile): Fingerprint {
return fingerprint(objectMapper.writeValueAsString(mtbFile))
}
- private fun fingerprint(s: String): String {
- return Base32().encodeAsString(DigestUtils.sha256(s))
- .replace("=", "")
- .lowercase()
+ private fun fingerprint(s: String): Fingerprint {
+ return Fingerprint(
+ Base32().encodeAsString(DigestUtils.sha256(s))
+ .replace("=", "")
+ .lowercase()
+ )
}
} \ No newline at end of file
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestService.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestService.kt
index e0043d2..a2e8de3 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestService.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestService.kt
@@ -19,11 +19,12 @@
package dev.dnpm.etl.processor.services
-import dev.dnpm.etl.processor.monitoring.Request
-import dev.dnpm.etl.processor.monitoring.RequestRepository
-import dev.dnpm.etl.processor.monitoring.RequestStatus
-import dev.dnpm.etl.processor.monitoring.RequestType
+import dev.dnpm.etl.processor.RequestId
+import dev.dnpm.etl.processor.monitoring.*
+import org.springframework.data.domain.Page
+import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Service
+import java.util.*
@Service
class RequestService(
@@ -32,6 +33,15 @@ class RequestService(
fun save(request: Request) = requestRepository.save(request)
+ fun findAll(): Iterable<Request> = requestRepository.findAll()
+
+ fun findAll(pageable: Pageable): Page<Request> = requestRepository.findAll(pageable)
+
+ fun findByUuid(uuid: RequestId): Optional<Request> =
+ requestRepository.findByUuidEquals(uuid)
+
+ fun findRequestByPatientId(patientId: String, pageable: Pageable): Page<Request> = requestRepository.findRequestByPatientId(patientId, pageable)
+
fun allRequestsByPatientPseudonym(patientPseudonym: String) = requestRepository
.findAllByPatientIdOrderByProcessedAtDesc(patientPseudonym)
@@ -41,6 +51,14 @@ class RequestService(
fun isLastRequestWithKnownStatusDeletion(patientPseudonym: String) =
Companion.isLastRequestWithKnownStatusDeletion(allRequestsByPatientPseudonym(patientPseudonym))
+ fun countStates(): Iterable<CountedState> = requestRepository.countStates()
+
+ fun countDeleteStates(): Iterable<CountedState> = requestRepository.countDeleteStates()
+
+ fun findPatientUniqueStates(): List<CountedState> = requestRepository.findPatientUniqueStates()
+
+ fun findPatientUniqueDeleteStates(): List<CountedState> = requestRepository.findPatientUniqueDeleteStates()
+
companion object {
fun lastMtbFileRequestForPatientPseudonym(allRequests: List<Request>) = allRequests
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/ResponseProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/ResponseProcessor.kt
index 4048348..ecb2ec7 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/services/ResponseProcessor.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/services/ResponseProcessor.kt
@@ -19,8 +19,8 @@
package dev.dnpm.etl.processor.services
+import dev.dnpm.etl.processor.RequestId
import dev.dnpm.etl.processor.monitoring.Report
-import dev.dnpm.etl.processor.monitoring.RequestRepository
import dev.dnpm.etl.processor.monitoring.RequestStatus
import org.slf4j.LoggerFactory
import org.springframework.context.event.EventListener
@@ -31,7 +31,7 @@ import java.util.*
@Service
class ResponseProcessor(
- private val requestRepository: RequestRepository,
+ private val requestService: RequestService,
private val statisticsUpdateProducer: Sinks.Many<Any>
) {
@@ -39,7 +39,7 @@ class ResponseProcessor(
@EventListener(classes = [ResponseEvent::class])
fun handleResponseEvent(event: ResponseEvent) {
- requestRepository.findByUuidEquals(event.requestUuid).ifPresentOrElse({
+ requestService.findByUuid(event.requestUuid).ifPresentOrElse({
it.processedAt = event.timestamp
it.status = event.status
@@ -76,7 +76,7 @@ class ResponseProcessor(
}
}
- requestRepository.save(it)
+ requestService.save(it)
statisticsUpdateProducer.emitNext("", Sinks.EmitFailureHandler.FAIL_FAST)
}, {
@@ -87,7 +87,7 @@ class ResponseProcessor(
}
data class ResponseEvent(
- val requestUuid: String,
+ val requestUuid: RequestId,
val timestamp: Instant,
val status: RequestStatus,
val body: Optional<String> = Optional.empty()
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/kafka/KafkaResponseProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/kafka/KafkaResponseProcessor.kt
index a29010f..12e824d 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/services/kafka/KafkaResponseProcessor.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/services/kafka/KafkaResponseProcessor.kt
@@ -22,6 +22,7 @@ package dev.dnpm.etl.processor.services.kafka
import com.fasterxml.jackson.annotation.JsonAlias
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.ObjectMapper
+import dev.dnpm.etl.processor.RequestId
import dev.dnpm.etl.processor.monitoring.RequestStatus
import dev.dnpm.etl.processor.output.asRequestStatus
import dev.dnpm.etl.processor.services.ResponseEvent
@@ -47,7 +48,7 @@ class KafkaResponseProcessor(
Optional.empty()
}.ifPresentOrElse({ responseBody ->
val event = ResponseEvent(
- responseBody.requestId,
+ RequestId(responseBody.requestId),
Instant.ofEpochMilli(data.timestamp()),
responseBody.statusCode.asRequestStatus(),
when (responseBody.statusCode.asRequestStatus()) {
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/types.kt b/src/main/kotlin/dev/dnpm/etl/processor/types.kt
new file mode 100644
index 0000000..b41a550
--- /dev/null
+++ b/src/main/kotlin/dev/dnpm/etl/processor/types.kt
@@ -0,0 +1,41 @@
+/*
+ * This file is part of ETL-Processor
+ *
+ * Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package dev.dnpm.etl.processor
+
+import java.util.*
+
+class Fingerprint(val value: String) {
+ override fun hashCode() = value.hashCode()
+
+ override fun equals(other: Any?) = other is Fingerprint && other.value == value
+
+ companion object {
+ fun empty() = Fingerprint("")
+ }
+}
+
+@JvmInline
+value class RequestId(val value: String) {
+
+ fun isBlank() = value.isBlank()
+
+}
+
+fun randomRequestId() = RequestId(UUID.randomUUID().toString()) \ No newline at end of file
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt
index eb9d541..25ec7cc 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt
@@ -27,10 +27,10 @@ import dev.dnpm.etl.processor.output.MtbFileSender
import dev.dnpm.etl.processor.pseudonym.Generator
import dev.dnpm.etl.processor.security.Role
import dev.dnpm.etl.processor.security.UserRole
-import dev.dnpm.etl.processor.services.Token
-import dev.dnpm.etl.processor.services.TokenService
+import dev.dnpm.etl.processor.security.Token
+import dev.dnpm.etl.processor.security.TokenService
import dev.dnpm.etl.processor.services.TransformationService
-import dev.dnpm.etl.processor.services.UserRoleService
+import dev.dnpm.etl.processor.security.UserRoleService
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.http.MediaType
import org.springframework.http.codec.ServerSentEvent
@@ -56,7 +56,7 @@ class ConfigController(
@GetMapping
fun index(model: Model): String {
val outputConnectionAvailable =
- connectionCheckServices.filterIsInstance<OutputConnectionCheckService>().first().connectionAvailable()
+ connectionCheckServices.filterIsInstance<OutputConnectionCheckService>().firstOrNull()?.connectionAvailable()
val gPasConnectionAvailable =
connectionCheckServices.filterIsInstance<GPasConnectionCheckService>().firstOrNull()?.connectionAvailable()
@@ -127,10 +127,11 @@ class ConfigController(
} else {
model.addAttribute("tokensEnabled", true)
val result = tokenService.addToken(name)
- if (result.isSuccess) {
- model.addAttribute("newTokenValue", result.getOrDefault(""))
+ result.onSuccess {
+ model.addAttribute("newTokenValue", it)
model.addAttribute("success", true)
- } else {
+ }
+ result.onFailure {
model.addAttribute("success", false)
}
model.addAttribute("tokens", tokenService.findAll())
@@ -182,6 +183,7 @@ class ConfigController(
}
@GetMapping(path = ["events"], produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
+ @ResponseBody
fun events(): Flux<ServerSentEvent<Any>> {
return connectionCheckUpdateProducer.asFlux().map {
val event = when (it) {
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt
index 6a256aa..ac003d3 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt
@@ -20,9 +20,9 @@
package dev.dnpm.etl.processor.web
import dev.dnpm.etl.processor.NotFoundException
+import dev.dnpm.etl.processor.RequestId
import dev.dnpm.etl.processor.monitoring.ReportService
-import dev.dnpm.etl.processor.monitoring.RequestId
-import dev.dnpm.etl.processor.monitoring.RequestRepository
+import dev.dnpm.etl.processor.services.RequestService
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Sort
import org.springframework.data.web.PageableDefault
@@ -35,7 +35,7 @@ import org.springframework.web.bind.annotation.RequestMapping
@Controller
@RequestMapping(path = ["/"])
class HomeController(
- private val requestRepository: RequestRepository,
+ private val requestService: RequestService,
private val reportService: ReportService
) {
@@ -44,7 +44,7 @@ class HomeController(
@PageableDefault(page = 0, size = 20, sort = ["processedAt"], direction = Sort.Direction.DESC) pageable: Pageable,
model: Model
): String {
- val requests = requestRepository.findAll(pageable)
+ val requests = requestService.findAll(pageable)
model.addAttribute("requests", requests)
return "index"
@@ -56,7 +56,7 @@ class HomeController(
@PageableDefault(page = 0, size = 20, sort = ["processedAt"], direction = Sort.Direction.DESC) pageable: Pageable,
model: Model
): String {
- val requests = requestRepository.findRequestByPatientId(patientId, pageable)
+ val requests = requestService.findRequestByPatientId(patientId, pageable)
model.addAttribute("patientId", patientId)
model.addAttribute("requests", requests)
@@ -65,7 +65,7 @@ class HomeController(
@GetMapping(path = ["/report/{id}"])
fun report(@PathVariable id: RequestId, model: Model): String {
- val request = requestRepository.findByUuidEquals(id.toString()).orElse(null) ?: throw NotFoundException()
+ val request = requestService.findByUuid(id).orElse(null) ?: throw NotFoundException()
model.addAttribute("request", request)
model.addAttribute("issues", reportService.deserialize(request.report?.dataQualityReport))
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsRestController.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsRestController.kt
index daa6af3..c034cb4 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsRestController.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsRestController.kt
@@ -19,9 +19,9 @@
package dev.dnpm.etl.processor.web
-import dev.dnpm.etl.processor.monitoring.RequestRepository
import dev.dnpm.etl.processor.monitoring.RequestStatus
import dev.dnpm.etl.processor.monitoring.RequestType
+import dev.dnpm.etl.processor.services.RequestService
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.http.MediaType
import org.springframework.http.codec.ServerSentEvent
@@ -41,15 +41,15 @@ import java.time.temporal.ChronoUnit
class StatisticsRestController(
@Qualifier("statisticsUpdateProducer")
private val statisticsUpdateProducer: Sinks.Many<Any>,
- private val requestRepository: RequestRepository
+ private val requestService: RequestService
) {
@GetMapping(path = ["requeststates"])
fun requestStates(@RequestParam(required = false, defaultValue = "false") delete: Boolean): List<NameValue> {
val states = if (delete) {
- requestRepository.countDeleteStates()
+ requestService.countDeleteStates()
} else {
- requestRepository.countStates()
+ requestService.countStates()
}
return states
@@ -79,7 +79,7 @@ class StatisticsRestController(
}
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneId.of("Europe/Berlin"))
- val data = requestRepository.findAll()
+ val data = requestService.findAll()
.filter { it.type == requestType }
.filter { it.processedAt.isAfter(Instant.now().minus(30, ChronoUnit.DAYS)) }
.groupBy { formatter.format(it.processedAt) }
@@ -115,9 +115,9 @@ class StatisticsRestController(
@GetMapping(path = ["requestpatientstates"])
fun requestPatientStates(@RequestParam(required = false, defaultValue = "false") delete: Boolean): List<NameValue> {
val states = if (delete) {
- requestRepository.findPatientUniqueDeleteStates()
+ requestService.findPatientUniqueDeleteStates()
} else {
- requestRepository.findPatientUniqueStates()
+ requestService.findPatientUniqueStates()
}
return states.map {
diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml
index 3d4827c..895f026 100644
--- a/src/main/resources/application-dev.yml
+++ b/src/main/resources/application-dev.yml
@@ -3,17 +3,34 @@ spring:
compose:
file: ./dev-compose.yml
+ security:
+ oauth2:
+ client:
+ registration:
+ custom:
+ client-name: App-Dev
+ client-id: app-dev
+ client-secret: very-secret-ae3f7a-5a9f-1190
+ scope:
+ - openid
+ provider:
+ custom:
+ issuer-uri: https://dnpm.dev/auth/realms/intern
+ user-name-attribute: name
+
app:
- #rest:
- # uri: http://localhost:9000/bwhc/etl/api
- kafka:
- topic: test
- response-topic: test_response
- servers: localhost:9094
- #security:
- # admin-user: admin
- # admin-password: "{noop}very-secret"
+ rest:
+ uri: http://localhost:9000/bwhc/etl/api
+ #kafka:
+ # topic: test
+ # response-topic: test_response
+ # servers: localhost:9094
+ security:
+ admin-user: admin
+ admin-password: "{noop}very-secret"
+ enable-oidc: "true"
server:
port: 8000
+
diff --git a/src/main/resources/static/style.css b/src/main/resources/static/style.css
index 1dd68ed..7066e2b 100644
--- a/src/main/resources/static/style.css
+++ b/src/main/resources/static/style.css
@@ -22,6 +22,10 @@
--bg-gray-op: rgba(112, 128, 144, .35);
}
+* {
+ font-family: sans-serif;
+}
+
html {
background: linear-gradient(-5deg, var(--bg-blue-op), transparent 10em);
min-height: 100vh;
@@ -30,7 +34,6 @@ html {
body {
margin: 0 0 5em 0;
- font-family: sans-serif;
font-size: .8rem;
color: var(--text);
@@ -619,6 +622,10 @@ input.inline:focus-visible {
text-align: center;
}
+.notification.info {
+ color: var(--bg-blue);
+}
+
.notification.success {
color: var(--bg-green);
}
@@ -651,6 +658,7 @@ input.inline:focus-visible {
border-radius: 0 .5em .5em .5em;
display: none;
padding: 1em;
+ background: white;
}
.tabcontent.active {
diff --git a/src/main/resources/templates/configs/gPasConnectionAvailable.html b/src/main/resources/templates/configs/gPasConnectionAvailable.html
index 6dccc60..a9a8517 100644
--- a/src/main/resources/templates/configs/gPasConnectionAvailable.html
+++ b/src/main/resources/templates/configs/gPasConnectionAvailable.html
@@ -2,15 +2,20 @@
<h2><span>🟦</span> gPAS nicht konfiguriert - Patienten-IDs werden intern anonymisiert</h2>
</th:block>
<th:block th:if="${gPasConnectionAvailable != null}">
- <h2><span th:if="${gPasConnectionAvailable}">✅</span><span th:if="${not(gPasConnectionAvailable)}">⚡</span> Verbindung zu gPAS</h2>
+ <h2><span th:if="${gPasConnectionAvailable.available}">✅</span><span th:if="${not(gPasConnectionAvailable.available)}">⚡</span> Verbindung zu gPAS</h2>
<div>
- Die Verbindung ist aktuell
- <strong th:if="${gPasConnectionAvailable}" style="color: green">verfügbar.</strong>
- <strong th:if="${not(gPasConnectionAvailable)}" style="color: red">nicht verfügbar.</strong>
+ Stand: <time style="font-weight: bold" th:datetime="${#temporals.formatISO(gPasConnectionAvailable.timestamp)}" th:text="${#temporals.formatISO(gPasConnectionAvailable.timestamp)}"></time>
+ &nbsp;|&nbsp;
+ Letzte Änderung: <time style="font-weight: bold" th:datetime="${#temporals.formatISO(gPasConnectionAvailable.lastChange)}" th:text="${#temporals.formatISO(gPasConnectionAvailable.lastChange)}"></time>
+ </div>
+ <div>
+ <span>Die Verbindung ist aktuell</span>
+ <strong th:if="${gPasConnectionAvailable.available}" style="color: green">verfügbar.</strong>
+ <strong th:if="${not(gPasConnectionAvailable.available)}" style="color: red">nicht verfügbar.</strong>
</div>
<div class="connection-display border">
<img th:src="@{/server.png}" alt="ETL-Processor" />
- <span class="connection" th:classappend="${gPasConnectionAvailable ? 'available' : ''}"></span>
+ <span class="connection" th:classappend="${gPasConnectionAvailable.available ? 'available' : ''}"></span>
<img th:src="@{/server.png}" alt="gPAS" />
<span>ETL-Processor</span>
<span></span>
diff --git a/src/main/resources/templates/configs/outputConnectionAvailable.html b/src/main/resources/templates/configs/outputConnectionAvailable.html
index 2b18b75..4b7f8d1 100644
--- a/src/main/resources/templates/configs/outputConnectionAvailable.html
+++ b/src/main/resources/templates/configs/outputConnectionAvailable.html
@@ -1,16 +1,26 @@
-<h2><span th:if="${outputConnectionAvailable}">✅</span><span th:if="${not(outputConnectionAvailable)}">⚡</span> MTB-File Verbindung</h2>
-<div>
- Verbindung über <code>[[ ${mtbFileSender} ]]</code>. Die Verbindung ist aktuell
- <strong th:if="${outputConnectionAvailable}" style="color: green">verfügbar.</strong>
- <strong th:if="${not(outputConnectionAvailable)}" style="color: red">nicht verfügbar.</strong>
-</div>
-<div class="connection-display border">
- <img th:src="@{/server.png}" alt="ETL-Processor" />
- <span class="connection" th:classappend="${outputConnectionAvailable ? 'available' : ''}"></span>
- <img th:if="${mtbFileSender.startsWith('Rest')}" th:src="@{/server.png}" alt="bwHC-Backend" />
- <img th:if="${mtbFileSender.startsWith('Kafka')}" th:src="@{/kafka.png}" alt="Kafka-Broker" />
- <span>ETL-Processor</span>
- <span></span>
- <span th:if="${mtbFileSender.startsWith('Rest')}">bwHC-Backend</span>
- <span th:if="${mtbFileSender.startsWith('Kafka')}">Kafka-Broker</span>
-</div> \ No newline at end of file
+<th:block th:if="${outputConnectionAvailable == null}">
+ <h2><span>🟦</span> Keine Ausgabenkonfiguration</h2>
+</th:block>
+<th:block th:if="${outputConnectionAvailable != null}">
+ <h2><span th:if="${outputConnectionAvailable.available}">✅</span><span th:if="${not(outputConnectionAvailable.available)}">⚡</span> MTB-File Verbindung</h2>
+ <div>
+ Stand: <time style="font-weight: bold" th:datetime="${#temporals.formatISO(outputConnectionAvailable.timestamp)}" th:text="${#temporals.formatISO(outputConnectionAvailable.timestamp)}"></time>
+ &nbsp;|&nbsp;
+ Letzte Änderung: <time style="font-weight: bold" th:datetime="${#temporals.formatISO(outputConnectionAvailable.lastChange)}" th:text="${#temporals.formatISO(outputConnectionAvailable.lastChange)}"></time>
+ </div>
+ <div>
+ Verbindung über <code>[[ ${mtbFileSender} ]]</code>. Die Verbindung ist aktuell
+ <strong th:if="${outputConnectionAvailable.available}" style="color: green">verfügbar.</strong>
+ <strong th:if="${not(outputConnectionAvailable.available)}" style="color: red">nicht verfügbar.</strong>
+ </div>
+ <div class="connection-display border">
+ <img th:src="@{/server.png}" alt="ETL-Processor" />
+ <span class="connection" th:classappend="${outputConnectionAvailable.available ? 'available' : ''}"></span>
+ <img th:if="${mtbFileSender.startsWith('Rest')}" th:src="@{/server.png}" alt="bwHC-Backend" />
+ <img th:if="${mtbFileSender.startsWith('Kafka')}" th:src="@{/kafka.png}" alt="Kafka-Broker" />
+ <span>ETL-Processor</span>
+ <span></span>
+ <span th:if="${mtbFileSender.startsWith('Rest')}">bwHC-Backend</span>
+ <span th:if="${mtbFileSender.startsWith('Kafka')}">Kafka-Broker</span>
+ </div>
+</th:block> \ No newline at end of file
diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html
index be3123b..520efb6 100644
--- a/src/main/resources/templates/index.html
+++ b/src/main/resources/templates/index.html
@@ -18,7 +18,11 @@
</h2>
</div>
- <div class="border">
+ <div class="border" th:if="${requests.totalElements == 0}">
+ <div class="notification info">Noch keine Anfragen eingegangen</div>
+ </div>
+
+ <div class="border" th:if="${requests.totalElements > 0}">
<div th:if="${patientId == null}" class="page-control">
<a id="first-page-link" th:href="@{/(page=${0})}" title="Zum Anfang: Taste W" th:if="${not requests.isFirst()}">&larrb;</a><a th:if="${requests.isFirst()}">&larrb;</a>
<a id="prev-page-link" th:href="@{/(page=${requests.getNumber() - 1})}" title="Seite zurück: Taste A" th:if="${not requests.isFirst()}">&larr;</a><a th:if="${requests.isFirst()}">&larr;</a>