diff --git a/.dockerignore b/.dockerignore index 91a5bd124..fc32859d4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,12 @@ -/src/test/ -/src/integrationTest -/src/gatling \ No newline at end of file +/appserver-jersey/src/test/ +/appserver-jersey/src/integrationTest +/appserver-jersey/build +/appserver-legacy/build +/buildSrc/build +/out +/.git +/.gradle* +/gradle/wrapper +/gradlew* +/.idea +/.github diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..643c4a57b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,24 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 4 + +[{*.yml, *.yaml}] +indent_size = 2 + +[*.gradle] +indent_size = 4 + +[*.{kt,kts}] +ij_kotlin_allow_trailing_comma_on_call_site = true +ij_kotlin_allow_trailing_comma = true +# To satisfy ktlint rules, see: https://stackoverflow.com/questions/59849619/intellij-does-not-sort-kotlin-imports-according-to-ktlints-expectations +ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^ diff --git a/.gitignore b/.gitignore index 1341d5613..b792bf064 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -.gradle -/build/ +**/.gradle +**/build/ !gradle/wrapper/gradle-wrapper.jar ### STS ### diff --git a/Dockerfile.appserver-jersey b/Dockerfile.appserver-jersey new file mode 100644 index 000000000..0d0afc1a9 --- /dev/null +++ b/Dockerfile.appserver-jersey @@ -0,0 +1,44 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM --platform=$BUILDPLATFORM gradle:8.5-jdk17 AS builder + +RUN mkdir /code +WORKDIR /code + +ENV GRADLE_USER_HOME=/code/.gradlecache \ + GRADLE_OPTS="-Djdk.lang.Process.launchMechanism=vfork -Dorg.gradle.vfs.watch=false" + +COPY ./buildSrc /code/buildSrc +COPY ./build.gradle.kts ./settings.gradle.kts /code/ +COPY appserver-jersey/build.gradle.kts /code/appserver-jersey/ + +RUN gradle appserver-jersey:downloadDependencies appserver-jersey:copyDependencies appserver-jersey:startScripts + +COPY appserver-jersey/src/ /code/appserver-jersey/src + +RUN gradle jar + +FROM eclipse-temurin:17-jre + +COPY --from=builder /code/appserver-jersey/build/scripts/* /usr/bin/ +COPY --from=builder /code/appserver-jersey/build/third-party/* /usr/lib/ +COPY --from=builder /code/appserver-jersey/build/libs/*.jar /usr/lib/ +COPY --from=builder /code/appserver-jersey/src/main/resources/appserver.yml /etc/appserver-jersey/appserver.yml + +USER 101 + +EXPOSE 8080 + +CMD ["appserver-jersey"] + + diff --git a/appserver-jersey/build.gradle.kts b/appserver-jersey/build.gradle.kts new file mode 100644 index 000000000..66b9ec192 --- /dev/null +++ b/appserver-jersey/build.gradle.kts @@ -0,0 +1,100 @@ +plugins { + application + kotlin("plugin.serialization") version Versions.kotlinVersion + id("org.radarbase.radar-kotlin") + kotlin("plugin.allopen") + kotlin("plugin.noarg") +} + +application { + mainClass.set("org.radarbase.appserver.jersey.JerseyAppserverKt") + + applicationDefaultJvmArgs = listOf( + "-Dcom.sun.management.jmxremote", + "-Dcom.sun.management.jmxremote.local.only=false", + "-Dcom.sun.management.jmxremote.port=9010", + "-Dcom.sun.management.jmxremote.authenticate=false", + "-Dcom.sun.management.jmxremote.ssl=false", + ) +} + +description = "RADAR Appserver for scheduling tasks and notifications." + +val integrationTestSourceSet = sourceSets.create("integrationTest") { + compileClasspath += sourceSets.main.get().output + runtimeClasspath += sourceSets.main.get().output +} + +val integrationTestImplementation: Configuration by configurations.getting { + extendsFrom(configurations.testImplementation.get()) +} + +val integrationTest by tasks.registering(Test::class) { + description = "Runs integration tests." + group = "verification" + testClassesDirs = integrationTestSourceSet.output.classesDirs + classpath = integrationTestSourceSet.runtimeClasspath + testLogging.showStandardStreams = true + shouldRunAfter("test") + outputs.upToDateWhen { false } +} + +configurations["integrationTestRuntimeOnly"].extendsFrom(configurations.testRuntimeOnly.get()) + +allOpen { + annotation("jakarta.persistence.MappedSuperclass") + annotation("jakarta.persistence.Entity") + annotation("jakarta.persistence.Embeddable") +} + +dependencies { + implementation(kotlin("reflect")) + + implementation("org.radarbase:radar-commons-kotlin:${Versions.radarCommonsVersion}") + implementation("org.radarbase:radar-jersey:${Versions.radarJerseyVersion}") + implementation("org.radarbase:radar-jersey-hibernate:${Versions.radarJerseyVersion}") { + runtimeOnly("org.postgresql:postgresql:${Versions.postgresqlVersion}") + } + implementation("com.h2database:h2:${Versions.h2Version}") + + implementation("io.ktor:ktor-client-core:${Versions.ktorVersion}") + implementation("io.ktor:ktor-client-cio:${Versions.ktorVersion}") + implementation("org.glassfish.jersey.ext:jersey-bean-validation:3.1.10") + + implementation("com.google.firebase:firebase-admin:9.3.0") { + constraints { + implementation("com.google.protobuf:protobuf-java:3.25.5") { + because("Provided version of protobuf has security vulnerabilities") + } + implementation("com.google.protobuf:protobuf-java-util:3.25.5") { + because("Provided version of protobuf has security vulnerabilities") + } + } + } + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") + + implementation("com.google.guava:guava:32.1.3-jre") + implementation("org.quartz-scheduler:quartz:2.5.0") + + testImplementation("io.mockk:mockk:1.14.4") + testImplementation("org.mockito.kotlin:mockito-kotlin:3.2.0") + testImplementation("org.hamcrest:hamcrest:2.1") + testImplementation("org.assertj:assertj-core:3.24.2") + + integrationTestImplementation(platform("io.ktor:ktor-bom:${Versions.ktorVersion}")) + integrationTestImplementation("io.ktor:ktor-client-content-negotiation") + integrationTestImplementation("io.ktor:ktor-serialization-kotlinx-json") +} + +ktlint { + ignoreFailures.set(false) + outputColorName.set("RED") +} + +radarKotlin { + javaVersion.set(Versions.java) + kotlinVersion.set(Versions.kotlinVersion) +// kotlinApiVersion.set(Versions.kotlinVersion) + junitVersion.set(Versions.junit5Version) + log4j2Version.set(Versions.log4j2) +} diff --git a/appserver-jersey/docker-compose.yml b/appserver-jersey/docker-compose.yml new file mode 100644 index 000000000..3f5cb1d05 --- /dev/null +++ b/appserver-jersey/docker-compose.yml @@ -0,0 +1,63 @@ +version: '3.8' + +services: + #---------------------------------------------------------------------------# + # ManagementPortal Postgres # + #---------------------------------------------------------------------------# + managementportal-postgresql: + image: postgres + environment: + POSTGRES_USER: radarbase + POSTGRES_PASSWORD: radarbase + POSTGRES_DB: managementportal + + + #---------------------------------------------------------------------------# + # Management Portal # + #---------------------------------------------------------------------------# + managementportal: + image: radarbase/management-portal:2.1.0 + environment: + SERVER_PORT: 8081 + SPRING_PROFILES_ACTIVE: prod + SPRING_DATASOURCE_URL: jdbc:postgresql://managementportal-postgresql:5432/managementportal + SPRING_DATASOURCE_USERNAME: radarbase + SPRING_DATASOURCE_PASSWORD: radarbase + SPRING_LIQUIBASE_CONTEXTS: dev #includes testing_data, remove for production builds + JHIPSTER_SLEEP: 10 # gives time for the database to boot before the application + JAVA_OPTS: -Xmx512m # maximum heap size for the JVM running ManagementPortal, increase this as necessary + MANAGEMENTPORTAL_COMMON_BASE_URL: http://localhost:8081/managementportal + MANAGEMENTPORTAL_COMMON_MANAGEMENT_PORTAL_BASE_URL: http://localhost:8081/managementportal + MANAGEMENTPORTAL_FRONTEND_CLIENT_SECRET: + MANAGEMENTPORTAL_OAUTH_CLIENTS_FILE: /mp-includes/config/oauth_client_details.csv + volumes: + - ./src/integrationTest/resources/docker/etc/:/mp-includes/ + + #---------------------------------------------------------------------------# + # Appserver Postgres # + #---------------------------------------------------------------------------# + appserver-postgres: + image: postgres + environment: + POSTGRES_DB: appserver + POSTGRES_USER: radar + POSTGRES_PASSWORD: radar + + #---------------------------------------------------------------------------# + # Appserver # + #---------------------------------------------------------------------------# + appserver: + build: + context: ../ + dockerfile: Dockerfile.appserver-jersey + image: radarbase/radar-appserver:SNAPSHOT + restart: always + ports: + - "8080:8080" + environment: + JDK_JAVA_OPTIONS: -Xmx4G -Djava.security.egd=file:/dev/./urandom + APPSERVER_JDBC_URL: jdbc:postgresql://postgres:5432/appserver + APPSERVER_JDBC_USERNAME: radar + APPSERVER_JDBC_PASSWORD: radar + APPSERVER_JDBC_DRIVER: org.postgresql.Driver + APPSERVER_HIBERNATE_DIALECT: org.hibernate.dialect.PostgreSQLDialect diff --git a/appserver-jersey/src/integrationTest/kotlin/org/radarbase/appserver/jersey/auth/NotificationEndpointAuthTest.kt b/appserver-jersey/src/integrationTest/kotlin/org/radarbase/appserver/jersey/auth/NotificationEndpointAuthTest.kt new file mode 100644 index 000000000..d049048d3 --- /dev/null +++ b/appserver-jersey/src/integrationTest/kotlin/org/radarbase/appserver/jersey/auth/NotificationEndpointAuthTest.kt @@ -0,0 +1,253 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.auth + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultRequest +import io.ktor.client.request.accept +import io.ktor.client.request.get +import io.ktor.client.request.header +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import io.ktor.http.headersOf +import io.ktor.serialization.kotlinx.json.json +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.MethodOrderer +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestMethodOrder +import org.radarbase.appserver.jersey.auth.commons.MpOAuthSupport +import org.radarbase.appserver.jersey.dto.ProjectDto +import org.radarbase.appserver.jersey.dto.fcm.FcmNotificationDto +import org.radarbase.appserver.jersey.dto.fcm.FcmNotifications +import org.radarbase.appserver.jersey.dto.fcm.FcmUserDto +import java.time.Duration +import java.time.Instant + +@TestMethodOrder(MethodOrderer.OrderAnnotation::class) +class NotificationEndpointAuthTest { + + val notification = FcmNotificationDto().apply { + scheduledTime = Instant.now().plus(Duration.ofSeconds(100)) + body = "Test Body" + sourceId = "test-source" + title = "Test Title" + ttlSeconds = 86400 + fcmMessageId = "123455" + additionalData = mutableMapOf() + appPackage = "armt" + sourceType = "armt" + type = "ESM" + } + + @BeforeEach + fun createUserAndProject(): Unit = runBlocking { + val project = ProjectDto(projectId = "radar") + + httpClient.post(PROJECT_PATH) { + contentType(ContentType.Application.Json) + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + setBody(project) + } + + val fcmUserDto = FcmUserDto( + projectId = "radar", + language = "en", + enrolmentDate = Instant.now(), + fcmToken = "xxx", + subjectId = "sub-1", + timezone = "Europe/London", + ) + + httpClient.post("$PROJECT_PATH/$DEFAULT_PROJECT/$USER_PATH") { + contentType(ContentType.Application.Json) + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + setBody(fcmUserDto) + } + } + + @Test + fun unAuthorizedViewNotificationsForUser(): Unit = runBlocking { + val response = httpClient.get( + "$PROJECT_PATH/$DEFAULT_PROJECT/$USER_PATH/$DEFAULT_USER/$NOTIFICATION_PATH", + ) { + accept(ContentType.Application.Json) + } + + assertEquals(response.status, HttpStatusCode.Unauthorized) + } + + @Test + fun unAuthorizedViewNotificationsForProject(): Unit = runBlocking { + val response = httpClient.get( + "$PROJECT_PATH/$DEFAULT_PROJECT/$NOTIFICATION_PATH", + ) { + accept(ContentType.Application.Json) + } + + assertEquals(response.status, HttpStatusCode.Unauthorized) + } + + @Test + fun unauthorizedCreateNotificationForUser(): Unit = runBlocking { + val response = httpClient.post( + "$PROJECT_PATH/$DEFAULT_PROJECT/$USER_PATH/$DEFAULT_USER/$NOTIFICATION_PATH", + ) { + contentType(ContentType.Application.Json) + setBody(notification) + } + + assertEquals(response.status, HttpStatusCode.Unauthorized) + } + + @Order(1) + @Test + fun createNotificationForUser(): Unit = runBlocking { + val response = httpClient.post( + "$PROJECT_PATH/$DEFAULT_PROJECT/$USER_PATH/$DEFAULT_USER/$NOTIFICATION_PATH", + ) { + setBody(notification) + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + contentType(ContentType.Application.Json) + } + + assertEquals(response.status, HttpStatusCode.Created) + } + + @Order(2) + @Test + fun createBatchNotificationForUser(): Unit = runBlocking { + val singleNotification = notification + val notifications = FcmNotifications( + mutableListOf( + singleNotification.apply { + title = "Test Title 1" + fcmMessageId = "xxxyyyy" + }, + ), + ) + + val response = httpClient.post( + "$PROJECT_PATH/$DEFAULT_PROJECT/$USER_PATH/$DEFAULT_USER/$NOTIFICATION_PATH/batch", + ) { + setBody(notifications) + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + contentType(ContentType.Application.Json) + } + + assertEquals(response.status, HttpStatusCode.Created) + } + + @Test + fun viewNotificationsForUser(): Unit = runBlocking { + val response = httpClient.get( + "$PROJECT_PATH/$DEFAULT_PROJECT/$USER_PATH/$DEFAULT_USER/$NOTIFICATION_PATH", + ) { + accept(ContentType.Application.Json) + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + } + + assertEquals(response.status, HttpStatusCode.OK) + } + + @Test + fun viewNotificationsForProject(): Unit = runBlocking { + val response = httpClient.get( + "$PROJECT_PATH/$DEFAULT_PROJECT/$NOTIFICATION_PATH", + ) { + accept(ContentType.Application.Json) + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + } + + assertEquals(response.status, HttpStatusCode.OK) + } + + @Test + fun forbiddenViewNotificationsForOtherUser(): Unit = runBlocking { + val response = httpClient.get( + "$PROJECT_PATH/$DEFAULT_PROJECT/$USER_PATH/sub-2/$NOTIFICATION_PATH", + ) { + accept(ContentType.Application.Json) + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + } + + assertEquals(response.status, HttpStatusCode.Forbidden) + } + + @Test + fun forbiddenViewNotificationsForOtherProject(): Unit = runBlocking { + val response = httpClient.get( + "$PROJECT_PATH/other-project/$NOTIFICATION_PATH", + ) { + accept(ContentType.Application.Json) + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + } + + assertEquals(response.status, HttpStatusCode.Forbidden) + } + + companion object { + private const val APPSERVER_URL = "http://localhost:8080" + private lateinit var AUTH_HEADERS: Headers + private lateinit var httpClient: HttpClient + private const val PROJECT_PATH = "projects" + private const val USER_PATH = "users" + private const val DEFAULT_PROJECT = "radar" + private const val DEFAULT_USER = "sub-1" + private const val NOTIFICATION_PATH = "messaging/notifications" + + @BeforeAll + @JvmStatic + fun init() { + httpClient = HttpClient(CIO) { + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + coerceInputValues = true + }, + ) + } + defaultRequest { + url("${APPSERVER_URL}/") + } + } + + val oAuthSupport = MpOAuthSupport().apply { + init() + } + + AUTH_HEADERS = runBlocking { + headersOf( + HttpHeaders.Authorization, + "Bearer ${oAuthSupport.requestAccessToken()}", + ) + } + } + } +} diff --git a/appserver-jersey/src/integrationTest/kotlin/org/radarbase/appserver/jersey/auth/ProjectEndpointAuthTest.kt b/appserver-jersey/src/integrationTest/kotlin/org/radarbase/appserver/jersey/auth/ProjectEndpointAuthTest.kt new file mode 100644 index 000000000..996c1da80 --- /dev/null +++ b/appserver-jersey/src/integrationTest/kotlin/org/radarbase/appserver/jersey/auth/ProjectEndpointAuthTest.kt @@ -0,0 +1,156 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.auth + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultRequest +import io.ktor.client.request.accept +import io.ktor.client.request.get +import io.ktor.client.request.header +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import io.ktor.http.headersOf +import io.ktor.serialization.kotlinx.json.json +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.MethodOrderer +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestMethodOrder +import org.radarbase.appserver.jersey.auth.commons.MpOAuthSupport +import org.radarbase.appserver.jersey.dto.ProjectDto + +@TestMethodOrder(MethodOrderer.OrderAnnotation::class) +class ProjectEndpointAuthTest { + + @Test + fun unAuthorizedCreatedProject(): Unit = runBlocking { + val project = ProjectDto(projectId = "radar") + val response = httpClient.post(PROJECT_PATH) { + contentType(ContentType.Application.Json) + setBody(project) + } + assertEquals(response.status, HttpStatusCode.Unauthorized) + } + + @Test + fun unAuthorizedViewProjects(): Unit = runBlocking { + val response = httpClient.get(PROJECT_PATH) { + accept(ContentType.Application.Json) + } + assertEquals(response.status, HttpStatusCode.Unauthorized) + } + + @Test + fun unAuthorizedViewSingleProject(): Unit = runBlocking { + val response = httpClient.get("$PROJECT_PATH/radar") { + accept(ContentType.Application.Json) + } + assertEquals(response.status, HttpStatusCode.Unauthorized) + } + + @Test + fun forbiddenViewProjects(): Unit = runBlocking { + val response = httpClient.get(PROJECT_PATH) { + accept(ContentType.Application.Json) + header(HttpHeaders.Authorization, "Bearer ${AUTH_HEADERS[HttpHeaders.Authorization]}") + } + // Only Admins Can View List Of All Projects + assertEquals(response.status, HttpStatusCode.Forbidden) + } + + @Test + @Order(1) + fun createSingleProjectWithAuth() = runBlocking { + val project = ProjectDto(projectId = "radar") + val response = httpClient.post(PROJECT_PATH) { + contentType(ContentType.Application.Json) + setBody(project) + header(HttpHeaders.Authorization, "Bearer ${AUTH_HEADERS[HttpHeaders.Authorization]}") + } + + if (response.status == HttpStatusCode.ExpectationFailed) { + return@runBlocking + } + assertEquals(HttpStatusCode.Created, response.status) + } + + @Test + @Order(2) + fun getSingleProjectWithAuth() = runBlocking { + val response = httpClient.get("$PROJECT_PATH/radar") { + accept(ContentType.Application.Json) + header(HttpHeaders.Authorization, "Bearer ${AUTH_HEADERS[HttpHeaders.Authorization]}") + } + assertEquals(HttpStatusCode.OK, response.status) + } + + @Test + @Order(3) + fun getForbiddenProjectWithAuth() = runBlocking { + val response = httpClient.get("$PROJECT_PATH/test") { + accept(ContentType.Application.Json) + header(HttpHeaders.Authorization, "Bearer ${AUTH_HEADERS[HttpHeaders.Authorization]}") + } + assertEquals(HttpStatusCode.Forbidden, response.status) + } + + companion object { + private const val APPSERVER_URL = "http://localhost:8080" + private const val PROJECT_PATH = "projects" + private lateinit var AUTH_HEADERS: Headers + private lateinit var httpClient: HttpClient + + @BeforeAll + @JvmStatic + fun init() { + httpClient = HttpClient(CIO) { + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + coerceInputValues = true + }, + ) + } + defaultRequest { + url("${APPSERVER_URL}/") + } + } + + val oAuthSupport = MpOAuthSupport().apply { + init() + } + + AUTH_HEADERS = runBlocking { + headersOf( + HttpHeaders.Authorization, + "Bearer ${oAuthSupport.requestAccessToken()}", + ) + } + } + } +} diff --git a/appserver-jersey/src/integrationTest/kotlin/org/radarbase/appserver/jersey/auth/UserEndpointAuthTest.kt b/appserver-jersey/src/integrationTest/kotlin/org/radarbase/appserver/jersey/auth/UserEndpointAuthTest.kt new file mode 100644 index 000000000..7f0e03c6e --- /dev/null +++ b/appserver-jersey/src/integrationTest/kotlin/org/radarbase/appserver/jersey/auth/UserEndpointAuthTest.kt @@ -0,0 +1,190 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.auth + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultRequest +import io.ktor.client.request.accept +import io.ktor.client.request.get +import io.ktor.client.request.header +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import io.ktor.http.headersOf +import io.ktor.serialization.kotlinx.json.json +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.MethodOrderer +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestMethodOrder +import org.radarbase.appserver.jersey.auth.commons.MpOAuthSupport +import org.radarbase.appserver.jersey.dto.ProjectDto +import org.radarbase.appserver.jersey.dto.fcm.FcmUserDto +import java.time.Instant + +@TestMethodOrder(MethodOrderer.OrderAnnotation::class) +class UserEndpointAuthTest { + val fcmUserDto = FcmUserDto( + projectId = "radar", + language = "en", + enrolmentDate = Instant.now(), + fcmToken = "xxx", + subjectId = "sub-1", + timezone = "Europe/London", + ) + + @BeforeEach + fun createProject(): Unit = runBlocking { + val project = ProjectDto(projectId = DEFAULT_PROJECT) + + httpClient.post { + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + contentType(ContentType.Application.Json) + setBody(project) + } + } + + @Test + fun unauthorizedViewSingleUser(): Unit = runBlocking { + val response = httpClient.get("$PROJECT_PATH/$DEFAULT_PROJECT/$USER_PATH/sub-1") { + accept(ContentType.Application.Json) + } + + assertEquals(response.status, HttpStatusCode.Unauthorized) + } + + @Test + fun unAuthorizedCreateUser(): Unit = runBlocking { + val response = httpClient.post("$PROJECT_PATH/$DEFAULT_PROJECT/$USER_PATH") { + contentType(ContentType.Application.Json) + setBody(fcmUserDto) + } + + assertEquals(response.status, HttpStatusCode.Unauthorized) + } + + @Test + @Order(1) + fun createUser(): Unit = runBlocking { + val response = httpClient.post("$PROJECT_PATH/$DEFAULT_PROJECT/$USER_PATH") { + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + contentType(ContentType.Application.Json) + setBody(fcmUserDto) + } + + if (response.status == HttpStatusCode.ExpectationFailed) { + // The auth was successful but expectation failed if the user already exits. + // Since this is just an auth test we can return. + return@runBlocking + } + + assertEquals(response.status, HttpStatusCode.Created) + } + + @Test + @Order(2) + fun viewUser(): Unit = runBlocking { + val response = httpClient.get("$PROJECT_PATH/$DEFAULT_PROJECT/$USER_PATH/sub-1") { + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + accept(ContentType.Application.Json) + } + + assertEquals(response.status, HttpStatusCode.OK) + } + + @Test + @Order(3) + fun viewUsersInProject(): Unit = runBlocking { + val response = httpClient.get("$PROJECT_PATH/$DEFAULT_PROJECT/$USER_PATH") { + accept(ContentType.Application.Json) + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + } + + assertEquals(HttpStatusCode.OK, response.status) + } + + @Test + @Order(4) + fun forbiddenViewUsersInOtherProject(): Unit = runBlocking { + val response = httpClient.get("$PROJECT_PATH/other-project/$USER_PATH") { + accept(ContentType.Application.Json) + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + } + + assertEquals(response.status, HttpStatusCode.Forbidden) + } + + @Test + @Order(5) + fun viewAllUsers() = runBlocking { + val response = httpClient.get(USER_PATH) { + accept(ContentType.Application.Json) + header(HttpHeaders.Authorization, AUTH_HEADERS[HttpHeaders.Authorization]) + } + + // Should return a filtered list of users for which the token has access. + assertEquals(response.status, HttpStatusCode.OK) + } + + companion object { + private const val APPSERVER_URL = "http://localhost:8080" + private const val PROJECT_PATH = "projects" + private const val USER_PATH = "users" + private const val DEFAULT_PROJECT = "radar" + private lateinit var AUTH_HEADERS: Headers + private lateinit var httpClient: HttpClient + + @BeforeAll + @JvmStatic + fun init() { + httpClient = HttpClient(CIO) { + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + coerceInputValues = true + }, + ) + } + defaultRequest { + url("${APPSERVER_URL}/") + } + } + + val oAuthSupport = MpOAuthSupport().apply { + init() + } + + AUTH_HEADERS = runBlocking { + headersOf( + HttpHeaders.Authorization, + "Bearer ${oAuthSupport.requestAccessToken()}", + ) + } + } + } +} diff --git a/appserver-jersey/src/integrationTest/kotlin/org/radarbase/appserver/jersey/auth/commons/MPMetaToken.kt b/appserver-jersey/src/integrationTest/kotlin/org/radarbase/appserver/jersey/auth/commons/MPMetaToken.kt new file mode 100644 index 000000000..43fc77294 --- /dev/null +++ b/appserver-jersey/src/integrationTest/kotlin/org/radarbase/appserver/jersey/auth/commons/MPMetaToken.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.auth.commons + +import kotlinx.serialization.Serializable + +@Serializable +data class MPMetaToken( + val refreshToken: String, +) diff --git a/appserver-jersey/src/integrationTest/kotlin/org/radarbase/appserver/jersey/auth/commons/MPPairResponse.kt b/appserver-jersey/src/integrationTest/kotlin/org/radarbase/appserver/jersey/auth/commons/MPPairResponse.kt new file mode 100644 index 000000000..3988e8358 --- /dev/null +++ b/appserver-jersey/src/integrationTest/kotlin/org/radarbase/appserver/jersey/auth/commons/MPPairResponse.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.auth.commons + +import kotlinx.serialization.Serializable + +@Serializable +class MPPairResponse( + val tokenUrl: String, +) diff --git a/appserver-jersey/src/integrationTest/kotlin/org/radarbase/appserver/jersey/auth/commons/MpOAuthSupport.kt b/appserver-jersey/src/integrationTest/kotlin/org/radarbase/appserver/jersey/auth/commons/MpOAuthSupport.kt new file mode 100644 index 000000000..c93741a9d --- /dev/null +++ b/appserver-jersey/src/integrationTest/kotlin/org/radarbase/appserver/jersey/auth/commons/MpOAuthSupport.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.auth.commons + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultRequest +import io.ktor.client.request.basicAuth +import io.ktor.client.request.forms.submitForm +import io.ktor.client.request.get +import io.ktor.http.HttpStatusCode +import io.ktor.http.Parameters +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.core.IsEqual.equalTo +import org.radarbase.ktor.auth.OAuth2AccessToken +import org.radarbase.ktor.auth.bearer + +class MpOAuthSupport { + private lateinit var httpClient: HttpClient + + fun init() { + httpClient = HttpClient(CIO) { + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + coerceInputValues = true + }, + ) + } + defaultRequest { + url("${MANAGEMENTPORTAL_URL}/") + } + } + } + + suspend fun requestAccessToken(): String { + val response = httpClient.submitForm( + url = "oauth/token", + formParameters = Parameters.build { + append("username", ADMIN_USER) + append("password", ADMIN_PASSWORD) + append("grant_type", "password") + }, + ) { + basicAuth(username = MP_CLIENT, password = "") + } + assertThat(response.status, equalTo(HttpStatusCode.OK)) + val token = response.body() + + val tokenUrl = httpClient.get("api/oauth-clients/pair") { + url { + parameters.append("clientId", REST_CLIENT) + parameters.append("login", "sub-1") + parameters.append("persistent", "false") + } + bearer(requireNotNull(token.accessToken)) + }.body().tokenUrl + + println("Requesting refresh token") + val refreshToken = httpClient.get(tokenUrl).body().refreshToken + + return requireNotNull( + httpClient.submitForm( + url = "oauth/token", + formParameters = Parameters.build { + append("grant_type", "refresh_token") + append("refresh_token", refreshToken) + }, + ) { + basicAuth(REST_CLIENT, "") + }.body().accessToken, + ) + } + + companion object { + private const val MANAGEMENTPORTAL_URL = "http://localhost:8081/managementportal" + const val MP_CLIENT = "ManagementPortalapp" + const val REST_CLIENT = "pRMT" + const val ADMIN_USER = "admin" + const val ADMIN_PASSWORD = "admin" + } +} diff --git a/appserver-jersey/src/integrationTest/resources/docker/docker-compose.yml b/appserver-jersey/src/integrationTest/resources/docker/docker-compose.yml new file mode 100644 index 000000000..b265a210e --- /dev/null +++ b/appserver-jersey/src/integrationTest/resources/docker/docker-compose.yml @@ -0,0 +1,46 @@ +version: '3.8' + +services: + #---------------------------------------------------------------------------# + # ManagementPortal Postgres # + #---------------------------------------------------------------------------# + managementportal-postgresql: + image: postgres + environment: + POSTGRES_USER: radarbase + POSTGRES_PASSWORD: radarbase + POSTGRES_DB: managementportal + + + #---------------------------------------------------------------------------# + # Management Portal # + #---------------------------------------------------------------------------# + managementportal: + image: radarbase/management-portal:2.1.0 + environment: + SERVER_PORT: 8081 + SPRING_PROFILES_ACTIVE: prod + SPRING_DATASOURCE_URL: jdbc:postgresql://managementportal-postgresql:5432/managementportal + SPRING_DATASOURCE_USERNAME: radarbase + SPRING_DATASOURCE_PASSWORD: radarbase + SPRING_LIQUIBASE_CONTEXTS: dev #includes testing_data, remove for production builds + JHIPSTER_SLEEP: 10 # gives time for the database to boot before the application + JAVA_OPTS: -Xmx512m # maximum heap size for the JVM running ManagementPortal, increase this as necessary + MANAGEMENTPORTAL_COMMON_BASE_URL: http://localhost:8081/managementportal + MANAGEMENTPORTAL_COMMON_MANAGEMENT_PORTAL_BASE_URL: http://localhost:8081/managementportal + MANAGEMENTPORTAL_FRONTEND_CLIENT_SECRET: + MANAGEMENTPORTAL_OAUTH_CLIENTS_FILE: /mp-includes/config/oauth_client_details.csv + volumes: + - ./etc/:/mp-includes/ + + #---------------------------------------------------------------------------# + # Appserver Postgres # + #---------------------------------------------------------------------------# + appserver-postgres: + image: postgres + ports: + - "5432:5432" + environment: + POSTGRES_DB: radar + POSTGRES_USER: radar + POSTGRES_PASSWORD: radar diff --git a/src/integrationTest/resources/docker/etc/config/keystore.p12 b/appserver-jersey/src/integrationTest/resources/docker/etc.config/keystore.p12 similarity index 100% rename from src/integrationTest/resources/docker/etc/config/keystore.p12 rename to appserver-jersey/src/integrationTest/resources/docker/etc.config/keystore.p12 diff --git a/src/integrationTest/resources/docker/etc/config/oauth_client_details.csv b/appserver-jersey/src/integrationTest/resources/docker/etc.config/oauth_client_details.csv similarity index 100% rename from src/integrationTest/resources/docker/etc/config/oauth_client_details.csv rename to appserver-jersey/src/integrationTest/resources/docker/etc.config/oauth_client_details.csv diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/JerseyAppserver.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/JerseyAppserver.kt new file mode 100644 index 000000000..1878aa3fb --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/JerseyAppserver.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey + +import org.radarbase.appserver.jersey.config.AppserverConfig +import org.radarbase.appserver.jersey.config.Validation +import org.radarbase.jersey.GrizzlyServer +import org.radarbase.jersey.config.ConfigLoader +import org.slf4j.LoggerFactory +import kotlin.system.exitProcess + +fun main(args: Array) { + val logger = LoggerFactory.getLogger("org.radarbase.appserver.JerseyAppServerKt") + + logger.info("Starting Jersey Appserver") + val config = try { + ConfigLoader.loadConfig( + listOf( + "appserver-jersey/src/main/resources/appserver.yml", + "/etc/appserver-jersey/appserver.yml", + ), + args, + ) + } catch (ex: IllegalArgumentException) { + logger.error("No configuration file (appserver.yml) was found.") + exitProcess(1) + } + + try { + (config as Validation).validate() + } catch (ex: IllegalStateException) { + logger.error("Incomplete configuration: {}", ex.message) + exitProcess(1) + } + + ConfigLoader.loadResources(config.resourceConfig, config).run { + GrizzlyServer(config.server.baseUri, this) + }.let { server: GrizzlyServer -> + server.start() + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/application/event/EventBusStartupListener.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/application/event/EventBusStartupListener.kt new file mode 100644 index 000000000..84e04d91f --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/application/event/EventBusStartupListener.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.application.event + +import com.google.common.eventbus.EventBus +import jakarta.inject.Inject +import org.glassfish.hk2.api.ServiceLocator +import org.glassfish.jersey.server.monitoring.ApplicationEvent +import org.glassfish.jersey.server.monitoring.ApplicationEventListener +import org.glassfish.jersey.server.monitoring.RequestEvent +import org.glassfish.jersey.server.monitoring.RequestEventListener +import org.radarbase.appserver.jersey.event.listener.MessageStateEventListener +import org.radarbase.appserver.jersey.event.listener.TaskStateEventListener + +class EventBusStartupListener @Inject constructor( + private val eventBus: EventBus, + private val serviceLocator: ServiceLocator, +) : ApplicationEventListener { + + override fun onEvent(event: ApplicationEvent) { + if (event.type == ApplicationEvent.Type.INITIALIZATION_FINISHED) { + val taskListener = serviceLocator.getService(TaskStateEventListener::class.java) + val messageListener = serviceLocator.getService(MessageStateEventListener::class.java) + + eventBus.register(taskListener) + eventBus.register(messageListener) + } + } + + override fun onRequest(requestEvent: RequestEvent?): RequestEventListener? { + return null + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/AppserverConfig.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/AppserverConfig.kt new file mode 100644 index 000000000..be63bc76b --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/AppserverConfig.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.config + +import org.radarbase.appserver.jersey.config.github.GithubConfig +import org.radarbase.appserver.jersey.config.questionnaire.QuestionnaireProtocolConfig +import org.radarbase.jersey.enhancer.EnhancerFactory + +data class AppserverConfig( + val resourceConfig: Class, + val server: ServerConfig, + val auth: AuthConfig = AuthConfig(), + val fcm: FcmServerConfig = FcmServerConfig(), + val github: GithubConfig = GithubConfig(), + val quartz: SchedulerConfig = SchedulerConfig(), + val db: DbConfig = DbConfig(), + val email: EmailConfig = EmailConfig(), + val eventBus: EventBusConfig = EventBusConfig(), + val protocol: QuestionnaireProtocolConfig = QuestionnaireProtocolConfig(), +) : Validation { + override fun validate() { + listOf(auth, server, db).forEach { validation: Validation -> + validation.validate() + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/AuthConfig.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/AuthConfig.kt new file mode 100644 index 000000000..2d30f8108 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/AuthConfig.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.config + +data class AuthConfig( + val resourceName: String = "res_Appserver", + val issuer: String? = null, + val managementPortalUrl: String = "http://localhost:8081/managementportal", + val publicKeyUrls: List? = null, +) : Validation { + override fun validate() { + check(managementPortalUrl.isBlank() || publicKeyUrls.isNullOrEmpty()) { + "At least one of auth.publicKeyUrls or auth.managementPortalUrl must be configured" + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/DbConfig.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/DbConfig.kt new file mode 100644 index 000000000..4f9dd4518 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/DbConfig.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.config + +import org.radarbase.appserver.jersey.entity.DataMessage +import org.radarbase.appserver.jersey.entity.DataMessageStateEvent +import org.radarbase.appserver.jersey.entity.Notification +import org.radarbase.appserver.jersey.entity.NotificationStateEvent +import org.radarbase.appserver.jersey.entity.Project +import org.radarbase.appserver.jersey.entity.Task +import org.radarbase.appserver.jersey.entity.TaskStateEvent +import org.radarbase.appserver.jersey.entity.User +import org.radarbase.appserver.jersey.entity.UserMetrics +import org.radarbase.appserver.jersey.utils.checkInvalidDetails +import org.radarbase.jersey.config.ConfigLoader.copyEnv + +data class DbConfig( + val classes: List = listOf( + Project::class.qualifiedName!!, + User::class.qualifiedName!!, + UserMetrics::class.qualifiedName!!, + Task::class.qualifiedName!!, + Notification::class.qualifiedName!!, + DataMessage::class.qualifiedName!!, + TaskStateEvent::class.qualifiedName!!, + NotificationStateEvent::class.qualifiedName!!, + DataMessageStateEvent::class.qualifiedName!!, + ), + val jdbcDriver: String = "org.postgresql.Driver", + val jdbcUrl: String = "jdbc:postgresql://localhost:5432/appserver", + val username: String = "radar", + val password: String = "radar", + val hibernateDialect: String = "org.hibernate.dialect.PostgreSQLDialect", + val additionalProperties: Map = emptyMap(), + val liquibase: LiquibaseConfig = LiquibaseConfig(), +) : Validation { + fun withEnv(): DbConfig = this + .copyEnv("APPSERVER_JDBC_URL") { + copy(jdbcUrl = it) + } + .copyEnv("APPSERVER_JDBC_USERNAME") { + copy(username = it) + } + .copyEnv("APPSERVER_JDBC_PASSWORD") { + copy(password = it) + } + .copyEnv("APPSERVER_HIBERNATE_DIALECT") { + copy(hibernateDialect = it) + } + .copyEnv("APPSERVER_JDBC_DRIVER") { + copy(jdbcDriver = it) + } + + override fun validate() { + checkInvalidDetails( + { + jdbcDriver.isBlank() || jdbcUrl.isBlank() + }, + { + "JDBC driver and URL must not be null or empty" + }, + ) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/EmailConfig.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/EmailConfig.kt new file mode 100644 index 000000000..411ff4765 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/EmailConfig.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.config + +// TODO: The email work needs to be done, not sure should we use smtp server here in jersey? +data class EmailConfig( + val enabled: Boolean = false, +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/EventBusConfig.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/EventBusConfig.kt new file mode 100644 index 000000000..c15aa90f4 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/EventBusConfig.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.config + +data class EventBusConfig( + val numThreads: Int = 3, +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/FcmServerConfig.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/FcmServerConfig.kt new file mode 100644 index 000000000..e83696871 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/FcmServerConfig.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.config + +data class FcmServerConfig( + val fcmsender: String? = "org.radarbase.appserver.jersey.fcm.downstream.AdminSdkFcmSender", + val credentials: String? = null, +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/LiquibaseConfig.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/LiquibaseConfig.kt new file mode 100644 index 000000000..c68999268 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/LiquibaseConfig.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.config + +data class LiquibaseConfig( + val enabled: Boolean = false, +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/SchedulerConfig.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/SchedulerConfig.kt new file mode 100644 index 000000000..f60fe9c03 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/SchedulerConfig.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.config + +data class SchedulerConfig( + val coroutineDispatcher: String = "io", + val coroutineJob: String = "supervisor-job", +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/ServerConfig.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/ServerConfig.kt new file mode 100644 index 000000000..56719af26 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/ServerConfig.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.config + +import java.net.URI + +data class ServerConfig( + /** Base URL to serve data with. This will determine the base path and the port. */ + val baseUri: URI = URI.create("http://0.0.0.0:8090/kafka/"), + /** + * Maximum time in seconds to wait for a request to complete. + * This timeout is applied to the co-routine context, not to the Grizzly server. + */ + val requestTimeout: Int = 30, + /** + * Whether JMX should be enabled. Disable if not needed, for higher performance. + */ + val isJmxEnabled: Boolean = false, +) : Validation { + override fun validate() { + check(baseUri.toString().isNotBlank()) { "Base URL must not be blank." } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/Validation.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/Validation.kt new file mode 100644 index 000000000..df802921c --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/Validation.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.config + +interface Validation { + fun validate() +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/github/GithubCacheConfig.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/github/GithubCacheConfig.kt new file mode 100644 index 000000000..28484ec10 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/github/GithubCacheConfig.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.config.github + +import com.fasterxml.jackson.annotation.JsonProperty + +data class GithubCacheConfig( + @field:JsonProperty("cacheDurationSec") + val cacheDuration: Long = 3600, + @field:JsonProperty("retryDurationSec") + val retryDuration: Long = 60, + val maxCacheSize: Int = 10000, +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/github/GithubClientConfig.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/github/GithubClientConfig.kt new file mode 100644 index 000000000..5fafb58d8 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/github/GithubClientConfig.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.config.github + +import com.fasterxml.jackson.annotation.JsonProperty + +data class GithubClientConfig( + val maxContentLength: Long = 10_00_000, + @field:JsonProperty("timeoutSec") + val timeout: Long = 10L, + val githubToken: String? = null, +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/github/GithubConfig.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/github/GithubConfig.kt new file mode 100644 index 000000000..a857b2acb --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/github/GithubConfig.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.config.github + +data class GithubConfig( + val cache: GithubCacheConfig = GithubCacheConfig(), + val client: GithubClientConfig = GithubClientConfig(), +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/questionnaire/QuestionnaireProtocolConfig.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/questionnaire/QuestionnaireProtocolConfig.kt new file mode 100644 index 000000000..1cfa8c9b2 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/config/questionnaire/QuestionnaireProtocolConfig.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.config.questionnaire + +data class QuestionnaireProtocolConfig( + val githubProtocolRepo: String = "RADAR-base/RADAR-aRMT-protocols", + val protocolFileName: String = "protocol.json", + val githubBranch: String = "master", +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/DataMessageStateEventDto.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/DataMessageStateEventDto.kt new file mode 100644 index 000000000..45683b847 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/DataMessageStateEventDto.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import org.radarbase.appserver.jersey.event.state.MessageState +import java.time.Instant + +@JsonIgnoreProperties(ignoreUnknown = true) +data class DataMessageStateEventDto( + var id: Long? = null, + var dataMessageId: Long? = null, + var state: MessageState? = null, + var time: Instant? = null, + var associatedInfo: String? = null, +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/NotificationStateEventDto.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/NotificationStateEventDto.kt new file mode 100644 index 000000000..e3b86d7cb --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/NotificationStateEventDto.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import org.radarbase.appserver.jersey.event.state.MessageState +import java.time.Instant + +@JsonIgnoreProperties(ignoreUnknown = true) +data class NotificationStateEventDto( + var id: Long? = null, + var notificationId: Long? = null, + var state: MessageState? = null, + var time: Instant? = null, + var associatedInfo: String? = null, +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/ProjectDto.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/ProjectDto.kt new file mode 100644 index 000000000..22efbda76 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/ProjectDto.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto + +import com.fasterxml.jackson.annotation.JsonFormat +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import jakarta.validation.constraints.NotEmpty +import jakarta.validation.constraints.NotNull +import java.time.Instant + +/** + * Data Transfer Object representing a [ManagementPortal](https://github.com/Radar-base/ManagementPortal) Project. + * + * This DTO is used to encapsulate project-related information, + * including its unique identifier, project ID, and timestamps for + * creation and updates. The timestamps follow ISO date-time format. + * + */ +@JsonIgnoreProperties(ignoreUnknown = true) +data class ProjectDto( + var id: Long? = null, + + @field:NotNull + @field:NotEmpty + var projectId: String? = null, + + @field:JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", + timezone = "UTC", + ) + var createdAt: Instant? = null, + + @field:JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", + timezone = "UTC", + ) + var updatedAt: Instant? = null, +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/ProjectDtos.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/ProjectDtos.kt new file mode 100644 index 000000000..79a5e500d --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/ProjectDtos.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto + +import jakarta.validation.constraints.Size + +data class ProjectDtos( + @field:Size(max = 500) + val projects: MutableList = mutableListOf(), +) { + fun withProjects(projects: List): ProjectDtos = apply { + this.projects.clear() + this.projects.addAll(projects) + } + + fun addProject(projectDto: ProjectDto): ProjectDtos = apply { + this.projects.add(projectDto) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/TaskStateEventDto.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/TaskStateEventDto.kt new file mode 100644 index 000000000..276509147 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/TaskStateEventDto.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import org.radarbase.appserver.jersey.event.state.TaskState +import java.time.Instant + +@JsonIgnoreProperties(ignoreUnknown = true) +data class TaskStateEventDto( + var id: Long? = null, + var taskId: Long? = null, + var state: TaskState? = null, + var time: Instant? = null, + var associatedInfo: String? = null, +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/fcm/FcmDataMessageDto.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/fcm/FcmDataMessageDto.kt new file mode 100644 index 000000000..cfbd66592 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/fcm/FcmDataMessageDto.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto.fcm + +import com.fasterxml.jackson.annotation.JsonFormat +import jakarta.validation.constraints.NotEmpty +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Size +import org.radarbase.appserver.jersey.entity.DataMessage +import java.time.Instant +import java.util.Objects + +class FcmDataMessageDto(dataMessageEntity: DataMessage? = null) { + var id: Long? = dataMessageEntity?.id + + @field:JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", + timezone = "UTC", + ) + var scheduledTime: @NotNull Instant? = dataMessageEntity?.scheduledTime + + var delivered: Boolean = dataMessageEntity?.delivered == true + + var ttlSeconds: Int = dataMessageEntity?.ttlSeconds ?: 0 + + @field:NotEmpty + var sourceId: String? = dataMessageEntity?.sourceId + + var fcmMessageId: String? = dataMessageEntity?.fcmMessageId + + var fcmTopic: String? = dataMessageEntity?.fcmTopic + + // for use with the FCM admin SDK + var fcmCondition: String? = dataMessageEntity?.fcmCondition + + @field:NotEmpty + var appPackage: String? = dataMessageEntity?.appPackage + + @field:NotEmpty + var sourceType: String? = dataMessageEntity?.sourceType + + @field:Size(max = 100) + var dataMap: MutableMap? = dataMessageEntity?.dataMap + + var priority: String? = dataMessageEntity?.priority + + var mutableContent: Boolean = dataMessageEntity?.mutableContent == true + + @field:JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", + timezone = "UTC", + ) + var createdAt: Instant? = dataMessageEntity?.createdAt?.toInstant() + + @field:JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", + timezone = "UTC", + ) + var updatedAt: Instant? = dataMessageEntity?.updatedAt?.toInstant() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is FcmDataMessageDto) return false + val that = other + return delivered == that.delivered && ttlSeconds == that.ttlSeconds && scheduledTime == that.scheduledTime && + appPackage == that.appPackage && + sourceType == that.sourceType + } + + override fun hashCode(): Int { + return Objects.hash( + scheduledTime, + delivered, + ttlSeconds, + appPackage, + sourceType, + ) + } + + override fun toString(): String { + return "FcmDataMessageDto(id=$id, scheduledTime=$scheduledTime, delivered=$delivered, ttlSeconds=$ttlSeconds, sourceId=$sourceId, fcmMessageId=$fcmMessageId, fcmTopic=$fcmTopic, fcmCondition=$fcmCondition, appPackage=$appPackage, sourceType=$sourceType, dataMap=$dataMap, priority=$priority, mutableContent=$mutableContent, createdAt=$createdAt, updatedAt=$updatedAt)" + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/fcm/FcmDataMessages.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/fcm/FcmDataMessages.kt new file mode 100644 index 000000000..a42f409ef --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/fcm/FcmDataMessages.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto.fcm + +import jakarta.validation.constraints.Size +import org.radarbase.appserver.jersey.utils.equalTo +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.util.Objects + +class FcmDataMessages( + @field:Size(max = 200) + private val _dataMessages: MutableList, +) { + + val dataMessages: List + get() = _dataMessages + + fun addDataMessage(dataMessageDto: FcmDataMessageDto): FcmDataMessages { + if (!_dataMessages.contains(dataMessageDto)) { + this._dataMessages.add(dataMessageDto) + } else { + logger.info("Data message {} already exists in the Fcm Data Messages.", dataMessageDto) + } + return this + } + + override fun equals(other: Any?): Boolean = equalTo( + other, + FcmDataMessages::_dataMessages, + ) + + override fun hashCode(): Int { + return Objects.hash(_dataMessages) + } + + companion object { + private val logger: Logger = LoggerFactory.getLogger(FcmDataMessages::class.java) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/fcm/FcmNotificationDto.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/fcm/FcmNotificationDto.kt new file mode 100644 index 000000000..8cfd353dc --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/fcm/FcmNotificationDto.kt @@ -0,0 +1,152 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto.fcm + +import com.fasterxml.jackson.annotation.JsonFormat +import jakarta.validation.constraints.NotEmpty +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Size +import org.radarbase.appserver.jersey.entity.Notification +import org.radarbase.appserver.jersey.utils.equalTo +import java.time.Instant +import java.util.Objects + +class FcmNotificationDto(notificationEntity: Notification? = null) { + var id: Long? = notificationEntity?.id + + @field:NotNull + @field:JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", + timezone = "UTC", + ) + var scheduledTime: Instant? = notificationEntity?.scheduledTime + + var delivered: Boolean = notificationEntity?.delivered == true + + @field:NotEmpty + var title: String? = notificationEntity?.title + + var body: String? = notificationEntity?.body + + var ttlSeconds: Int = notificationEntity?.ttlSeconds ?: 0 + + @field:NotEmpty + var sourceId: String? = notificationEntity?.sourceId + + var fcmMessageId: String? = notificationEntity?.fcmMessageId + + var fcmTopic: String? = notificationEntity?.fcmTopic + + // for use with the FCM admin SDK + var fcmCondition: String? = notificationEntity?.fcmCondition + + @field:NotEmpty + var type: String? = notificationEntity?.type + + @field:NotEmpty + var appPackage: String? = notificationEntity?.appPackage + + @field:NotEmpty + var sourceType: String? = notificationEntity?.sourceType + + @field:Size(max = 100) + var additionalData: Map? = notificationEntity?.additionalData + + var priority: String? = notificationEntity?.priority + + var sound: String? = notificationEntity?.sound + + // For IOS + var badge: String? = notificationEntity?.badge + + // For IOS + var subtitle: String? = notificationEntity?.subtitle + + // For android + var icon: String? = notificationEntity?.icon + + // For android. Color of the icon + var color: String? = notificationEntity?.color + + var bodyLocKey: String? = notificationEntity?.bodyLocKey + + var bodyLocArgs: String? = notificationEntity?.bodyLocArgs + + var titleLocKey: String? = notificationEntity?.titleLocKey + + var titleLocArgs: String? = notificationEntity?.titleLocArgs + + // For android + var androidChannelId: String? = notificationEntity?.androidChannelId + + // For android + var tag: String? = notificationEntity?.tag + + var clickAction: String? = notificationEntity?.clickAction + + var emailEnabled: Boolean = notificationEntity?.emailEnabled == true + + var emailTitle: String? = notificationEntity?.emailTitle + + var emailBody: String? = notificationEntity?.emailBody + + var mutableContent: Boolean = notificationEntity?.mutableContent == true + + @field:JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", + timezone = "UTC", + ) + var createdAt: Instant? = notificationEntity?.createdAt?.toInstant() + + @field:JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", + timezone = "UTC", + ) + var updatedAt: Instant? = notificationEntity?.updatedAt?.toInstant() + + override fun equals(other: Any?): Boolean = equalTo( + other, + FcmNotificationDto::delivered, + FcmNotificationDto::ttlSeconds, + FcmNotificationDto::scheduledTime, + FcmNotificationDto::title, + FcmNotificationDto::body, + FcmNotificationDto::type, + FcmNotificationDto::appPackage, + FcmNotificationDto::sourceType, + ) + + override fun hashCode(): Int { + return Objects.hash( + scheduledTime, + delivered, + title, + body, + ttlSeconds, + type, + appPackage, + sourceType, + ) + } + + override fun toString(): String { + return "FcmNotificationDto(id=$id, scheduledTime=$scheduledTime, delivered=$delivered, title=$title, body=$body, ttlSeconds=$ttlSeconds, sourceId=$sourceId, fcmMessageId=$fcmMessageId, fcmTopic=$fcmTopic, fcmCondition=$fcmCondition, type=$type, appPackage=$appPackage, sourceType=$sourceType, additionalData=$additionalData, priority=$priority, sound=$sound, badge=$badge, subtitle=$subtitle, icon=$icon, color=$color, bodyLocKey=$bodyLocKey, bodyLocArgs=$bodyLocArgs, titleLocKey=$titleLocKey, titleLocArgs=$titleLocArgs, androidChannelId=$androidChannelId, tag=$tag, clickAction=$clickAction, emailEnabled=$emailEnabled, emailTitle=$emailTitle, emailBody=$emailBody, mutableContent=$mutableContent, createdAt=$createdAt, updatedAt=$updatedAt)" + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/fcm/FcmNotifications.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/fcm/FcmNotifications.kt new file mode 100644 index 000000000..1c1e3fa64 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/fcm/FcmNotifications.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto.fcm + +import jakarta.validation.constraints.Size +import org.radarbase.appserver.jersey.utils.equalTo +import org.radarbase.appserver.jersey.utils.stringRepresentation +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.util.Objects + +class FcmNotifications( + @field:Size(max = 200) + private val _notifications: MutableList, +) { + + val notifications: List + get() = _notifications.toMutableList() + + fun addNotification(notificationDto: FcmNotificationDto): FcmNotifications = apply { + if (!_notifications.contains(notificationDto)) { + this._notifications.add(notificationDto) + } else { + logger.info("Notification {} already exists in the Fcm Notifications.", notificationDto) + } + } + + override fun equals(other: Any?): Boolean = equalTo( + other, + FcmNotifications::_notifications, + ) + + override fun toString(): String = stringRepresentation( + FcmNotifications::_notifications, + ) + + override fun hashCode(): Int { + return Objects.hash(_notifications) + } + + companion object { + private val logger: Logger = LoggerFactory.getLogger(FcmNotifications::class.java) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/fcm/FcmUserDto.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/fcm/FcmUserDto.kt new file mode 100644 index 000000000..aad284158 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/fcm/FcmUserDto.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto.fcm + +import com.fasterxml.jackson.annotation.JsonFormat +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.NotEmpty +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Size +import org.radarbase.appserver.jersey.entity.User +import java.time.Instant + +@Suppress("unused") +data class FcmUserDto( + var id: Long? = null, + + /** + * Project ID to be used in org.radarcns.kafka.ObservationKey record keys + */ + var projectId: String? = null, + + /** + * User ID to be used in org.radarcns.kafka.ObservationKey record keys + */ + @field:NotEmpty + var subjectId: String? = null, + + /** + * Email address of the user (optional, needed when sending notifications via email) + */ + @field:Email + var email: String? = null, + + /** + * The most recent time when the app was opened + */ + var lastOpened: Instant? = null, + + /** + * The most recent time when a notification for the app was delivered + */ + var lastDelivered: Instant? = null, + + @field:JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", + timezone = "UTC", + ) + var createdAt: Instant? = null, + + @field:JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", + timezone = "UTC", + ) + var updatedAt: Instant? = null, + + @field:NotNull + @field:JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", + timezone = "UTC", + ) + var enrolmentDate: Instant? = null, + + @field:NotNull + var timezone: String? = null, + + /** + * Timezone of the user based on tz database names + */ + var fcmToken: String? = null, + + var language: String? = null, + + @field:Size(max = 100) + var attributes: Map? = null, +) { + constructor(user: User) : this( + id = user.id, + projectId = user.project?.projectId, + subjectId = user.subjectId, + email = user.emailAddress, + lastOpened = user.usermetrics?.lastOpened, + lastDelivered = user.usermetrics?.lastDelivered, + createdAt = user.createdAt?.toInstant(), + updatedAt = user.updatedAt?.toInstant(), + enrolmentDate = user.enrolmentDate, + timezone = user.timezone, + fcmToken = user.fcmToken, + language = user.language, + attributes = user.attributes, + ) } diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/fcm/FcmUsers.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/fcm/FcmUsers.kt new file mode 100644 index 000000000..c9e93bd72 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/fcm/FcmUsers.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto.fcm + +import jakarta.validation.constraints.Size + +data class FcmUsers( + @field:Size(max = 1500) + val users: List, +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/Assessment.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/Assessment.kt new file mode 100644 index 000000000..996730202 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/Assessment.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto.protocol + +import jakarta.persistence.Column +import kotlinx.serialization.Serializable + +/** + * Data Transfer object (DTO) for Assessment. A project may represent a Protocol for scheduling + * questionnaires. + * + * @see aRMT Protocols + * @see Protocol + */ + +@Serializable +data class Assessment( + var name: String? = null, + private var _type: AssessmentType? = null, + var showIntroduction: String? = null, + var questionnaire: DefinitionInfo? = null, + var startText: LanguageText? = null, + var endText: LanguageText? = null, + var warn: LanguageText? = null, + var estimatedCompletionTime: Int? = null, + var protocol: AssessmentProtocol? = null, + @Column(name = "\"order\"") + var order: Int = 0, + var nQuestions: Int = 0, + var showInCalendar: Boolean = true, + var isDemo: Boolean = false, +) { + var type: AssessmentType? + get() { + return _type + ?: if (protocol?.clinicalProtocol != null) AssessmentType.CLINICAL else AssessmentType.SCHEDULED + } + set(value) { + _type = value + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/AssessmentProtocol.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/AssessmentProtocol.kt new file mode 100644 index 000000000..5c312f1f4 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/AssessmentProtocol.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto.protocol + +import kotlinx.serialization.Serializable +import org.radarbase.appserver.jersey.serialization.ReferenceTimestampSerializer + +@Serializable +data class AssessmentProtocol( + var repeatProtocol: RepeatProtocol? = null, + var reminders: ReminderTimePeriod? = null, + var completionWindow: TimePeriod? = null, + var repeatQuestionnaire: RepeatQuestionnaire? = null, + @Serializable(with = ReferenceTimestampSerializer::class) + var referenceTimestamp: ReferenceTimestamp? = null, + var clinicalProtocol: ClinicalProtocol? = null, + var notification: NotificationProtocol = NotificationProtocol(), +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/AssessmentType.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/AssessmentType.kt new file mode 100644 index 000000000..c8800312a --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/AssessmentType.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto.protocol + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +enum class AssessmentType { + @SerialName("scheduled") + SCHEDULED, + + @SerialName("clinical") + CLINICAL, + + @SerialName("triggered") + TRIGGERED, + + @SerialName("all") + ALL, +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/ClinicalProtocol.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/ClinicalProtocol.kt new file mode 100644 index 000000000..ae45e64a0 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/ClinicalProtocol.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto.protocol + +import kotlinx.serialization.Serializable + +@Serializable +data class ClinicalProtocol( + var requiresInClinicCompletion: Boolean = false, + var repeatAfterClinicVisit: RepeatQuestionnaire? = null, +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/DefinitionInfo.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/DefinitionInfo.kt new file mode 100644 index 000000000..7aee2b4d8 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/DefinitionInfo.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto.protocol + +import kotlinx.serialization.Serializable +import org.radarbase.appserver.jersey.serialization.URISerializer +import java.net.URI + +@Serializable +data class DefinitionInfo( + @Serializable(with = URISerializer::class) + val repository: URI? = null, + val name: String? = null, + val avsc: String? = null, +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/EmailNotificationProtocol.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/EmailNotificationProtocol.kt new file mode 100644 index 000000000..e15d08527 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/EmailNotificationProtocol.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto.protocol + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class EmailNotificationProtocol( + @SerialName("enabled") + val enabled: Boolean = false, + + @SerialName("title") + var title: LanguageText? = null, + + @SerialName("text") + var body: LanguageText? = null, +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/GithubContent.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/GithubContent.kt new file mode 100644 index 000000000..88490320d --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/GithubContent.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto.protocol + +import kotlinx.serialization.Serializable +import org.radarbase.appserver.jersey.serialization.Base64AsStringSerializer + +@Serializable +data class GithubContent( + @Serializable(with = Base64AsStringSerializer::class) + var content: String? = null, + var sha: String? = null, + var size: String? = null, + var url: String? = null, + var node_id: String? = null, + var encoding: String? = null, +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/LanguageText.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/LanguageText.kt new file mode 100644 index 000000000..c719a1f6b --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/LanguageText.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto.protocol + +import kotlinx.serialization.Serializable +import java.util.Locale + +/** + * Data Transfer object (DTO) for LanguageText. Handles multi-language support for text. + */ +@Serializable +data class LanguageText( + var en: String? = null, + var it: String? = null, + var nl: String? = null, + var da: String? = null, + var de: String? = null, + var es: String? = null, +) { + fun getText(languageCode: String?): String { + return when (languageCode?.lowercase(Locale.getDefault())) { + "en" -> en + "it" -> it + "nl" -> nl + "da" -> da + "de" -> de + "es" -> es + else -> "" + } ?: "" + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/NotificationProtocol.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/NotificationProtocol.kt new file mode 100644 index 000000000..14a3e8e3c --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/NotificationProtocol.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto.protocol + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class NotificationProtocol( + @SerialName("mode") + var mode: NotificationProtocolMode = NotificationProtocolMode.STANDARD, + @SerialName("title") + var title: LanguageText? = null, + @SerialName("text") + var body: LanguageText? = null, + @SerialName("email") + var email: EmailNotificationProtocol = EmailNotificationProtocol(), +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/NotificationProtocolMode.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/NotificationProtocolMode.kt new file mode 100644 index 000000000..54072fb43 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/NotificationProtocolMode.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto.protocol + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +enum class NotificationProtocolMode { + @SerialName("standard") + STANDARD, + + @SerialName("disabled") + DISABLED, + + @SerialName("combined") + COMBINED, +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/Protocol.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/Protocol.kt new file mode 100644 index 000000000..57b55cbbe --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/Protocol.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto.protocol + +import kotlinx.serialization.Serializable + +/** + * Data Transfer object (DTO) for Protocol. A project may represent a `Protocol` for scheduling + * questionnaires. + * + * @see aRMT Protocols + */ +@Serializable +data class Protocol( + var version: String? = null, + var schemaVersion: String? = null, + var name: String? = null, + var healthIssues: List? = null, + var protocols: List? = null, +) { + fun hasAssessment(assessment: String?): Boolean { + return protocols?.any { it.name == assessment } == true + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/ProtocolCacheEntry.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/ProtocolCacheEntry.kt new file mode 100644 index 000000000..ceb99ff52 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/ProtocolCacheEntry.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto.protocol + +import kotlinx.serialization.Serializable + +@Serializable +data class ProtocolCacheEntry(val id: String, val protocol: Protocol?) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/ReferenceTimestamp.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/ReferenceTimestamp.kt new file mode 100644 index 000000000..934e13560 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/ReferenceTimestamp.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto.protocol + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ReferenceTimestamp( + @SerialName("timestamp") + var timestamp: String? = null, + @SerialName("format") + var format: ReferenceTimestampType? = null, +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/ReferenceTimestampType.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/ReferenceTimestampType.kt new file mode 100644 index 000000000..9f311d8c5 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/ReferenceTimestampType.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto.protocol + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Suppress("unused") +@Serializable +enum class ReferenceTimestampType { + @SerialName("date") + DATE, + + @SerialName("datetime") + DATETIME, + + @SerialName("datetimeutc") + DATETIMEUTC, + + @SerialName("now") + NOW, + + @SerialName("today") + TODAY, +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/ReminderTimePeriod.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/ReminderTimePeriod.kt new file mode 100644 index 000000000..c3bb28d63 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/ReminderTimePeriod.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto.protocol + +import kotlinx.serialization.Serializable +import org.radarbase.appserver.jersey.utils.equalTo +import org.radarbase.appserver.jersey.utils.stringRepresentation +import java.util.Objects + +@Serializable +class ReminderTimePeriod( + val repeat: Int? = null, +) : TimePeriod() { + override fun toString(): String = stringRepresentation( + ReminderTimePeriod::amount, + ReminderTimePeriod::unit, + ReminderTimePeriod::repeat, + ) + + override fun equals(other: Any?): Boolean = equalTo( + other, + ReminderTimePeriod::repeat, + ) + + override fun hashCode(): Int { + return Objects.hash(repeat) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/RepeatProtocol.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/RepeatProtocol.kt new file mode 100644 index 000000000..db08dc988 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/RepeatProtocol.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto.protocol + +import jakarta.validation.constraints.NotNull +import kotlinx.serialization.Serializable +import org.radarbase.appserver.jersey.utils.annotation.CheckExactlyOneNotNull + +@CheckExactlyOneNotNull(fieldNames = ["amount", "randomAmountBetween"]) +@Serializable +data class RepeatProtocol( + @field:NotNull + var unit: String? = null, + var amount: Int? = null, + var randomAmountBetween: Array? = null, + var dayOfWeek: String? = null, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RepeatProtocol + + if (amount != other.amount) return false + if (unit != other.unit) return false + if (!randomAmountBetween.contentEquals(other.randomAmountBetween)) return false + if (dayOfWeek != other.dayOfWeek) return false + + return true + } + + override fun hashCode(): Int { + var result = amount ?: 0 + result = 31 * result + (unit?.hashCode() ?: 0) + result = 31 * result + (randomAmountBetween?.contentHashCode() ?: 0) + result = 31 * result + (dayOfWeek?.hashCode() ?: 0) + return result + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/RepeatQuestionnaire.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/RepeatQuestionnaire.kt new file mode 100644 index 000000000..709e20edf --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/RepeatQuestionnaire.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto.protocol + +import jakarta.validation.constraints.NotNull +import kotlinx.serialization.Serializable +import org.radarbase.appserver.jersey.utils.annotation.CheckExactlyOneNotNull + +/** + * Data Transfer object (DTO) for RepeatQuestionnaire. Handles repeat configurations for questionnaires. + */ +@CheckExactlyOneNotNull(fieldNames = ["unitsFromZero", "randomUnitsFromZeroBetween", "dayOfWeekMap"]) +@Serializable +data class RepeatQuestionnaire( + @field:NotNull + var unit: String? = null, + var unitsFromZero: List? = null, + var randomUnitsFromZeroBetween: List>? = null, + var dayOfWeekMap: Map? = null, +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/TimePeriod.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/TimePeriod.kt new file mode 100644 index 000000000..b272904f3 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/protocol/TimePeriod.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto.protocol + +import kotlinx.serialization.Serializable +import org.radarbase.appserver.jersey.utils.equalTo +import org.radarbase.appserver.jersey.utils.stringRepresentation +import java.util.Objects + +@Serializable +open class TimePeriod(var unit: String? = null, var amount: Int? = null) { + override fun toString(): String = stringRepresentation( + TimePeriod::amount, + TimePeriod::unit, + ) + + override fun equals(other: Any?): Boolean = equalTo( + other, + TimePeriod::amount, + TimePeriod::unit, + ) + + override fun hashCode(): Int { + return Objects.hash(amount, unit) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/questionnaire/AssessmentSchedule.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/questionnaire/AssessmentSchedule.kt new file mode 100644 index 000000000..c36beb3ea --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/questionnaire/AssessmentSchedule.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto.questionnaire + +import org.radarbase.appserver.jersey.entity.Notification +import org.radarbase.appserver.jersey.entity.Task +import java.time.Instant + +data class AssessmentSchedule( + var name: String? = null, + var referenceTimestamp: Instant? = null, + var referenceTimestamps: List? = null, + var tasks: List? = null, + var notifications: List? = null, + var reminders: List? = null, +) { + fun hasTasks(): Boolean { + return !tasks.isNullOrEmpty() + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/questionnaire/Schedule.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/questionnaire/Schedule.kt new file mode 100644 index 000000000..72fcc6793 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/dto/questionnaire/Schedule.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.dto.questionnaire + +import org.radarbase.appserver.jersey.entity.User + +@Suppress("unused") +data class Schedule( + var assessmentSchedules: MutableList = mutableListOf(), + var user: User? = null, + var version: String = "0.0.0", + var timezone: String? = user?.timezone, +) { + constructor(user: User) : this(mutableListOf(), user, "0.0.0", user.timezone) + + constructor(user: User, assessmentSchedules: List) : this( + assessmentSchedules.toMutableList(), + user, + "0.0.0", + user.timezone, + ) + + constructor( + assessmentSchedules: List, + user: User, + version: String?, + ) : this( + assessmentSchedules.toMutableList(), + user, + version ?: "0.0.0", + user.timezone, + ) + + fun addAssessmentSchedule(assessmentSchedule: AssessmentSchedule): Schedule = apply { + this.assessmentSchedules.add(assessmentSchedule) + } + + fun addAssessmentSchedules(assessmentSchedules: List): Schedule = apply { + this.assessmentSchedules.addAll(assessmentSchedules) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/enhancer/AppserverResourceEnhancer.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/enhancer/AppserverResourceEnhancer.kt new file mode 100644 index 000000000..21469206c --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/enhancer/AppserverResourceEnhancer.kt @@ -0,0 +1,323 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.enhancer + +import com.google.common.eventbus.EventBus +import com.google.firebase.FirebaseOptions +import jakarta.inject.Singleton +import kotlinx.coroutines.CoroutineScope +import org.glassfish.hk2.api.TypeLiteral +import org.glassfish.jersey.internal.inject.AbstractBinder +import org.glassfish.jersey.server.ResourceConfig +import org.glassfish.jersey.server.validation.ValidationFeature +import org.quartz.JobListener +import org.quartz.Scheduler +import org.quartz.SchedulerListener +import org.radarbase.appserver.jersey.application.event.EventBusStartupListener +import org.radarbase.appserver.jersey.config.AppserverConfig +import org.radarbase.appserver.jersey.config.FcmServerConfig +import org.radarbase.appserver.jersey.dto.ProjectDto +import org.radarbase.appserver.jersey.dto.fcm.FcmDataMessageDto +import org.radarbase.appserver.jersey.dto.fcm.FcmNotificationDto +import org.radarbase.appserver.jersey.dto.fcm.FcmUserDto +import org.radarbase.appserver.jersey.entity.DataMessage +import org.radarbase.appserver.jersey.entity.Notification +import org.radarbase.appserver.jersey.entity.Project +import org.radarbase.appserver.jersey.entity.User +import org.radarbase.appserver.jersey.event.listener.MessageStateEventListener +import org.radarbase.appserver.jersey.event.listener.TaskStateEventListener +import org.radarbase.appserver.jersey.event.listener.quartz.QuartzMessageJobListener +import org.radarbase.appserver.jersey.event.listener.quartz.QuartzMessageSchedulerListener +import org.radarbase.appserver.jersey.exception.handler.UnhandledExceptionMapper +import org.radarbase.appserver.jersey.factory.coroutines.SchedulerScopedCoroutine +import org.radarbase.appserver.jersey.factory.event.EventBusFactory +import org.radarbase.appserver.jersey.factory.fcm.FcmSenderFactory +import org.radarbase.appserver.jersey.factory.fcm.FirebaseOptionsFactory +import org.radarbase.appserver.jersey.factory.quartz.QuartzSchedulerFactory +import org.radarbase.appserver.jersey.factory.scheduling.SchedulingServiceFactory +import org.radarbase.appserver.jersey.fcm.downstream.FcmSender +import org.radarbase.appserver.jersey.mapper.DataMessageMapper +import org.radarbase.appserver.jersey.mapper.Mapper +import org.radarbase.appserver.jersey.mapper.NotificationMapper +import org.radarbase.appserver.jersey.mapper.ProjectMapper +import org.radarbase.appserver.jersey.mapper.UserMapper +import org.radarbase.appserver.jersey.repository.DataMessageRepository +import org.radarbase.appserver.jersey.repository.DataMessageStateEventRepository +import org.radarbase.appserver.jersey.repository.NotificationRepository +import org.radarbase.appserver.jersey.repository.NotificationStateEventRepository +import org.radarbase.appserver.jersey.repository.ProjectRepository +import org.radarbase.appserver.jersey.repository.TaskRepository +import org.radarbase.appserver.jersey.repository.TaskStateEventRepository +import org.radarbase.appserver.jersey.repository.UserRepository +import org.radarbase.appserver.jersey.repository.impl.DataMessageRepositoryImpl +import org.radarbase.appserver.jersey.repository.impl.DataMessageStateEventRepositoryImpl +import org.radarbase.appserver.jersey.repository.impl.NotificationRepositoryImpl +import org.radarbase.appserver.jersey.repository.impl.NotificationStateEventRepositoryImpl +import org.radarbase.appserver.jersey.repository.impl.ProjectRepositoryImpl +import org.radarbase.appserver.jersey.repository.impl.TaskRepositoryImpl +import org.radarbase.appserver.jersey.repository.impl.TaskStateEventRepositoryImpl +import org.radarbase.appserver.jersey.repository.impl.UserRepositoryImpl +import org.radarbase.appserver.jersey.service.DataMessageStateEventService +import org.radarbase.appserver.jersey.service.FcmDataMessageService +import org.radarbase.appserver.jersey.service.FcmNotificationService +import org.radarbase.appserver.jersey.service.NotificationStateEventService +import org.radarbase.appserver.jersey.service.ProjectService +import org.radarbase.appserver.jersey.service.TaskService +import org.radarbase.appserver.jersey.service.TaskStateEventService +import org.radarbase.appserver.jersey.service.UserService +import org.radarbase.appserver.jersey.service.github.GithubClient +import org.radarbase.appserver.jersey.service.github.GithubService +import org.radarbase.appserver.jersey.service.github.protocol.ProtocolFetcherStrategy +import org.radarbase.appserver.jersey.service.github.protocol.ProtocolGenerator +import org.radarbase.appserver.jersey.service.github.protocol.impl.DefaultProtocolGenerator +import org.radarbase.appserver.jersey.service.github.protocol.impl.GithubProtocolFetcherStrategy +import org.radarbase.appserver.jersey.service.quartz.QuartzNamingStrategy +import org.radarbase.appserver.jersey.service.quartz.SchedulerService +import org.radarbase.appserver.jersey.service.quartz.SchedulerServiceImpl +import org.radarbase.appserver.jersey.service.quartz.SimpleQuartzNamingStrategy +import org.radarbase.appserver.jersey.service.questionnaire.schedule.MessageSchedulerService +import org.radarbase.appserver.jersey.service.questionnaire.schedule.QuestionnaireScheduleGeneratorService +import org.radarbase.appserver.jersey.service.questionnaire.schedule.QuestionnaireScheduleService +import org.radarbase.appserver.jersey.service.questionnaire.schedule.ScheduleGeneratorService +import org.radarbase.appserver.jersey.service.scheduling.SchedulingService +import org.radarbase.appserver.jersey.service.transmitter.DataMessageTransmitter +import org.radarbase.appserver.jersey.service.transmitter.FcmTransmitter +import org.radarbase.appserver.jersey.service.transmitter.NotificationTransmitter +import org.radarbase.jersey.enhancer.JerseyResourceEnhancer + +class AppserverResourceEnhancer(private val config: AppserverConfig) : JerseyResourceEnhancer { + + override val packages: Array + get() = arrayOf( + "org.radarbase.appserver.jersey.resource", + ) + + override val classes: Array> + get() = super.classes + + override fun AbstractBinder.enhance() { + bind(config) + .to(AppserverConfig::class.java) + .`in`(Singleton::class.java) + + bind(config.fcm) + .to(FcmServerConfig::class.java) + .`in`(Singleton::class.java) + + bind(ProjectRepositoryImpl::class.java) + .to(ProjectRepository::class.java) + .`in`(Singleton::class.java) + + bind(UserRepositoryImpl::class.java) + .to(UserRepository::class.java) + .`in`(Singleton::class.java) + + bind(DataMessageRepositoryImpl::class.java) + .to(DataMessageRepository::class.java) + .`in`(Singleton::class.java) + + bind(NotificationRepositoryImpl::class.java) + .to(NotificationRepository::class.java) + .`in`(Singleton::class.java) + + bind(TaskRepositoryImpl::class.java) + .to(TaskRepository::class.java) + .`in`(Singleton::class.java) + + bind(DataMessageStateEventRepositoryImpl::class.java) + .to(DataMessageStateEventRepository::class.java) + .`in`(Singleton::class.java) + + bind(NotificationStateEventRepositoryImpl::class.java) + .to(NotificationStateEventRepository::class.java) + .`in`(Singleton::class.java) + + bind(TaskStateEventRepositoryImpl::class.java) + .to(TaskStateEventRepository::class.java) + .`in`(Singleton::class.java) + + bind(TaskStateEventService::class.java) + .to(TaskStateEventService::class.java) + .`in`(Singleton::class.java) + + bind(ProjectMapper::class.java) + .to(object : TypeLiteral>() {}.type) + .named(PROJECT_MAPPER) + .`in`(Singleton::class.java) + + bind(UserMapper::class.java) + .to(object : TypeLiteral>() {}.type) + .named(USER_MAPPER) + .`in`(Singleton::class.java) + + bind(DataMessageMapper::class.java) + .to(object : TypeLiteral>() {}.type) + .named(DATA_MESSAGE_MAPPER) + .`in`(Singleton::class.java) + + bind(NotificationMapper::class.java) + .to(object : TypeLiteral>() {}.type) + .named(NOTIFICATION_MAPPER) + .`in`(Singleton::class.java) + + bind(ProjectService::class.java) + .to(ProjectService::class.java) + .`in`(Singleton::class.java) + + bind(UserService::class.java) + .to(UserService::class.java) + .`in`(Singleton::class.java) + + bind(TaskService::class.java) + .to(TaskService::class.java) + .`in`(Singleton::class.java) + + bind(FcmDataMessageService::class.java) + .to(FcmDataMessageService::class.java) + .`in`(Singleton::class.java) + + bind(FcmNotificationService::class.java) + .to(FcmNotificationService::class.java) + .`in`(Singleton::class.java) + + bind(GithubClient::class.java) + .to(GithubClient::class.java) + .`in`(Singleton::class.java) + + bind(GithubService::class.java) + .to(GithubService::class.java) + .`in`(Singleton::class.java) + + bind(GithubProtocolFetcherStrategy::class.java) + .to(ProtocolFetcherStrategy::class.java) + .`in`(Singleton::class.java) + + bind(DefaultProtocolGenerator::class.java) + .to(ProtocolGenerator::class.java) + .`in`(Singleton::class.java) + + bind(QuestionnaireScheduleGeneratorService::class.java) + .to(ScheduleGeneratorService::class.java) + .`in`(Singleton::class.java) + + bind(QuestionnaireScheduleService::class.java) + .to(QuestionnaireScheduleService::class.java) + .`in`(Singleton::class.java) + + bind(QuartzMessageSchedulerListener::class.java) + .to(SchedulerListener::class.java) + .`in`(Singleton::class.java) + + bind(QuartzMessageJobListener::class.java) + .to(JobListener::class.java) + .`in`(Singleton::class.java) + + bind(SchedulerServiceImpl::class.java) + .to(SchedulerService::class.java) + .`in`(Singleton::class.java) + + bind(MessageSchedulerService::class.java) + .to(object : TypeLiteral>() {}.type) + .`in`(Singleton::class.java) + + bind(MessageSchedulerService::class.java) + .to(object : TypeLiteral>() {}.type) + .`in`(Singleton::class.java) + + bind(SimpleQuartzNamingStrategy::class.java) + .to(QuartzNamingStrategy::class.java) + .`in`(Singleton::class.java) + + bind(FcmTransmitter::class.java) + .to(NotificationTransmitter::class.java) + .`in`(Singleton::class.java) + + bind(FcmTransmitter::class.java) + .to(DataMessageTransmitter::class.java) + .`in`(Singleton::class.java) + + bind(TaskStateEventListener::class.java) + .to(TaskStateEventListener::class.java) + .`in`(Singleton::class.java) + + bind(MessageStateEventListener::class.java) + .to(MessageStateEventListener::class.java) + .`in`(Singleton::class.java) + + bind(DataMessageStateEventService::class.java) + .to(DataMessageStateEventService::class.java) + .`in`(Singleton::class.java) + + bind(NotificationStateEventService::class.java) + .to(NotificationStateEventService::class.java) + .`in`(Singleton::class.java) + + bindFactory(SchedulerScopedCoroutine::class.java) + .to(CoroutineScope::class.java) + .`in`(Singleton::class.java) + + bindFactory(EventBusFactory::class.java) + .to(EventBus::class.java) + .`in`(Singleton::class.java) + + bindFactory(QuartzSchedulerFactory::class.java) + .to(Scheduler::class.java) + .`in`(Singleton::class.java) + + bindFactory(SchedulingServiceFactory::class.java) + .to(SchedulingService::class.java) + .`in`(Singleton::class.java) + + bindFactory(FirebaseOptionsFactory::class.java) + .to(FirebaseOptions::class.java) + .`in`(Singleton::class.java) + + bindFactory(FcmSenderFactory::class.java) + .to(FcmSender::class.java) + .`in`(Singleton::class.java) + + bind(UnverifiedProjectService::class.java) + .to(org.radarbase.jersey.service.ProjectService::class.java) + .`in`(Singleton::class.java) + } + + override fun ResourceConfig.enhance() { + register(ValidationFeature::class.java) + register(UnhandledExceptionMapper::class.java) + register(EventBusStartupListener::class.java) + } + + /** Project service without validation of the project's existence. */ + class UnverifiedProjectService : org.radarbase.jersey.service.ProjectService { + override suspend fun ensureOrganization(organizationId: String) = Unit + + override suspend fun ensureProject(projectId: String) = Unit + + override suspend fun ensureSubject(projectId: String, userId: String) = Unit + + override suspend fun listProjects(organizationId: String): List = emptyList() + + override suspend fun projectOrganization(projectId: String): String = "main" + } + + companion object { + const val PROJECT_MAPPER = "project_mapper" + const val USER_MAPPER = "user_mapper" + const val DATA_MESSAGE_MAPPER = "data_message_mapper" + const val NOTIFICATION_MAPPER = "notification_mapper" + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/enhancer/factory/AppserverResourceEnhancerFactory.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/enhancer/factory/AppserverResourceEnhancerFactory.kt new file mode 100644 index 000000000..5f941451c --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/enhancer/factory/AppserverResourceEnhancerFactory.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.enhancer.factory + +import org.radarbase.appserver.jersey.config.AppserverConfig +import org.radarbase.appserver.jersey.enhancer.AppserverResourceEnhancer +import org.radarbase.jersey.auth.AuthConfig +import org.radarbase.jersey.auth.MPConfig +import org.radarbase.jersey.enhancer.EnhancerFactory +import org.radarbase.jersey.enhancer.Enhancers +import org.radarbase.jersey.enhancer.JerseyResourceEnhancer +import org.radarbase.jersey.hibernate.config.DatabaseConfig +import org.radarbase.jersey.hibernate.config.HibernateResourceEnhancer + +class AppserverResourceEnhancerFactory(private val config: AppserverConfig) : EnhancerFactory { + override fun createEnhancers(): List { + val authConfig = AuthConfig( + managementPortal = MPConfig( + url = config.auth.managementPortalUrl, + ), + jwtResourceName = config.auth.resourceName, + jwtIssuer = config.auth.issuer, + jwksUrls = config.auth.publicKeyUrls ?: emptyList(), + ) + + val dbConfig = DatabaseConfig( + managedClasses = config.db.classes, + url = config.db.jdbcUrl, + driver = config.db.jdbcDriver, + user = config.db.username, + password = config.db.password, + dialect = config.db.hibernateDialect, + properties = config.db.additionalProperties, + liquibase = org.radarbase.jersey.hibernate.config.LiquibaseConfig( + enable = config.db.liquibase.enabled, + ), + ) + + return listOf( + AppserverResourceEnhancer(config), + Enhancers.radar(authConfig), + Enhancers.managementPortal(authConfig), + HibernateResourceEnhancer(dbConfig), + Enhancers.health, + Enhancers.exception, + ) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/AuditModel.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/AuditModel.kt new file mode 100644 index 000000000..0ba40f917 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/AuditModel.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.entity + +import jakarta.persistence.Column +import jakarta.persistence.MappedSuperclass +import jakarta.persistence.Temporal +import jakarta.persistence.TemporalType +import org.hibernate.annotations.CreationTimestamp +import org.hibernate.annotations.UpdateTimestamp +import java.util.Date + +@MappedSuperclass +abstract class AuditModel { + @CreationTimestamp + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "created_at", nullable = false, updatable = false) + var createdAt: Date? = null + + @UpdateTimestamp + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "updated_at", nullable = false) + var updatedAt: Date? = null +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/DataMessage.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/DataMessage.kt new file mode 100644 index 000000000..c7cc3af6e --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/DataMessage.kt @@ -0,0 +1,187 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.entity + +import jakarta.annotation.Nullable +import jakarta.persistence.CollectionTable +import jakarta.persistence.Column +import jakarta.persistence.ElementCollection +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.Inheritance +import jakarta.persistence.InheritanceType +import jakarta.persistence.MapKeyColumn +import jakarta.persistence.Table +import jakarta.persistence.UniqueConstraint +import java.io.Serial +import java.time.Instant + +/** + * [Entity] for persisting data messages. The corresponding DTO is [FcmDataMessageDto]. + * This also includes information for scheduling the data message through the Firebase Cloud + * Messaging(FCM) system. + * + * @see Scheduled + * @see org.radarbase.appserver.service.scheduler.DataMessageSchedulerService + */ +@Suppress("unused") +@Entity +@Table( + name = "data_messages", + uniqueConstraints = [ + UniqueConstraint( + columnNames = [ + "user_id", "source_id", "scheduled_time", "ttl_seconds", "delivered", "dry_run", + ], + ), + ], +) +@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) +class DataMessage : Message() { + @Nullable + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "data_message_map") + @MapKeyColumn(name = "data_message_key", nullable = true) + @Column(name = "data_message_value") + var dataMap: MutableMap? = null + + class DataMessageBuilder(dataMessage: DataMessage? = null) { + var id: Long? = dataMessage?.id + var user: User? = dataMessage?.user + var sourceId: String? = dataMessage?.sourceId + var scheduledTime: Instant? = dataMessage?.scheduledTime + var ttlSeconds: Int = dataMessage?.ttlSeconds ?: 0 + var fcmMessageId: String? = dataMessage?.fcmMessageId + var fcmTopic: String? = dataMessage?.fcmTopic + var fcmCondition: String? = dataMessage?.fcmCondition + var delivered: Boolean = dataMessage?.delivered == true + var validated: Boolean = dataMessage?.validated == true + var appPackage: String? = dataMessage?.appPackage + var sourceType: String? = dataMessage?.sourceType + var dryRun: Boolean = dataMessage?.dryRun == true + var priority: String? = dataMessage?.priority + var mutableContent: Boolean = dataMessage?.mutableContent == true + var dataMap: MutableMap? = dataMessage?.dataMap + + fun id(id: Long?): DataMessageBuilder = apply { + this.id = id + } + + fun user(user: User?): DataMessageBuilder = apply { + this.user = user + } + + fun sourceId(sourceId: String?): DataMessageBuilder = apply { + this.sourceId = sourceId + } + + fun scheduledTime(scheduledTime: Instant?): DataMessageBuilder = apply { + this.scheduledTime = scheduledTime + } + + fun ttlSeconds(ttlSeconds: Int): DataMessageBuilder = apply { + this.ttlSeconds = ttlSeconds + } + + fun fcmMessageId(fcmMessageId: String?): DataMessageBuilder = apply { + this.fcmMessageId = fcmMessageId + } + + fun fcmTopic(fcmTopic: String?): DataMessageBuilder = apply { + this.fcmTopic = fcmTopic + } + + fun fcmCondition(fcmCondition: String?): DataMessageBuilder = apply { + this.fcmCondition = fcmCondition + } + + fun delivered(delivered: Boolean): DataMessageBuilder = apply { + this.delivered = delivered + } + + fun appPackage(appPackage: String?): DataMessageBuilder = apply { + this.appPackage = appPackage + } + + fun sourceType(sourceType: String?): DataMessageBuilder = apply { + this.sourceType = sourceType + } + + fun dryRun(dryRun: Boolean): DataMessageBuilder = apply { + this.dryRun = dryRun + } + + fun priority(priority: String?): DataMessageBuilder = apply { + this.priority = priority + } + + fun mutableContent(mutableContent: Boolean): DataMessageBuilder = apply { + this.mutableContent = mutableContent + } + + fun dataMap(dataMap: MutableMap?): DataMessageBuilder = apply { + this.dataMap = dataMap + } + + fun build(): DataMessage { + val dataMessage = DataMessage() + dataMessage.id = this.id + dataMessage.user = this.user + dataMessage.sourceId = this.sourceId + dataMessage.scheduledTime = this.scheduledTime + dataMessage.ttlSeconds = this.ttlSeconds + dataMessage.fcmMessageId = this.fcmMessageId + dataMessage.fcmTopic = this.fcmTopic + dataMessage.fcmCondition = this.fcmCondition + dataMessage.delivered = this.delivered + dataMessage.validated = this.validated + dataMessage.appPackage = this.appPackage + dataMessage.sourceType = this.sourceType + dataMessage.dryRun = this.dryRun + dataMessage.priority = this.priority + dataMessage.mutableContent = this.mutableContent + dataMessage.dataMap = this.dataMap + + return dataMessage + } + } + + override fun toString(): String { + return "DataMessage(dataMap=$dataMap)" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + if (!super.equals(other)) return false + + other as DataMessage + + return dataMap == other.dataMap + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + (dataMap?.hashCode() ?: 0) + return result + } + + companion object { + @Serial + private const val serialVersionUID = 4L + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/DataMessageStateEvent.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/DataMessageStateEvent.kt new file mode 100644 index 000000000..96ae03f2e --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/DataMessageStateEvent.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.entity + +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table +import jakarta.validation.constraints.NotNull +import org.hibernate.annotations.OnDelete +import org.hibernate.annotations.OnDeleteAction +import org.radarbase.appserver.jersey.event.state.MessageState +import java.time.Instant + +@Entity +@Table(name = "data_message_state_events") +class DataMessageStateEvent : MessageStateEvent { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + var id: Long? = null + + @field:NotNull + @ManyToOne(fetch = FetchType.EAGER, optional = false) + @JoinColumn(name = "data_message_id", nullable = false) + @field:OnDelete(action = OnDeleteAction.CASCADE) + @field:JsonIgnore + var dataMessage: DataMessage? = null + + constructor( + dataMessage: DataMessage?, + state: MessageState, + time: Instant, + associatedInfo: String?, + ) : super(state, time, associatedInfo) { + this.dataMessage = dataMessage + } + + constructor() +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/Message.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/Message.kt new file mode 100644 index 000000000..a5b45c281 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/Message.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.entity + +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.annotation.Nullable +import jakarta.persistence.CascadeType +import jakarta.persistence.Column +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.MappedSuperclass +import jakarta.validation.constraints.NotNull +import org.hibernate.annotations.OnDelete +import org.hibernate.annotations.OnDeleteAction +import org.radarbase.appserver.jersey.utils.equalTo +import java.io.Serial +import java.io.Serializable +import java.time.Instant +import java.util.Objects + +@MappedSuperclass +class Message( + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + var id: Long? = null, + + @field:NotNull + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + @JsonIgnore + var user: User? = null, + + @ManyToOne(fetch = FetchType.LAZY, optional = true, cascade = [CascadeType.ALL]) + @JoinColumn(name = "task_id", nullable = true) + @OnDelete(action = OnDeleteAction.CASCADE) + @JsonIgnore + var task: Task? = null, + + @Column(name = "source_id") + @field:Nullable + var sourceId: String? = null, + + @field:NotNull + @Column(name = "scheduled_time", nullable = false) + override var scheduledTime: Instant? = null, + + @Column(name = "ttl_seconds") + var ttlSeconds: Int = 0, + + @Column(name = "fcm_message_id", unique = true) + var fcmMessageId: String? = null, + + // for use with the FCM admin SDK + @Column(name = "fcm_topic") + @field:Nullable + var fcmTopic: String? = null, + + // for use with the FCM admin SDK + @Column(name = "fcm_condition") + @field:Nullable + var fcmCondition: String? = null, + + // TODO: REMOVE DELIVERED AND VALIDATED. These can be handled by state lifecycle. + var delivered: Boolean = false, + + var validated: Boolean = false, + + @Column(name = "app_package") + @field:Nullable + var appPackage: String? = null, + + @Column(name = "source_type") + @field:Nullable + var sourceType: String? = null, + + @Column(name = "dry_run") + var dryRun: Boolean = false, + + var priority: String? = null, + + @Column(name = "mutable_content") + var mutableContent: Boolean = false, +) : AuditModel(), Serializable, Scheduled { + + override fun equals(other: Any?): Boolean = equalTo( + other, + Message::ttlSeconds, + Message::delivered, + Message::dryRun, + Message::user, + Message::scheduledTime, + Message::sourceId, + Message::appPackage, + Message::sourceType, + ) + + override fun hashCode(): Int { + return Objects.hash( + user, + sourceId, + scheduledTime, + ttlSeconds, + delivered, + dryRun, + appPackage, + sourceType, + ) + } + + override fun toString(): String { + return "Message(id=$id, user=$user, task=$task, sourceId=$sourceId, scheduledTime=$scheduledTime, ttlSeconds=$ttlSeconds, fcmMessageId=$fcmMessageId, fcmTopic=$fcmTopic, fcmCondition=$fcmCondition, delivered=$delivered, validated=$validated, appPackage=$appPackage, sourceType=$sourceType, dryRun=$dryRun, priority=$priority, mutableContent=$mutableContent)" + } + + companion object { + @Serial + private const val serialVersionUID = -367424816328519L + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/MessageStateEvent.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/MessageStateEvent.kt new file mode 100644 index 000000000..628a229ae --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/MessageStateEvent.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.entity + +import jakarta.persistence.Column +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.MappedSuperclass +import jakarta.validation.constraints.NotNull +import org.radarbase.appserver.jersey.event.state.MessageState +import java.time.Instant + +@MappedSuperclass +class MessageStateEvent( + @field:NotNull + @Column(nullable = false) + @Enumerated(EnumType.STRING) + var state: MessageState? = null, + + @field:NotNull + @Column(nullable = false) + var time: Instant? = null, + + @Column(name = "associated_info", length = 1250) + var associatedInfo: String? = null, +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/Notification.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/Notification.kt new file mode 100644 index 000000000..8f2609f22 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/Notification.kt @@ -0,0 +1,396 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.entity + +import jakarta.annotation.Nullable +import jakarta.persistence.Column +import jakarta.persistence.ElementCollection +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.MapKeyColumn +import jakarta.persistence.Table +import jakarta.persistence.UniqueConstraint +import jakarta.validation.constraints.NotNull +import java.io.Serial +import java.time.Instant +import java.util.Objects + +@Suppress("unused") +@Table( + name = "notifications", + uniqueConstraints = [ + UniqueConstraint( + columnNames = [ + "user_id", + "source_id", + "scheduled_time", + "title", + "body", + "type", + "ttl_seconds", + "delivered", + "dry_run", + ], + ), + ], +) +@Entity +class Notification : Message() { + + @field:NotNull + @Column(nullable = false) + var title: String? = null + + var body: String? = null + + // Type of notification. In terms of aRMT - PHQ8, RSES, ESM, etc. + @field:Nullable + var type: String? = null + + var sound: String? = null + + // For IOS + var badge: String? = null + + // For IOS + var subtitle: String? = null + + // For android + var icon: String? = null + + // For android. Color of the icon + var color: String? = null + + @Column(name = "body_loc_key") + var bodyLocKey: String? = null + + @Column(name = "body_loc_args") + var bodyLocArgs: String? = null + + @Column(name = "title_loc_key") + var titleLocKey: String? = null + + @Column(name = "title_loc_args") + var titleLocArgs: String? = null + + // For android + @Column(name = "android_channel_id") + var androidChannelId: String? = null + + // For android + var tag: String? = null + + @Column(name = "click_action") + var clickAction: String? = null + + @Column(name = "email_enabled") + var emailEnabled: Boolean = false + + @Column(name = "email_title") + var emailTitle: String? = null + + @Column(name = "email_body") + var emailBody: String? = null + + @Nullable + @ElementCollection(fetch = FetchType.EAGER) + @MapKeyColumn(name = "additional_key", nullable = true) + @Column(name = "additional_value") + var additionalData: Map? = null + + class NotificationBuilder(notification: Notification? = null) { + private var id: Long? = notification?.id + private var user: User? = notification?.user + private var sourceId: String? = notification?.sourceId + private var scheduledTime: Instant? = notification?.scheduledTime + private var ttlSeconds: Int = notification?.ttlSeconds ?: 0 + private var fcmMessageId: String? = notification?.fcmMessageId + private var fcmTopic: String? = notification?.fcmTopic + private var fcmCondition: String? = notification?.fcmCondition + private var delivered: Boolean = notification?.delivered == true + private var validated: Boolean = notification?.validated == true + private var appPackage: String? = notification?.appPackage + private var sourceType: String? = notification?.sourceType + private var dryRun: Boolean = notification?.dryRun == true + private var priority: String? = notification?.priority + private var mutableContent: Boolean = notification?.mutableContent == true + private var title: String? = notification?.title + private var body: String? = notification?.body + private var type: String? = notification?.type + private var sound: String? = notification?.sound + private var badge: String? = notification?.badge + private var subtitle: String? = notification?.subtitle + private var icon: String? = notification?.icon + private var color: String? = notification?.color + private var bodyLocKey: String? = notification?.bodyLocKey + private var bodyLocArgs: String? = notification?.bodyLocArgs + private var titleLocKey: String? = notification?.titleLocKey + private var titleLocArgs: String? = notification?.titleLocArgs + private var androidChannelId: String? = notification?.androidChannelId + private var tag: String? = notification?.tag + private var clickAction: String? = notification?.clickAction + private var emailEnabled: Boolean = notification?.emailEnabled == true + private var additionalData: Map? = notification?.additionalData + private var task: Task? = notification?.task + private var emailTitle: String? = notification?.emailTitle + private var emailBody: String? = notification?.emailBody + + fun id(id: Long?): NotificationBuilder = apply { + this.id = id + return this + } + + fun user(user: User?): NotificationBuilder = apply { + this.user = user + return this + } + + fun sourceId(sourceId: String?): NotificationBuilder = apply { + this.sourceId = sourceId + return this + } + + fun scheduledTime(scheduledTime: Instant?): NotificationBuilder = apply { + this.scheduledTime = scheduledTime + return this + } + + fun ttlSeconds(ttlSeconds: Int): NotificationBuilder = apply { + this.ttlSeconds = ttlSeconds + return this + } + + fun fcmMessageId(fcmMessageId: String?): NotificationBuilder = apply { + this.fcmMessageId = fcmMessageId + return this + } + + fun fcmTopic(fcmTopic: String?): NotificationBuilder = apply { + this.fcmTopic = fcmTopic + return this + } + + fun fcmCondition(fcmCondition: String?): NotificationBuilder = apply { + this.fcmCondition = fcmCondition + return this + } + + fun delivered(delivered: Boolean): NotificationBuilder = apply { + this.delivered = delivered + return this + } + + fun appPackage(appPackage: String?): NotificationBuilder = apply { + this.appPackage = appPackage + return this + } + + fun sourceType(sourceType: String?): NotificationBuilder = apply { + this.sourceType = sourceType + return this + } + + fun dryRun(dryRun: Boolean): NotificationBuilder = apply { + this.dryRun = dryRun + return this + } + + fun priority(priority: String?): NotificationBuilder = apply { + this.priority = priority + return this + } + + fun mutableContent(mutableContent: Boolean): NotificationBuilder = apply { + this.mutableContent = mutableContent + return this + } + + fun title(title: String?): NotificationBuilder = apply { + this.title = title + return this + } + + fun body(body: String?): NotificationBuilder = apply { + this.body = body + return this + } + + fun type(type: String?): NotificationBuilder = apply { + this.type = type + return this + } + + fun sound(sound: String?): NotificationBuilder = apply { + this.sound = sound + return this + } + + fun badge(badge: String?): NotificationBuilder = apply { + this.badge = badge + return this + } + + fun subtitle(subtitle: String?): NotificationBuilder = apply { + this.subtitle = subtitle + return this + } + + fun icon(icon: String?): NotificationBuilder = apply { + this.icon = icon + return this + } + + fun color(color: String?): NotificationBuilder = apply { + this.color = color + return this + } + + fun bodyLocKey(bodyLocKey: String?): NotificationBuilder = apply { + this.bodyLocKey = bodyLocKey + return this + } + + fun bodyLocArgs(bodyLocArgs: String?): NotificationBuilder = apply { + this.bodyLocArgs = bodyLocArgs + return this + } + + fun titleLocKey(titleLocKey: String?): NotificationBuilder = apply { + this.titleLocKey = titleLocKey + return this + } + + fun titleLocArgs(titleLocArgs: String?): NotificationBuilder = apply { + this.titleLocArgs = titleLocArgs + return this + } + + fun androidChannelId(androidChannelId: String?): NotificationBuilder = apply { + this.androidChannelId = androidChannelId + return this + } + + fun tag(tag: String?): NotificationBuilder = apply { + this.tag = tag + return this + } + + fun clickAction(clickAction: String?): NotificationBuilder = apply { + this.clickAction = clickAction + return this + } + + fun emailEnabled(emailEnabled: Boolean): NotificationBuilder = apply { + this.emailEnabled = emailEnabled + return this + } + + fun emailTitle(title: String?): NotificationBuilder = apply { + this.emailTitle = title + return this + } + + fun emailBody(body: String?): NotificationBuilder = apply { + this.emailBody = body + return this + } + + fun additionalData(additionalData: Map?): NotificationBuilder = apply { + this.additionalData = additionalData + return this + } + + fun task(task: Task?): NotificationBuilder = apply { + this.task = task + return this + } + + fun build(): Notification { + val notification = Notification() + notification.id = this.id + notification.user = this.user + notification.sourceId = this.sourceId + notification.scheduledTime = this.scheduledTime + notification.ttlSeconds = this.ttlSeconds + notification.fcmMessageId = this.fcmMessageId + notification.fcmTopic = this.fcmTopic + notification.fcmCondition = this.fcmCondition + notification.delivered = this.delivered + notification.validated = this.validated + notification.appPackage = this.appPackage + notification.sourceType = this.sourceType + notification.dryRun = this.dryRun + notification.priority = this.priority + notification.mutableContent = this.mutableContent + notification.additionalData = this.additionalData + notification.title = this.title + notification.body = this.body + notification.type = this.type + notification.sound = this.sound + notification.badge = this.badge + notification.subtitle = this.subtitle + notification.icon = this.icon + notification.color = this.color + notification.bodyLocKey = this.bodyLocKey + notification.bodyLocArgs = this.bodyLocArgs + notification.titleLocKey = this.titleLocKey + notification.titleLocArgs = this.titleLocArgs + notification.androidChannelId = this.androidChannelId + notification.tag = this.tag + notification.clickAction = this.clickAction + notification.emailEnabled = this.emailEnabled + notification.emailTitle = this.emailTitle + notification.emailBody = this.emailBody + notification.additionalData = this.additionalData + notification.task = this.task + + return notification + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + if (other !is Notification) { + return false + } + val that = other + return super.equals(other) && + title == that.title && + body == that.body && + type == that.type + } + + override fun hashCode(): Int { + return Objects.hash( + super.hashCode(), + title, + body, + type, + ) + } + + override fun toString(): String { + return "Notification(title=$title, body=$body, type=$type, sound=$sound, badge=$badge, subtitle=$subtitle, icon=$icon, color=$color, bodyLocKey=$bodyLocKey, bodyLocArgs=$bodyLocArgs, titleLocKey=$titleLocKey, titleLocArgs=$titleLocArgs, androidChannelId=$androidChannelId, tag=$tag, clickAction=$clickAction, emailEnabled=$emailEnabled, emailTitle=$emailTitle, emailBody=$emailBody, additionalData=$additionalData, Message=${super.toString()})" + } + + companion object { + @Serial + private const val serialVersionUID = 6L + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/NotificationStateEvent.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/NotificationStateEvent.kt new file mode 100644 index 000000000..9e3f2077c --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/NotificationStateEvent.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.entity + +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table +import jakarta.validation.constraints.NotNull +import org.hibernate.annotations.OnDelete +import org.hibernate.annotations.OnDeleteAction +import org.radarbase.appserver.jersey.event.state.MessageState +import java.time.Instant + +@Entity +@Table(name = "notification_state_events") +class NotificationStateEvent : MessageStateEvent { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + var id: Long? = null + + @field:NotNull + @field:JsonIgnore + @ManyToOne(fetch = FetchType.EAGER, optional = false) + @JoinColumn(name = "notification_id", nullable = false) + @field:OnDelete(action = OnDeleteAction.CASCADE) + var notification: Notification? = null + + constructor( + notification: Notification, + state: MessageState, + time: Instant, + associatedInfo: String?, + ) : super(state, time, associatedInfo) { + this.notification = notification + } + + constructor() +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/Project.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/Project.kt new file mode 100644 index 000000000..a233a9e19 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/Project.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.entity + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.Table +import jakarta.validation.constraints.NotNull +import org.radarbase.appserver.jersey.utils.stringRepresentation +import java.util.Objects + +/** + * Represents an entity for the "projects" table in the database. + * + * @property id The primary key of the project, which is generated automatically. + * @property projectId A unique identifier for the project. This field is mandatory. + */ +@Entity +@Table(name = "projects") +class Project( + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + var id: Long? = null, + + @field:NotNull + @Column(name = "project_id", unique = true, nullable = false) + var projectId: String? = null, +) : AuditModel() { + constructor(projectId: String) : this(null, projectId) + + override fun hashCode(): Int { + return Objects.hash(projectId) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Project + + return projectId == other.projectId + } + + override fun toString(): String = stringRepresentation( + Project::id, + Project::projectId, + Project::createdAt, + Project::updatedAt, + ) +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/Scheduled.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/Scheduled.kt new file mode 100644 index 000000000..1ce935627 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/Scheduled.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.entity + +import java.time.Instant + +interface Scheduled { + val scheduledTime: Instant? +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/Task.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/Task.kt new file mode 100644 index 000000000..ebcdac31c --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/Task.kt @@ -0,0 +1,235 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.entity + +import com.fasterxml.jackson.annotation.JsonFormat +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table +import jakarta.validation.constraints.NotNull +import org.hibernate.annotations.OnDelete +import org.hibernate.annotations.OnDeleteAction +import org.radarbase.appserver.jersey.dto.protocol.AssessmentType +import org.radarbase.appserver.jersey.event.state.TaskState +import java.sql.Timestamp + +@Entity +@Table(name = "tasks") +class Task : AuditModel() { + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + var id: Long? = null + + @field:NotNull + var completed: Boolean? = null + + @field:NotNull + @JsonFormat(shape = JsonFormat.Shape.NUMBER) + var timestamp: Timestamp? = null + + @field:NotNull + var name: String? = null + + @field:NotNull + @Enumerated(EnumType.STRING) + var type: AssessmentType? = null + + var estimatedCompletionTime = 0 + + @field:NotNull + var completionWindow: Long? = null + + var warning: String? = null + + var isClinical: Boolean? = null + + @JsonFormat(shape = JsonFormat.Shape.NUMBER) + var timeCompleted: Timestamp? = null + + @field:NotNull + var showInCalendar: Boolean? = null + + @field:NotNull + var isDemo: Boolean? = null + + @field:NotNull + var priority: Int = 0 + + @field:NotNull + var nQuestions: Int = 0 + + @field:NotNull + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + @JsonIgnore + var user: User? = null + + @field:NotNull + @Enumerated(EnumType.STRING) + @Column(nullable = false) + var status: TaskState = TaskState.UNKNOWN + + class TaskBuilder(task: Task? = null) { + + var id: Long? = task?.id + + var completed: Boolean? = task?.completed == true + + @field:NotNull + var timestamp: Timestamp? = task?.timestamp + + var name: String? = task?.name + + var type: AssessmentType? = task?.type + + var estimatedCompletionTime: Int = task?.estimatedCompletionTime ?: 0 + + var completionWindow: Long? = task?.completionWindow + + var warning: String? = task?.warning + + var isClinical: Boolean? = task?.isClinical == true + + var timeCompleted: Timestamp? = task?.timeCompleted + + var showInCalendar: Boolean? = task?.showInCalendar != false + + var isDemo: Boolean? = task?.isDemo == true + + var priority: Int = task?.priority ?: 0 + + var nQuestions: Int = task?.nQuestions ?: 0 + + var user: User? = task?.user + + fun id(id: Long?) = apply { + this.id = id + } + + fun completed(completed: Boolean?) = apply { + this.completed = completed + } + + fun timestamp(timestamp: Timestamp?) = apply { + this.timestamp = timestamp + } + + fun timestamp(timestamp: java.time.Instant?) = apply { + this.timestamp = timestamp?.let { + Timestamp.from(it) + } + } + + fun name(name: String?) = apply { + this.name = name + } + + fun type(type: AssessmentType?) = apply { + this.type = type + } + + fun estimatedCompletionTime(estimatedCompletionTime: Int) = apply { + this.estimatedCompletionTime = estimatedCompletionTime + } + + fun completionWindow(completionWindow: Long?) = apply { + this.completionWindow = completionWindow + } + + fun warning(warning: String?) = apply { + this.warning = warning + } + + fun isClinical(isClinical: Boolean?) = apply { + this.isClinical = isClinical + } + + fun timeCompleted(timeCompleted: Timestamp?) = apply { + this.timeCompleted = timeCompleted + } + + fun showInCalendar(showInCalendar: Boolean?) = apply { + this.showInCalendar = showInCalendar + } + + fun isDemo(isDemo: Boolean?) = apply { + this.isDemo = isDemo + } + + fun priority(priority: Int) = apply { + this.priority = priority + } + + fun nQuestions(nQuestions: Int) = apply { + this.nQuestions = nQuestions + } + + fun user(user: User?) = apply { + this.user = user + } + + fun build(): Task { + var task = Task() + task.id = id + task.completed = completed + task.timestamp = timestamp + task.name = name + task.type = type + task.estimatedCompletionTime = estimatedCompletionTime + task.completionWindow = completionWindow + task.warning = warning + task.isClinical = isClinical + task.timeCompleted = timeCompleted + task.showInCalendar = showInCalendar + task.isDemo = isDemo + task.priority = priority + task.nQuestions = nQuestions + task.user = user + return task + } + } + + override fun toString(): String { + return "Task" + + "(id=$id, " + + "completed=$completed, " + + "timestamp=$timestamp, " + + "name=$name, type=$type, " + + "estimatedCompletionTime=$estimatedCompletionTime, " + + "completionWindow=$completionWindow, " + + "warning=$warning, " + + "isClinical=$isClinical, " + + "timeCompleted=$timeCompleted, " + + "showInCalendar=$showInCalendar, " + + "isDemo=$isDemo, " + + "priority=$priority, " + + "nQuestions=$nQuestions, " + + "user=$user, " + + "status=$status" + + ")" + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/TaskStateEvent.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/TaskStateEvent.kt new file mode 100644 index 000000000..30c2aa7d3 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/TaskStateEvent.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.entity + +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table +import jakarta.validation.constraints.NotNull +import org.hibernate.annotations.OnDelete +import org.hibernate.annotations.OnDeleteAction +import org.radarbase.appserver.jersey.event.state.TaskState +import java.time.Instant + +@Entity +@Table(name = "task_state_events") +class TaskStateEvent { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + val id: Long? = null + + @field:NotNull + @Column(nullable = false) + @Enumerated(EnumType.STRING) + var state: TaskState? = null + + @field:NotNull + @Column(nullable = false) + var time: Instant? = null + + @Column(name = "associated_info", length = 1250) + var associatedInfo: String? = null + + @field:NotNull + @field:JsonIgnore + @OnDelete(action = OnDeleteAction.CASCADE) + @JoinColumn(name = "task_id", nullable = false) + @ManyToOne(fetch = FetchType.EAGER, optional = false) + var task: Task? = null + + constructor() + + constructor(task: Task?, state: TaskState?, time: Instant?, associatedInfo: String?) { + this.state = state + this.time = time + this.associatedInfo = associatedInfo + this.task = task + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/User.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/User.kt new file mode 100644 index 000000000..b91a61aa9 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/User.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.entity + +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.persistence.CascadeType +import jakarta.persistence.CollectionTable +import jakarta.persistence.Column +import jakarta.persistence.ElementCollection +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.MapKeyColumn +import jakarta.persistence.OneToOne +import jakarta.persistence.Table +import jakarta.persistence.UniqueConstraint +import jakarta.validation.constraints.NotEmpty +import jakarta.validation.constraints.NotNull +import org.hibernate.annotations.OnDelete +import org.hibernate.annotations.OnDeleteAction +import org.radarbase.appserver.jersey.utils.equalTo +import org.radarbase.appserver.jersey.utils.stringRepresentation +import java.time.Instant +import java.util.Objects + +/** + * [Entity] for persisting users. The corresponding DTO is [org.radarbase.appserver.jersey.dto.fcm.FcmUserDto]. + * [Project] can have multiple [User] (Many-to-One). + */ +@Table( + name = "users", + uniqueConstraints = [ + UniqueConstraint(columnNames = ["subject_id", "project_id"]), + UniqueConstraint(columnNames = ["fcm_token"]), + ], +) +@Entity +class User( + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + var id: Long? = null, + + @field:NotNull + @Column(name = "subject_id", nullable = false) + var subjectId: String? = null, + + @Column(name = "email") + var emailAddress: String? = null, + + @field:NotNull + @Column(name = "fcm_token", nullable = false, unique = true) + var fcmToken: String? = null, + + @field:NotNull + @ManyToOne(fetch = FetchType.EAGER, optional = false) + @JoinColumn(name = "project_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + @JsonIgnore + var project: Project? = null, + + @field:NotNull + @Column(name = "enrolment_date") + var enrolmentDate: Instant? = null, + + @OneToOne(cascade = [CascadeType.ALL]) + var usermetrics: UserMetrics? = null, + + /** + * Timezone of the user based on tz database names + */ + @field:NotNull + @Column(name = "timezone") + var timezone: String? = null, + + @field:NotEmpty + @Column(name = "language") + var language: String? = null, + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "attributes_map") + @MapKeyColumn(name = "attribute_key", nullable = true) + @Column(name = "attribute_value") + var attributes: Map? = HashMap(), +) : AuditModel() { + + override fun toString(): String = stringRepresentation( + User::id, + User::subjectId, + User::emailAddress, + User::fcmToken, + User::project, + User::enrolmentDate, + User::usermetrics, + User::timezone, + User::language, + ) + + override fun equals(other: Any?): Boolean = equalTo( + other, + User::subjectId, + User::fcmToken, + User::project, + User::enrolmentDate, + ) + + override fun hashCode(): Int { + return Objects.hash(subjectId, fcmToken, project, enrolmentDate) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/UserMetrics.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/UserMetrics.kt new file mode 100644 index 000000000..637f6de8d --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/entity/UserMetrics.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.entity + +import jakarta.annotation.Nullable +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.OneToOne +import jakarta.persistence.Table +import jakarta.validation.constraints.NotNull +import org.radarbase.appserver.jersey.utils.stringRepresentation +import java.time.Instant + +@Entity +@Table(name = "user_metrics") +class UserMetrics( + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + var id: Long? = null, + + /** + * The most recent time when the app was opened + */ + @Nullable + @Column(name = "last_opened") + var lastOpened: Instant? = null, + + /** + * The most recent time when a notification for the app was delivered. + */ + @Nullable + @Column(name = "last_delivered") + var lastDelivered: Instant? = null, + + @NotNull + @OneToOne(mappedBy = "usermetrics") + var user: User? = null, +) : AuditModel() { + + constructor(lastOpened: Instant?, lastDelivered: Instant?) : this( + null, + lastOpened = lastOpened, + lastDelivered = lastDelivered, + ) + + override fun toString(): String = stringRepresentation( + UserMetrics::id, + UserMetrics::lastOpened, + UserMetrics::lastDelivered, + ) +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/AppserverEventBus.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/AppserverEventBus.kt new file mode 100644 index 000000000..b37d4974c --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/AppserverEventBus.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.event + +import com.google.common.eventbus.AsyncEventBus +import io.ktor.utils.io.core.Closeable +import jakarta.inject.Inject +import java.util.concurrent.Executors + +class AppserverEventBus @Inject constructor( + numThreads: Int, +) : Closeable { + private val executor = Executors.newFixedThreadPool(numThreads) + + fun getEventBus(): AsyncEventBus = AsyncEventBus( + executor, + ) + + override fun close() { + executor.shutdown() + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/listener/MessageStateEventListener.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/listener/MessageStateEventListener.kt new file mode 100644 index 000000000..c522a07db --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/listener/MessageStateEventListener.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.event.listener + +import com.google.common.eventbus.AllowConcurrentEvents +import com.google.common.eventbus.Subscribe +import jakarta.inject.Inject +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.json.Json +import org.glassfish.hk2.api.ServiceLocator +import org.radarbase.appserver.jersey.entity.DataMessageStateEvent +import org.radarbase.appserver.jersey.entity.NotificationStateEvent +import org.radarbase.appserver.jersey.event.state.dto.DataMessageStateEventDto +import org.radarbase.appserver.jersey.event.state.dto.NotificationStateEventDto +import org.radarbase.appserver.jersey.service.DataMessageStateEventService +import org.radarbase.appserver.jersey.service.NotificationStateEventService +import org.radarbase.jersey.service.AsyncCoroutineService +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +@Suppress("unused") +class MessageStateEventListener @Inject constructor( + private val asyncService: AsyncCoroutineService, + private val serviceLocator: ServiceLocator, +) { + private val json = Json { + encodeDefaults = true + ignoreUnknownKeys = true + } + + private var notificationStateEventService: NotificationStateEventService? = null + get() { + if (field == null) { + return serviceLocator.getService(NotificationStateEventService::class.java) + ?.also { field = it } + } + return field + } + + private var dataMessageStateEventService: DataMessageStateEventService? = null + get() { + if (field == null) { + return serviceLocator.getService(DataMessageStateEventService::class.java) + ?.also { field = it } + } + return field + } + + /** + * Handle an application event. + * + * @param event the event to respond to + */ + @Subscribe + @AllowConcurrentEvents + fun onNotificationStateChange(event: NotificationStateEventDto) { + val info = convertMapToString(event.additionalInfo) + logger.info("Notification state changed. ID: {}, STATE: {}.", event.notification.id, event.state) + val eventEntity = NotificationStateEvent( + event.notification, + event.state, + event.time, + info, + ) + asyncService.runBlocking { + notificationStateEventService?.addNotificationStateEvent(eventEntity) + ?: logger.error("NotificationStateEventService is not initialized.") + } + } + + @Subscribe + @AllowConcurrentEvents + fun onDataMessageStateChange(event: DataMessageStateEventDto) { + val info = convertMapToString(event.additionalInfo) + logger.debug("Data Message state changed. ID: {}, STATE: {}", event.dataMessage.id, event.state) + val eventEntity = DataMessageStateEvent( + event.dataMessage, + event.state, + event.time, + info, + ) + asyncService.runBlocking { + dataMessageStateEventService?.addDataMessageStateEvent(eventEntity) + ?: logger.error("DataMessageStateEventService is not initialized.") + } + } + + fun convertMapToString(additionalInfoMap: Map?): String? { + if (additionalInfoMap == null) return null + return try { + json.encodeToString( + MapSerializer(String.serializer(), String.serializer()), + additionalInfoMap, + ) + } catch (e: Exception) { + logger.warn("error processing event's additional info: {}", additionalInfoMap, e) + null + } + } + + companion object { + private val logger: Logger = LoggerFactory.getLogger(MessageStateEventListener::class.java) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/listener/TaskStateEventListener.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/listener/TaskStateEventListener.kt new file mode 100644 index 000000000..a56dfa3fc --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/listener/TaskStateEventListener.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.event.listener + +import com.google.common.eventbus.AllowConcurrentEvents +import com.google.common.eventbus.Subscribe +import jakarta.inject.Inject +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.json.Json +import org.glassfish.hk2.api.ServiceLocator +import org.radarbase.appserver.jersey.entity.TaskStateEvent +import org.radarbase.appserver.jersey.event.state.dto.TaskStateEventDto +import org.radarbase.appserver.jersey.service.TaskStateEventService +import org.radarbase.jersey.service.AsyncCoroutineService +import org.slf4j.LoggerFactory + +@Suppress("unused") +class TaskStateEventListener @Inject constructor( + private val asyncService: AsyncCoroutineService, + private val serviceLocator: ServiceLocator, +) { + private var taskStateEventService: TaskStateEventService? = null + get() { + if (field == null) { + return serviceLocator.getService(TaskStateEventService::class.java) + ?.also { field = it } + } + return field + } + + private val json = Json { + encodeDefaults = true + ignoreUnknownKeys = true + } + + /** + * Handle an application event. + * // we can add more event listeners by annotating with @EventListener + * + * @param event the event to respond to + */ + @Subscribe + @AllowConcurrentEvents + fun onTaskStateChange(event: TaskStateEventDto) { + val info = convertMapToString(event.additionalInfo) + logger.info("Task state changed. ID: {}, STATE: {}", event.task?.id, event.state) + val eventEntity = TaskStateEvent( + event.task, + event.state, + event.time, + info, + ) + asyncService.runBlocking { + taskStateEventService?.addTaskStateEvent(eventEntity) + ?: logger.error("TaskStateEventService is not initialized.") + } + } + + fun convertMapToString(additionalInfoMap: Map?): String? { + if (additionalInfoMap == null) return null + return try { + json.encodeToString( + MapSerializer(String.serializer(), String.serializer()), + additionalInfoMap, + ) + } catch (e: Exception) { + logger.warn("error processing event's additional info: {}", additionalInfoMap, e) + null + } + } + + companion object { + private val logger = LoggerFactory.getLogger(TaskStateEventListener::class.java) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/listener/quartz/QuartzMessageJobListener.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/listener/quartz/QuartzMessageJobListener.kt new file mode 100644 index 000000000..05e35813b --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/listener/quartz/QuartzMessageJobListener.kt @@ -0,0 +1,158 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.event.listener.quartz + +import com.google.common.eventbus.EventBus +import jakarta.inject.Inject +import org.quartz.JobExecutionContext +import org.quartz.JobExecutionException +import org.quartz.JobListener +import org.radarbase.appserver.jersey.event.state.MessageState +import org.radarbase.appserver.jersey.event.state.dto.DataMessageStateEventDto +import org.radarbase.appserver.jersey.event.state.dto.NotificationStateEventDto +import org.radarbase.appserver.jersey.repository.DataMessageRepository +import org.radarbase.appserver.jersey.repository.NotificationRepository +import org.radarbase.appserver.jersey.service.quartz.MessageType +import org.radarbase.jersey.service.AsyncCoroutineService +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.time.Instant + +class QuartzMessageJobListener @Inject constructor( + private val messageStateEventPublisher: EventBus, + private val notificationRepository: NotificationRepository, + private val dataMessageRepository: DataMessageRepository, + private val asyncService: AsyncCoroutineService, +) : JobListener { + /** + * Get the name of the `JobListener`. + */ + override fun getName(): String { + return javaClass.getName() + } + + /** + * Called by the `[org.quartz.Scheduler]` when a `[org.quartz.JobDetail]` is about to + * be executed (an associated `[org.quartz.Trigger]` has occurred). + * + * + * This method will not be invoked if the execution of the Job was vetoed by a `TriggerListener`. + * + * @see .jobExecutionVetoed + */ + override fun jobToBeExecuted(context: JobExecutionContext?) { + // Not implemented + } + + /** + * Called by the `[org.quartz.Scheduler]` when a `[org.quartz.JobDetail]` was about to + * be executed (an associated `[org.quartz.Trigger]` has occurred), but a `TriggerListener` vetoed its execution. + * + * @see .jobToBeExecuted + */ + override fun jobExecutionVetoed(context: JobExecutionContext?) { + // Not implemented + } + + /** + * Called by the `[org.quartz.Scheduler]` after a `[org.quartz.JobDetail]` has been + * executed, and be for the associated `Trigger`'s `triggered(xx)` method + * has been called. + */ + override fun jobWasExecuted(context: JobExecutionContext, jobException: JobExecutionException?) { + val jobDataMap = context.mergedJobDataMap + val messageId = jobDataMap.getLongValue("messageId") + val messageType = jobDataMap.getString("messageType") ?: run { + logger.warn("Message type does not exist.") + return + } + + val type = MessageType.valueOf(messageType) + when (type) { + MessageType.NOTIFICATION -> { + val notification = asyncService.runBlocking { + notificationRepository.find(messageId) + } ?: run { + logger.warn("The notification does not exist in database and yet was scheduled.") + return + } + if (jobException != null) { + val additionalInfo: MutableMap = hashMapOf() + additionalInfo.put("error", jobException.message!!) + additionalInfo.put("error_description", jobException.toString()) + val notificationStateEventError = NotificationStateEventDto( + notification, + MessageState.ERRORED, + additionalInfo, + Instant.now(), + ) + messageStateEventPublisher.post(notificationStateEventError) + + logger.warn("The job could not be executed.", jobException) + return + } + + val notificationStateEvent = NotificationStateEventDto( + notification, + MessageState.EXECUTED, + null, + Instant.now(), + ) + messageStateEventPublisher.post(notificationStateEvent) + } + + MessageType.DATA -> { + val dataMessage = asyncService.runBlocking { + dataMessageRepository.find(messageId) + } ?: run { + logger.warn("The data message does not exist in database and yet was scheduled.") + return + } + + if (jobException != null) { + val additionalInfo: MutableMap = hashMapOf() + additionalInfo.put("error", jobException.message!!) + additionalInfo.put("error_description", jobException.toString()) + val dataMessageStateEventError = DataMessageStateEventDto( + dataMessage, + MessageState.ERRORED, + additionalInfo, + Instant.now(), + ) + messageStateEventPublisher.post(dataMessageStateEventError) + + logger.warn("The job could not be executed.", jobException) + return + } + + val dataMessageStateEvent = DataMessageStateEventDto( + dataMessage, + MessageState.EXECUTED, + null, + Instant.now(), + ) + messageStateEventPublisher.post(dataMessageStateEvent) + } + + MessageType.UNKNOWN -> logger.warn("The message type does not exist.") + } + } + + companion object { + private val logger: Logger = LoggerFactory.getLogger(QuartzMessageJobListener::class.java) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/listener/quartz/QuartzMessageSchedulerListener.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/listener/quartz/QuartzMessageSchedulerListener.kt new file mode 100644 index 000000000..a8c43e4ad --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/listener/quartz/QuartzMessageSchedulerListener.kt @@ -0,0 +1,295 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.event.listener.quartz + +import com.google.common.eventbus.EventBus +import jakarta.inject.Inject +import kotlinx.coroutines.runBlocking +import org.quartz.JobDetail +import org.quartz.JobKey +import org.quartz.Scheduler +import org.quartz.SchedulerException +import org.quartz.SchedulerListener +import org.quartz.Trigger +import org.quartz.TriggerKey +import org.radarbase.appserver.jersey.event.state.MessageState +import org.radarbase.appserver.jersey.event.state.dto.DataMessageStateEventDto +import org.radarbase.appserver.jersey.event.state.dto.NotificationStateEventDto +import org.radarbase.appserver.jersey.repository.DataMessageRepository +import org.radarbase.appserver.jersey.repository.NotificationRepository +import org.radarbase.appserver.jersey.service.quartz.MessageType +import org.radarbase.appserver.jersey.service.quartz.QuartzNamingStrategy +import org.radarbase.appserver.jersey.service.quartz.SimpleQuartzNamingStrategy +import org.radarbase.jersey.service.AsyncCoroutineService +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.time.Instant + +class QuartzMessageSchedulerListener @Inject constructor( + private val messageStateEventPublisher: EventBus, + private val notificationRepository: NotificationRepository, + private val dataMessageRepository: DataMessageRepository, + private val scheduler: Scheduler, + private val asyncService: AsyncCoroutineService, +) : SchedulerListener { + /** + * Called by the `[Scheduler]` when a `[JobDetail]` is + * scheduled. + */ + override fun jobScheduled(trigger: Trigger) { + val jobDetail: JobDetail + try { + jobDetail = scheduler.getJobDetail(trigger.jobKey) + } catch (exc: SchedulerException) { + logger.warn( + "Encountered error while getting job information from Trigger: ", + exc, + ) + return + } + + val jobDataMap = jobDetail.jobDataMap + val type = MessageType.valueOf(jobDataMap.getString("messageType")) + val messageId = jobDataMap.getLongValue("messageId") + + when (type) { + MessageType.NOTIFICATION -> { + val notification = asyncService.runBlocking { + notificationRepository.find(messageId) + } ?: run { + logger.warn("The notification does not exist in database and yet was scheduled.") + return + } + val notificationStateEvent = + NotificationStateEventDto( + notification, + MessageState.SCHEDULED, + null, + Instant.now(), + ) + messageStateEventPublisher.post(notificationStateEvent) + } + + MessageType.DATA -> { + val dataMessage = asyncService.runBlocking { + dataMessageRepository.find(messageId) + } ?: run { + logger.warn("The data message does not exist in database and yet was scheduled.") + return + } + val dataMessageStateEvent = DataMessageStateEventDto( + dataMessage, + MessageState.SCHEDULED, + null, + Instant.now(), + ) + messageStateEventPublisher.post(dataMessageStateEvent) + } + + MessageType.UNKNOWN -> logger.warn("Job is scheduled for unknown message type") + } + } + + /** + * Called by the `[Scheduler]` when a `[JobDetail]` is + * unscheduled. + * + * @see SchedulerListener.schedulingDataCleared + */ + override fun jobUnscheduled(triggerKey: TriggerKey) { + val notificationId: Long + try { + notificationId = requireNotNull(NAMING_STRATEGY.getMessageId(triggerKey.name)) { + "Message ID shouldn't be null when retrieving from naming strategy" + }.toLong() + } catch (_: NumberFormatException) { + logger.warn("The message id could not be established from unscheduled trigger.") + return + } + val notification = runBlocking { + notificationRepository.find(notificationId) + } ?: run { + logger.warn("The notification does not exist in database and yet was unscheduled.") + return + } + val notificationStateEvent = NotificationStateEventDto( + notification, + MessageState.CANCELLED, + null, + Instant.now(), + ) + messageStateEventPublisher.post(notificationStateEvent) + } + + /** + * Called by the `[Scheduler]` when a `[Trigger]` has reached + * the condition in which it will never fire again. + */ + override fun triggerFinalized(trigger: Trigger?) { + // Not implemented + } + + /** + * Called by the `[Scheduler]` when a `[Trigger]` has been + * paused. + */ + override fun triggerPaused(triggerKey: TriggerKey?) { + // Not implemented + } + + /** + * Called by the `[Scheduler]` when a group of `[Trigger]s` has + * been paused. + * + * + * If all groups were paused then triggerGroup will be null + * + * @param triggerGroup the paused group, or null if all were paused + */ + override fun triggersPaused(triggerGroup: String?) { + // Not implemented + } + + /** + * Called by the `[Scheduler]` when a `[Trigger]` has been + * un-paused. + */ + override fun triggerResumed(triggerKey: TriggerKey?) { + // Not implemented + } + + /** + * Called by the `[Scheduler]` when a group of `[Trigger]s` has + * been un-paused. + */ + override fun triggersResumed(triggerGroup: String?) { + // Not implemented + } + + /** + * Called by the `[Scheduler]` when a `[JobDetail]` has been + * added. + */ + override fun jobAdded(jobDetail: JobDetail?) { + // Not implemented + } + + /** + * Called by the `[Scheduler]` when a `[JobDetail]` has been + * deleted. + */ + override fun jobDeleted(jobKey: JobKey?) { + // Not implemented + } + + /** + * Called by the `[Scheduler]` when a `[JobDetail]` has been + * paused. + */ + override fun jobPaused(jobKey: JobKey?) { + // Not implemented + } + + /** + * Called by the `[Scheduler]` when a group of `[JobDetail]s` + * has been paused. + * + * @param jobGroup the paused group, or null if all were paused + */ + override fun jobsPaused(jobGroup: String?) { + // Not implemented + } + + /** + * Called by the `[Scheduler]` when a `[JobDetail]` has been + * un-paused. + */ + override fun jobResumed(jobKey: JobKey?) { + // Not implemented + } + + /** + * Called by the `[Scheduler]` when a group of `[JobDetail]s` + * has been un-paused. + */ + override fun jobsResumed(jobGroup: String?) { + // Not implemented + } + + /** + * Called by the `[Scheduler]` when a serious error has occurred within the + * scheduler - such as repeated failures in the `JobStore`, or the inability to + * instantiate a `Job` instance when its `Trigger` has fired. + * + * + * The `getErrorCode()` method of the given SchedulerException can be used to + * determine more specific information about the type of error that was encountered. + */ + override fun schedulerError(msg: String?, cause: SchedulerException?) { + // Not implemented + } + + /** + * Called by the `[Scheduler]` to inform the listener that it has move to standby + * mode. + */ + override fun schedulerInStandbyMode() { + // Not implemented + } + + /** + * Called by the `[Scheduler]` to inform the listener that it has started. + */ + override fun schedulerStarted() { + // Not implemented + } + + /** + * Called by the `[Scheduler]` to inform the listener that it is starting. + */ + override fun schedulerStarting() { + // Not implemented + } + + /** + * Called by the `[Scheduler]` to inform the listener that it has shutdown. + */ + override fun schedulerShutdown() { + // Not implemented + } + + /** + * Called by the `[Scheduler]` to inform the listener that it has begun the + * shutdown sequence. + */ + override fun schedulerShuttingdown() { + // Not implemented + } + + /** + * Called by the `[Scheduler]` to inform the listener that all jobs, triggers and + * calendars were deleted. + */ + override fun schedulingDataCleared() { + // Not implemented + } + + companion object { + private val NAMING_STRATEGY: QuartzNamingStrategy = SimpleQuartzNamingStrategy() + private val logger: Logger = LoggerFactory.getLogger(QuartzMessageSchedulerListener::class.java) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/state/MessageState.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/state/MessageState.kt new file mode 100644 index 000000000..49c33cef9 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/state/MessageState.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.event.state + +enum class MessageState { + // Database controlled + ADDED, UPDATED, CANCELLED, + + // Scheduler Controlled + SCHEDULED, + EXECUTED, + + // Controlled by entities outside the appserver. + // These will need to be reported to the appserver. + DELIVERED, OPENED, DISMISSED, + + // Misc + ERRORED, UNKNOWN +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/state/TaskState.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/state/TaskState.kt new file mode 100644 index 000000000..70c8f779d --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/state/TaskState.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.event.state + +enum class TaskState { + ADDED, UPDATED, CANCELLED, SCHEDULED, + COMPLETED, + ERRORED, UNKNOWN +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/state/dto/DataMessageStateEventDto.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/state/dto/DataMessageStateEventDto.kt new file mode 100644 index 000000000..717ce1f37 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/state/dto/DataMessageStateEventDto.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.event.state.dto + +import org.radarbase.appserver.jersey.entity.DataMessage +import org.radarbase.appserver.jersey.event.state.MessageState +import java.time.Instant + +/** + * Create a new ApplicationEvent. + * + * @param dataMessage the data message associated with this state event. + * @param state the current [MessageState].. + * @param additionalInfo any additional info associated with the state change. + */ + +class DataMessageStateEventDto( + val dataMessage: DataMessage, + state: MessageState, + additionalInfo: Map?, + time: Instant, +) : MessageStateEventDto(state, additionalInfo, time) { + + override fun toString(): String { + return "DataMessageStateEventDto(dataMessage=$dataMessage) ${super.toString()}" + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/state/dto/MessageStateEventDto.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/state/dto/MessageStateEventDto.kt new file mode 100644 index 000000000..48b29392b --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/state/dto/MessageStateEventDto.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.event.state.dto + +import org.radarbase.appserver.jersey.event.state.MessageState +import org.radarbase.appserver.jersey.utils.stringRepresentation +import java.time.Instant + +open class MessageStateEventDto( + var state: MessageState, + var additionalInfo: Map?, + var time: Instant, +) { + override fun toString(): String = stringRepresentation( + MessageStateEventDto::state, + MessageStateEventDto::additionalInfo, + MessageStateEventDto::time, + ) +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/state/dto/NotificationStateEventDto.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/state/dto/NotificationStateEventDto.kt new file mode 100644 index 000000000..d5baf126d --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/state/dto/NotificationStateEventDto.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.event.state.dto + +import org.radarbase.appserver.jersey.entity.Notification +import org.radarbase.appserver.jersey.event.state.MessageState +import java.time.Instant + +/** + * Create a new ApplicationEvent. + * + * @param notification the notification associated with this state event. + * @param state the current [MessageState]. + * @param additionalInfo any additional info associated with the state change. + */ +class NotificationStateEventDto( + val notification: Notification, + state: MessageState, + additionalInfo: Map?, + time: Instant, +) : MessageStateEventDto(state, additionalInfo, time) { + + override fun toString(): String { + return "NotificationStateEventDto(notification=$notification) ${super.toString()}" + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/state/dto/TaskStateEventDto.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/state/dto/TaskStateEventDto.kt new file mode 100644 index 000000000..8b52af5ba --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/event/state/dto/TaskStateEventDto.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.event.state.dto + +import org.radarbase.appserver.jersey.entity.Task +import org.radarbase.appserver.jersey.event.state.TaskState +import org.radarbase.appserver.jersey.utils.stringRepresentation +import java.time.Instant + +class TaskStateEventDto( + val task: Task?, + val state: TaskState?, + val additionalInfo: Map?, + val time: Instant?, +) { + + override fun toString(): String = stringRepresentation( + TaskStateEventDto::task, + TaskStateEventDto::state, + TaskStateEventDto::additionalInfo, + TaskStateEventDto::time, + ) +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/AlreadyExistsException.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/AlreadyExistsException.kt new file mode 100644 index 000000000..1c75bd799 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/AlreadyExistsException.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.exception + +import jakarta.ws.rs.core.Response +import org.radarbase.jersey.exception.HttpApplicationException + +class AlreadyExistsException(code: String, message: String) : HttpApplicationException( + Response.Status.EXPECTATION_FAILED, + code, + message, +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/EmailMessageTransmitException.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/EmailMessageTransmitException.kt new file mode 100644 index 000000000..98fe03697 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/EmailMessageTransmitException.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.exception + +class EmailMessageTransmitException : MessageTransmitException { + constructor(message: String) : super( + "fcm_message_transmit_exception", + message, + ) +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/FcmMessageTransmitException.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/FcmMessageTransmitException.kt new file mode 100644 index 000000000..d43207c8f --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/FcmMessageTransmitException.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.exception + +class FcmMessageTransmitException : MessageTransmitException { + constructor(message: String) : super( + "fcm_message_transmit_exception", + message, + ) +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/InvalidNotificationDetailsException.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/InvalidNotificationDetailsException.kt new file mode 100644 index 000000000..e46d1db61 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/InvalidNotificationDetailsException.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.exception + +import jakarta.ws.rs.core.Response +import org.radarbase.jersey.exception.HttpApplicationException + +class InvalidNotificationDetailsException(message: String) : HttpApplicationException( + Response.Status.EXPECTATION_FAILED, + "invalid_notification_details", + message, +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/InvalidProjectDetailsException.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/InvalidProjectDetailsException.kt new file mode 100644 index 000000000..310693ca9 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/InvalidProjectDetailsException.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.exception + +import jakarta.ws.rs.core.Response +import org.radarbase.jersey.exception.HttpApplicationException + +class InvalidProjectDetailsException(message: String) : HttpApplicationException( + Response.Status.EXPECTATION_FAILED, + "invalid_project_details", + message, +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/InvalidUserDetailsException.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/InvalidUserDetailsException.kt new file mode 100644 index 000000000..74045b549 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/InvalidUserDetailsException.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.exception + +import jakarta.ws.rs.core.Response +import org.radarbase.jersey.exception.HttpApplicationException + +class InvalidUserDetailsException(message: String) : HttpApplicationException( + Response.Status.EXPECTATION_FAILED, + "invalid_user_details", + message, +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/MessageTransmitException.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/MessageTransmitException.kt new file mode 100644 index 000000000..9d4de8e17 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/MessageTransmitException.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.exception + +import jakarta.ws.rs.core.Response +import org.radarbase.jersey.exception.HttpApplicationException + +open class MessageTransmitException : HttpApplicationException { + constructor(message: String) : super( + Response.Status.INTERNAL_SERVER_ERROR, + "message_transmit_exception", + message, + ) + + constructor(code: String, message: String) : super( + Response.Status.INTERNAL_SERVER_ERROR, + code, + message, + ) +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/entity/ErrorResponse.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/entity/ErrorResponse.kt new file mode 100644 index 000000000..d960e629f --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/entity/ErrorResponse.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.exception.entity + +data class ErrorResponse( + val error: String, + val description: String, +) diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/handler/UnhandledExceptionMapper.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/handler/UnhandledExceptionMapper.kt new file mode 100644 index 000000000..a08559b65 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/exception/handler/UnhandledExceptionMapper.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.exception.handler + +import jakarta.inject.Singleton +import jakarta.ws.rs.container.ContainerRequestContext +import jakarta.ws.rs.core.Context +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.core.UriInfo +import jakarta.ws.rs.ext.ExceptionMapper +import jakarta.ws.rs.ext.Provider +import org.radarbase.appserver.jersey.exception.entity.ErrorResponse +import org.slf4j.LoggerFactory + +@Provider +@Singleton +class UnhandledExceptionMapper( + @Context private val uriInfo: UriInfo, + @Context private val requestContext: ContainerRequestContext, +) : ExceptionMapper { + + override fun toResponse(exception: Exception): Response { + logger.error("[500] {} {} → {}", requestContext.method, uriInfo.path, exception.message, exception) + + val errorResponse = ErrorResponse( + error = "internal_server_error", + description = exception.message ?: "An unexpected error occurred.", + ) + + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .type("application/json; charset=utf-8") + .entity(errorResponse) + .build() + } + + companion object { + private val logger = LoggerFactory.getLogger(UnhandledExceptionMapper::class.java) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/factory/coroutines/SchedulerScopedCoroutine.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/factory/coroutines/SchedulerScopedCoroutine.kt new file mode 100644 index 000000000..adcd7ad3a --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/factory/coroutines/SchedulerScopedCoroutine.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.factory.coroutines + +import jakarta.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import org.glassfish.jersey.internal.inject.DisposableSupplier +import org.radarbase.appserver.jersey.config.AppserverConfig + +class SchedulerScopedCoroutine @Inject constructor( + appserverConfig: AppserverConfig, +) : DisposableSupplier { + private val schedulerConfig = appserverConfig.quartz + + private val dispatcher: CoroutineDispatcher = when (schedulerConfig.coroutineDispatcher) { + "default" -> Dispatchers.Default + "io" -> Dispatchers.IO + else -> Dispatchers.Unconfined + } + + private val job: Job = when (schedulerConfig.coroutineJob) { + "supervisor-job" -> SupervisorJob() + "coroutine-job" -> Job() + else -> error("Unknown coroutine job for quartz scheduler. Select either 'supervisor-job' or 'coroutine-job'") + } + + private val scope: CoroutineScope = CoroutineScope(dispatcher + job) + + override fun get(): CoroutineScope = scope + + override fun dispose(instance: CoroutineScope) { + scope.cancel() + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/factory/event/EventBusFactory.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/factory/event/EventBusFactory.kt new file mode 100644 index 000000000..ff8dc34cc --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/factory/event/EventBusFactory.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.factory.event + +import com.google.common.eventbus.EventBus +import jakarta.inject.Inject +import org.glassfish.jersey.internal.inject.DisposableSupplier +import org.radarbase.appserver.jersey.config.AppserverConfig +import org.radarbase.appserver.jersey.event.AppserverEventBus + +class EventBusFactory @Inject constructor( + config: AppserverConfig, +) : DisposableSupplier { + private val appserverEventBus = AppserverEventBus( + checkNotNull(config.eventBus.numThreads) { + "eventBus.numThreads must be set in the configuration file." + }, + ) + + override fun get(): EventBus { + return appserverEventBus.getEventBus() + } + + override fun dispose(instance: EventBus?) { + appserverEventBus.close() + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/factory/fcm/FcmSenderFactory.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/factory/fcm/FcmSenderFactory.kt new file mode 100644 index 000000000..45a218748 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/factory/fcm/FcmSenderFactory.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.factory.fcm + +import com.google.common.base.Supplier +import com.google.firebase.FirebaseOptions +import jakarta.inject.Inject +import org.radarbase.appserver.jersey.config.FcmServerConfig +import org.radarbase.appserver.jersey.fcm.downstream.AdminSdkFcmSender +import org.radarbase.appserver.jersey.fcm.downstream.DisabledFcmSender +import org.radarbase.appserver.jersey.fcm.downstream.FcmSender + +class FcmSenderFactory @Inject constructor( + private val firebaseOptions: FirebaseOptions, + private val serverConfig: FcmServerConfig, +) : Supplier { + override fun get(): FcmSender { + var sender = serverConfig.fcmsender + if (sender == null) { + sender = "rest" + } + return when (sender) { + "rest", "org.radarbase.appserver.jersey.fcm.downstream.AdminSdkFcmSender" -> AdminSdkFcmSender( + firebaseOptions, + ) + + "disabled" -> DisabledFcmSender() + else -> throw IllegalStateException("Unknown FCM sender type $sender") + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/factory/fcm/FirebaseOptionsFactory.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/factory/fcm/FirebaseOptionsFactory.kt new file mode 100644 index 000000000..d78428e99 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/factory/fcm/FirebaseOptionsFactory.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.factory.fcm + +import com.google.auth.oauth2.GoogleCredentials +import com.google.firebase.FirebaseOptions +import jakarta.inject.Inject +import org.radarbase.appserver.jersey.config.FcmServerConfig +import org.slf4j.LoggerFactory +import java.io.ByteArrayInputStream +import java.util.Base64 +import java.util.function.Supplier + +class FirebaseOptionsFactory @Inject constructor( + private val serverConfig: FcmServerConfig, +) : Supplier { + override fun get(): FirebaseOptions { + var googleCredentials: GoogleCredentials? = null + + if (serverConfig.credentials != null) { + try { + // read base64 encoded value directly + val decodedCredentials = Base64.getDecoder().decode(serverConfig.credentials) + ByteArrayInputStream(decodedCredentials).use { input -> + googleCredentials = GoogleCredentials.fromStream(input) + } + } catch (ex: IllegalArgumentException) { + logger.error("Cannot load credentials from fcmserver.credentials", ex) + } + } + + if (googleCredentials == null) { + // read from a path specified with environment variable GOOGLE_APPLICATION_CREDENTIALS + googleCredentials = GoogleCredentials.getApplicationDefault() + } + return FirebaseOptions.builder() + .setCredentials(googleCredentials) + .build() + } + + companion object { + private val logger = LoggerFactory.getLogger(FirebaseOptionsFactory::class.java) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/factory/quartz/HK2JobFactory.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/factory/quartz/HK2JobFactory.kt new file mode 100644 index 000000000..68565dc2e --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/factory/quartz/HK2JobFactory.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.factory.quartz + +import org.glassfish.hk2.api.ServiceLocator +import org.quartz.Job +import org.quartz.Scheduler +import org.quartz.SchedulerException +import org.quartz.spi.JobFactory +import org.quartz.spi.TriggerFiredBundle + +class HK2JobFactory( + private val serviceLocator: ServiceLocator, +) : JobFactory { + + @Throws(SchedulerException::class) + override fun newJob(bundle: TriggerFiredBundle, scheduler: Scheduler): Job { + val jobClass = bundle.jobDetail.jobClass + return serviceLocator.create(jobClass) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/factory/quartz/QuartzSchedulerFactory.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/factory/quartz/QuartzSchedulerFactory.kt new file mode 100644 index 000000000..15a6d4757 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/factory/quartz/QuartzSchedulerFactory.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.factory.quartz + +import jakarta.inject.Inject +import org.glassfish.hk2.api.ServiceLocator +import org.glassfish.jersey.internal.inject.DisposableSupplier +import org.quartz.Scheduler +import org.quartz.SchedulerFactory +import org.quartz.impl.StdSchedulerFactory +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +class QuartzSchedulerFactory @Inject constructor( + private val serviceLocator: ServiceLocator, +) : DisposableSupplier { + val schedulerFactory: SchedulerFactory = StdSchedulerFactory() + var scheduler: Scheduler? = null + + override fun get(): Scheduler { + logger.info("Retrieving quartz scheduler instance") + return scheduler.let { sch -> + if (sch == null || sch.isShutdown) { + scheduler = schedulerFactory.scheduler.also { + if (!it.isStarted) { + it.start() + } + it.setJobFactory(HK2JobFactory(serviceLocator)) + } + } + scheduler!! + } + } + + override fun dispose(instance: Scheduler?) { + logger.info("Disposing quartz scheduler") + scheduler?.shutdown() + } + + companion object { + private val logger: Logger = LoggerFactory.getLogger(QuartzSchedulerFactory::class.java) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/factory/scheduling/SchedulingServiceFactory.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/factory/scheduling/SchedulingServiceFactory.kt new file mode 100644 index 000000000..eb50abbbe --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/factory/scheduling/SchedulingServiceFactory.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.factory.scheduling + +import org.glassfish.jersey.internal.inject.DisposableSupplier +import org.radarbase.appserver.jersey.service.scheduling.SchedulingService + +class SchedulingServiceFactory : DisposableSupplier { + override fun get() = SchedulingService() + + override fun dispose(instance: SchedulingService?) { + instance?.close() + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/fcm/downstream/AdminSdkFcmSender.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/fcm/downstream/AdminSdkFcmSender.kt new file mode 100644 index 000000000..a9b4613fd --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/fcm/downstream/AdminSdkFcmSender.kt @@ -0,0 +1,266 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.fcm.downstream + +import com.google.firebase.FirebaseApp +import com.google.firebase.FirebaseOptions +import com.google.firebase.messaging.AndroidConfig +import com.google.firebase.messaging.AndroidNotification +import com.google.firebase.messaging.ApnsConfig +import com.google.firebase.messaging.Aps +import com.google.firebase.messaging.ApsAlert +import com.google.firebase.messaging.FcmOptions +import com.google.firebase.messaging.FirebaseMessaging +import com.google.firebase.messaging.FirebaseMessagingException +import com.google.firebase.messaging.Message +import com.google.firebase.messaging.Notification +import org.radarbase.appserver.jersey.fcm.model.FcmDataMessage +import org.radarbase.appserver.jersey.fcm.model.FcmDownstreamMessage +import org.radarbase.appserver.jersey.fcm.model.FcmNotificationMessage +import org.radarbase.appserver.jersey.utils.requireNotNullField +import org.slf4j.LoggerFactory +import java.time.Duration +import java.time.Instant + +class AdminSdkFcmSender(options: FirebaseOptions) : FcmSender { + init { + try { + FirebaseApp.initializeApp(options) + } catch (exc: IllegalStateException) { + logger.warn("Firebase app was already initialised. {}", exc.message) + } + } + + @Throws(FirebaseMessagingException::class) + override fun send(downstreamMessage: FcmDownstreamMessage) { + val priority = downstreamMessage.priority + + val message = Message.builder() + .setToken(downstreamMessage.to) + .setFcmOptions(FcmOptions.builder().build()) + .setCondition(downstreamMessage.condition) + + val ttl = getValidTtlMillis(requireNotNullField(downstreamMessage.timeToLive, "Downstream message time to live")) + + when (downstreamMessage) { + is FcmNotificationMessage -> { + message.apply { + setAndroidConfig( + AndroidConfig.builder().run { + setCollapseKey(downstreamMessage.collapseKey) + setPriority( + if (priority == null) { + AndroidConfig.Priority.HIGH + } else { + AndroidConfig.Priority.valueOf( + priority, + ) + }, + ) + setTtl(ttl.toMillis()) + setNotification(getAndroidNotification(downstreamMessage)) + putAllData(downstreamMessage.data) + build() + }, + ) + + setApnsConfig( + getApnsConfigBuilder(downstreamMessage, ttl)!! + .putHeader("apns-push-type", "alert") + .build(), + ) + + putAllData(downstreamMessage.data) + setCondition(downstreamMessage.condition) + setNotification( + Notification.builder().run { + setBody( + downstreamMessage.notification!!.getOrDefault("body", "").toString(), + ) + setTitle( + downstreamMessage.notification!!.getOrDefault("title", "").toString(), + ) + setImage( + downstreamMessage.notification!!.getOrDefault("image_url", "").toString(), + ) + build() + }, + ) + } + } + + is FcmDataMessage -> { + message.apply { + setAndroidConfig( + AndroidConfig.builder().run { + setCollapseKey(downstreamMessage.collapseKey) + setPriority( + if (priority == null) { + AndroidConfig.Priority.NORMAL + } else { + AndroidConfig.Priority.valueOf( + priority, + ) + }, + ) + setTtl(ttl.toMillis()) + putAllData(downstreamMessage.data) + build() + }, + ) + + setApnsConfig(getApnsConfigBuilder(downstreamMessage, ttl)!!.build()) + setCondition(downstreamMessage.condition) + putAllData(downstreamMessage.data) + } + } + + else -> throw IllegalArgumentException("Unknown downstream message type: ${downstreamMessage.javaClass.name}") + } + + FirebaseMessaging.getInstance().send(message.build()).also { response -> + logger.info("Message Sent with response : {}", response) + } + } + + private fun getAndroidNotification(notificationMessage: FcmNotificationMessage): AndroidNotification { + val notification = requireNotNullField(notificationMessage.notification, "Fcm downstream message Notification") + + val builder = AndroidNotification.builder() + .setBody(notification.getOrDefault("body", "").toString()) + .setTitle(notification.getOrDefault("title", "").toString()) + .setChannelId(getString(notification["android_channel_id"])) + .setColor(getString(notification["color"])) + .setTag(getString(notification["tag"])) + .setIcon(getString(notification["icon"])) + .setSound(getString(notification["sound"])) + .setClickAction(getString(notification["click_action"])) + + val bodyLocKey = getString(notification["body_loc_key"]) + val titleLocKey = getString(notification["title_loc_key"]) + + if (bodyLocKey != null) { + builder + .setBodyLocalizationKey(getString(notification["body_loc_key"])) + .addBodyLocalizationArg(getString(notification["body_loc_args"])) + } + + if (titleLocKey != null) { + builder + .addTitleLocalizationArg(getString(notification["title_loc_args"])) + .setTitleLocalizationKey(getString(notification["title_loc_key"])) + } + + return builder.build() + } + + private fun getApnsConfigBuilder(message: FcmDownstreamMessage, ttl: Duration): ApnsConfig.Builder? { + val config = ApnsConfig.builder() + if (message.collapseKey != null) config.putHeader("apns-collapse-id", message.collapseKey) + + // The date at which the notification is no longer valid. This value is a UNIX epoch + // expressed in seconds (UTC). + config.putHeader("apns-expiration", Instant.now().plus(ttl).epochSecond.toString()) + + when (message) { + is FcmNotificationMessage -> { + val notificationMessage = message + val notification = requireNotNullField(notificationMessage.notification, "Fcm downstream message Notification") + val apnsData: Map = HashMap(notificationMessage.data ?: emptyMap()) + + val apsAlertBuilder = ApsAlert.builder() + val title = getString(notification["title"]) + if (title != null) apsAlertBuilder.setTitle(title) + val body = getString(notification["body"]) + if (body != null) apsAlertBuilder.setBody(body) + val titleLocKey = getString(notification["title_loc_key"]) + if (titleLocKey != null) apsAlertBuilder.setTitleLocalizationKey(titleLocKey) + val titleLocArgs = getString(notification["title_loc_args"]) + if (titleLocKey != null && titleLocArgs != null) apsAlertBuilder.addTitleLocalizationArg(titleLocArgs) + val bodyLocKey = getString(notification["body_loc_key"]) + if (bodyLocKey != null) apsAlertBuilder.setLocalizationKey(bodyLocKey) + val bodyLocArgs = getString(notification["body_loc_args"]) + if (bodyLocKey != null && bodyLocArgs != null) apsAlertBuilder.addLocalizationArg(bodyLocArgs) + val apsBuilder = Aps.builder() + val sound = getString(notification["sound"]) + if (sound != null) apsBuilder.setSound(sound) + val badge = getString(notification["badge"]) + if (badge != null) apsBuilder.setBadge(badge.toInt()) + val category = getString(notification["category"]) + if (category != null) apsBuilder.setCategory(category) + val threadId = getString(notification["thread_id"]) + if (threadId != null) apsBuilder.setThreadId(threadId) + + if (notificationMessage.contentAvailable != null) { + apsBuilder.setContentAvailable( + requireNotNullField( + notificationMessage.contentAvailable, + "Fcm downstream message contentAvailable", + ), + ) + } + + if (notificationMessage.mutableContent != null) { + apsBuilder.setMutableContent( + requireNotNullField( + notificationMessage.mutableContent, + "Fcm downstream message mutableContent", + ), + ) + } + + return config + .putAllCustomData(apnsData) + .setAps(apsBuilder.setAlert(apsAlertBuilder.build()).build()) + .putHeader("apns-push-type", "alert") + } + + is FcmDataMessage -> { + val dataMessage = message + val apnsData: Map = HashMap(dataMessage.data ?: emptyMap()) + + return config + .putAllCustomData(apnsData) + .setAps(Aps.builder().setContentAvailable(true).setSound("default").build()) + .putHeader("apns-push-type", "background") // No alert is shown + .putHeader("apns-priority", "5") // 5 required in case of a background type + } + + else -> { + throw IllegalArgumentException("Unknown message type -- ${message.javaClass.name}") + } + } + } + + private fun getString(obj: Any?): String? { + return obj?.toString() + } + + fun getValidTtlMillis(ttl: Int): Duration { + val ttlSeconds = if (ttl in 0..DEFAULT_TIME_TO_LIVE) ttl else DEFAULT_TIME_TO_LIVE + return Duration.ofSeconds(ttlSeconds.toLong()) + } + + override fun doesProvideDeliveryReceipt(): Boolean { + return false + } + + companion object { + private val logger = LoggerFactory.getLogger(AdminSdkFcmSender::class.java) + const val DEFAULT_TIME_TO_LIVE: Int = 2419200 // 4 weeks + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/fcm/downstream/DisabledFcmSender.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/fcm/downstream/DisabledFcmSender.kt new file mode 100644 index 000000000..017662f06 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/fcm/downstream/DisabledFcmSender.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.fcm.downstream + +import org.radarbase.appserver.jersey.fcm.model.FcmDownstreamMessage + +class DisabledFcmSender : FcmSender { + @Throws(Exception::class) + override fun send(downstreamMessage: FcmDownstreamMessage) { + // do nothing + } + + override fun doesProvideDeliveryReceipt(): Boolean { + return false + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/fcm/downstream/FcmSender.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/fcm/downstream/FcmSender.kt new file mode 100644 index 000000000..f55ac704d --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/fcm/downstream/FcmSender.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.fcm.downstream + +import org.radarbase.appserver.jersey.fcm.model.FcmDownstreamMessage + +interface FcmSender { + @Throws(Exception::class) + fun send(downstreamMessage: FcmDownstreamMessage) + + fun doesProvideDeliveryReceipt(): Boolean +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/fcm/model/FcmDataMessage.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/fcm/model/FcmDataMessage.kt new file mode 100644 index 000000000..67b6ea867 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/fcm/model/FcmDataMessage.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.fcm.model + +class FcmDataMessage : FcmDownstreamMessage() { + var data: Map? = null +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/fcm/model/FcmDownstreamMessage.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/fcm/model/FcmDownstreamMessage.kt new file mode 100644 index 000000000..8cb6e0ca7 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/fcm/model/FcmDownstreamMessage.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.fcm.model + +abstract class FcmDownstreamMessage : FcmMessage { + var to: String? = null + var condition: String? = null + var messageId: String? = null + var collapseKey: String? = null + var priority: String? = null + var contentAvailable: Boolean? = null + var mutableContent: Boolean? = null + var timeToLive: Int? = null + var deliveryReceiptRequested: Boolean? = null + var dryRun: Boolean? = null +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/fcm/model/FcmMessage.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/fcm/model/FcmMessage.kt new file mode 100644 index 000000000..049f1ee44 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/fcm/model/FcmMessage.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.fcm.model + +interface FcmMessage diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/fcm/model/FcmNotificationMessage.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/fcm/model/FcmNotificationMessage.kt new file mode 100644 index 000000000..3fc0ec595 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/fcm/model/FcmNotificationMessage.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.fcm.model + +class FcmNotificationMessage : FcmDownstreamMessage() { + var notification: Map? = null + var data: Map? = null +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/mapper/DataMessageMapper.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/mapper/DataMessageMapper.kt new file mode 100644 index 000000000..7edb40a36 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/mapper/DataMessageMapper.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.mapper + +import org.radarbase.appserver.jersey.dto.fcm.FcmDataMessageDto +import org.radarbase.appserver.jersey.entity.DataMessage + +class DataMessageMapper : Mapper { + override suspend fun dtoToEntity(dto: FcmDataMessageDto): DataMessage { + return DataMessage.DataMessageBuilder().apply { + mutableContent(dto.mutableContent) + priority(dto.priority) + fcmCondition(dto.fcmCondition) + fcmTopic(dto.fcmTopic) + fcmMessageId(dto.hashCode().toString()) + appPackage(dto.appPackage) + sourceType(dto.sourceType) + sourceId(dto.sourceId) + ttlSeconds(dto.ttlSeconds) + scheduledTime(dto.scheduledTime) + dataMap(dto.dataMap) + }.build() + } + + override suspend fun entityToDto(entity: DataMessage): FcmDataMessageDto { + return FcmDataMessageDto(entity) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/mapper/Mapper.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/mapper/Mapper.kt new file mode 100644 index 000000000..da00d4c3b --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/mapper/Mapper.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.mapper + +import kotlinx.coroutines.Dispatchers +import org.radarbase.appserver.jersey.utils.mapParallel + +interface Mapper { + suspend fun dtoToEntity(dto: D): E + suspend fun entityToDto(entity: E): D + + /** Convert all entities to DTOs in parallel coroutines. */ + suspend fun entitiesToDtos(entities: Iterable): List = + entities.mapParallel(Dispatchers.Default) { entity -> + entityToDto(entity) + } + + /** Convert all DTOs to entities in parallel coroutines. */ + suspend fun dtosToEntities(dtos: Iterable): List = + dtos.mapParallel(Dispatchers.Default) { dto -> + dtoToEntity(dto) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/mapper/NotificationMapper.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/mapper/NotificationMapper.kt new file mode 100644 index 000000000..bbdc54378 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/mapper/NotificationMapper.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.mapper + +import org.radarbase.appserver.jersey.dto.fcm.FcmNotificationDto +import org.radarbase.appserver.jersey.entity.Notification + +class NotificationMapper : Mapper { + + override suspend fun dtoToEntity(dto: FcmNotificationDto): Notification { + return Notification.NotificationBuilder().apply { + body(dto.body) + scheduledTime(dto.scheduledTime) + title(dto.title) + sourceId(dto.sourceId) + type(dto.type) + ttlSeconds(dto.ttlSeconds) + fcmMessageId(dto.hashCode().toString()) + appPackage(dto.appPackage) + sourceType(dto.sourceType) + additionalData(dto.additionalData) + androidChannelId(dto.androidChannelId) + bodyLocArgs(dto.bodyLocArgs) + bodyLocKey(dto.bodyLocKey) + titleLocKey(dto.titleLocKey) + titleLocArgs(dto.titleLocArgs) + badge(dto.badge) + clickAction(dto.clickAction) + color(dto.color) + fcmCondition(dto.fcmCondition) + fcmTopic(dto.fcmTopic) + icon(dto.icon) + mutableContent(dto.mutableContent) + priority(dto.priority) + sound(dto.sound) + subtitle(dto.subtitle) + tag(dto.tag) + emailEnabled(dto.emailEnabled) + emailTitle(dto.emailTitle) + emailBody(dto.emailBody) + }.build() + } + + override suspend fun entityToDto(entity: Notification): FcmNotificationDto { + return FcmNotificationDto(entity) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/mapper/ProjectMapper.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/mapper/ProjectMapper.kt new file mode 100644 index 000000000..ed5320790 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/mapper/ProjectMapper.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.mapper + +import org.radarbase.appserver.jersey.dto.ProjectDto +import org.radarbase.appserver.jersey.entity.Project + +class ProjectMapper : Mapper { + + /** + * Converts a ProjectDTO object to a Project entity. + */ + override suspend fun dtoToEntity(dto: ProjectDto): Project { + return Project(id = dto.id, projectId = dto.projectId) + } + + /** + * Converts a Project entity to a ProjectDTO object. + */ + override suspend fun entityToDto(entity: Project): ProjectDto = ProjectDto( + id = entity.id, + projectId = entity.projectId, + createdAt = entity.createdAt?.toInstant(), + updatedAt = entity.updatedAt?.toInstant(), + ) +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/mapper/UserMapper.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/mapper/UserMapper.kt new file mode 100644 index 000000000..4287700ab --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/mapper/UserMapper.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.mapper + +import org.radarbase.appserver.jersey.dto.fcm.FcmUserDto +import org.radarbase.appserver.jersey.entity.User +import org.radarbase.appserver.jersey.entity.UserMetrics +import java.time.Instant + +class UserMapper : Mapper { + override suspend fun dtoToEntity(dto: FcmUserDto): User = User().apply { + this.fcmToken = dto.fcmToken + this.subjectId = dto.subjectId + this.emailAddress = dto.email + this.usermetrics = getValidUserMetrics(dto) + this.enrolmentDate = dto.enrolmentDate + this.timezone = dto.timezone + this.language = dto.language + this.attributes = dto.attributes + } + + override suspend fun entityToDto(entity: User): FcmUserDto = FcmUserDto(entity) + + companion object { + fun getValidUserMetrics(fcmUserDto: FcmUserDto): UserMetrics { + val lastOpened: Instant = fcmUserDto.lastOpened ?: Instant.now() + return UserMetrics( + lastOpened, + fcmUserDto.lastDelivered, + ) + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/BaseRepository.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/BaseRepository.kt new file mode 100644 index 000000000..d22daa859 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/BaseRepository.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.repository + +interface BaseRepository { + suspend fun find(id: Long): T? + suspend fun exists(id: Long): Boolean + suspend fun add(entity: T): T + suspend fun delete(entity: T) + suspend fun findAll(): List + suspend fun update(entity: T): T? +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/DataMessageRepository.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/DataMessageRepository.kt new file mode 100644 index 000000000..2aa17543e --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/DataMessageRepository.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.repository + +import org.radarbase.appserver.jersey.entity.DataMessage +import java.time.Instant + +interface DataMessageRepository : BaseRepository { + suspend fun findByUserId(userId: Long): List + suspend fun findByIdAndUserId(id: Long, userId: Long): DataMessage? + suspend fun findByFcmMessageId(fcmMessageId: String): DataMessage? + suspend fun existsByUserIdAndSourceIdAndScheduledTimeAndTtlSeconds( + userId: Long, + sourceId: String, + scheduledTime: Instant, + ttlSeconds: Int, + ): Boolean + suspend fun existsByIdAndUserId(id: Long, userId: Long): Boolean + suspend fun deleteByUserId(userId: Long) + suspend fun deleteByIdAndUserId(id: Long, userId: Long) + suspend fun deleteByFcmMessageId(fcmMessageId: String) +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/DataMessageStateEventRepository.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/DataMessageStateEventRepository.kt new file mode 100644 index 000000000..d71dbefbc --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/DataMessageStateEventRepository.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.repository + +import org.radarbase.appserver.jersey.entity.DataMessageStateEvent + +interface DataMessageStateEventRepository : BaseRepository { + suspend fun findByDataMessageId(dataMessageId: Long): List + suspend fun countByDataMessageId(dataMessageId: Long): Long +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/NotificationRepository.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/NotificationRepository.kt new file mode 100644 index 000000000..b88b6d0e7 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/NotificationRepository.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.repository + +import org.radarbase.appserver.jersey.entity.Notification +import java.time.Instant + +interface NotificationRepository : BaseRepository { + suspend fun findByUserId(userId: Long): List + suspend fun findByIdAndUserId(id: Long, userId: Long): Notification? + suspend fun findByUserIdAndTaskId(userId: Long, taskId: Long): List + suspend fun findByTaskId(id: Long): List + suspend fun findByFcmMessageId(fcmMessageId: String): Notification? + suspend fun findByUserIdAndSourceIdAndScheduledTimeAndTitleAndBodyAndTypeAndTtlSeconds( + userId: Long, + sourceId: String, + scheduledTime: Instant, + title: String, + body: String, + type: String, + ttlSeconds: Int, + ): Notification? + + suspend fun existsByUserIdAndSourceIdAndScheduledTimeAndTitleAndBodyAndTypeAndTtlSeconds( + userId: Long, + sourceId: String, + scheduledTime: Instant, + title: String, + body: String, + type: String, + ttlSeconds: Int, + ): Boolean + + suspend fun existsByIdAndUserId(id: Long, userId: Long): Boolean + suspend fun existsByTaskId(taskId: Long): Boolean + suspend fun deleteByUserId(userId: Long) + suspend fun deleteByTaskId(taskId: Long) + suspend fun deleteByUserIdAndTaskId(userId: Long, taskId: Long) + suspend fun deleteByFcmMessageId(fcmMessageId: String) + suspend fun deleteByIdAndUserId(id: Long, userId: Long) +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/NotificationStateEventRepository.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/NotificationStateEventRepository.kt new file mode 100644 index 000000000..99adff080 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/NotificationStateEventRepository.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.repository + +import org.radarbase.appserver.jersey.entity.NotificationStateEvent + +interface NotificationStateEventRepository : BaseRepository { + suspend fun findByNotificationId(notificationId: Long): List + suspend fun countByNotificationId(notificationId: Long): Long +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/ProjectRepository.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/ProjectRepository.kt new file mode 100644 index 000000000..42ccc90b6 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/ProjectRepository.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.repository + +import org.radarbase.appserver.jersey.dto.ProjectDto +import org.radarbase.appserver.jersey.entity.Project + +interface ProjectRepository : BaseRepository { + suspend fun findByProjectId(projectId: String): Project? + suspend fun existsByProjectId(projectId: String): Boolean + suspend fun updateEfficiently(dto: ProjectDto): Project +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/TaskRepository.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/TaskRepository.kt new file mode 100644 index 000000000..fd0b99b05 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/TaskRepository.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.repository + +import org.radarbase.appserver.jersey.dto.protocol.AssessmentType +import org.radarbase.appserver.jersey.entity.Task +import org.radarbase.appserver.jersey.search.QuerySpecification +import java.sql.Timestamp + +interface TaskRepository : BaseRepository { + suspend fun findByUserId(userId: Long): List + + suspend fun findByUserIdAndType(userId: Long, type: AssessmentType): List + + suspend fun deleteByUserId(userId: Long) + + suspend fun deleteByUserIdAndType(userId: Long, type: AssessmentType) + + suspend fun existsByIdAndUserId(id: Long, userId: Long): Boolean + + suspend fun existsByUserIdAndNameAndTimestamp( + userId: Long, + name: String, + timestamp: Timestamp, + ): Boolean + + suspend fun findByIdAndUserId(id: Long, userId: Long): Task? + + suspend fun findAll(specification: QuerySpecification): List + + suspend fun deleteAll(tasks: List) +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/TaskStateEventRepository.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/TaskStateEventRepository.kt new file mode 100644 index 000000000..316494d30 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/TaskStateEventRepository.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.repository + +import org.radarbase.appserver.jersey.entity.TaskStateEvent + +interface TaskStateEventRepository : BaseRepository { + suspend fun findByTaskId(taskId: Long): List + suspend fun countByTaskId(taskId: Long): Long +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/UserRepository.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/UserRepository.kt new file mode 100644 index 000000000..4e4076b2e --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/UserRepository.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.repository + +import org.radarbase.appserver.jersey.entity.User + +interface UserRepository : BaseRepository { + suspend fun findBySubjectId(subjectId: String): User? + suspend fun findByProjectId(projectId: Long): List + suspend fun findBySubjectIdAndProjectId(subjectId: String, projectId: Long): User? + suspend fun findByFcmToken(fcmToken: String): User? + suspend fun existsBySubjectId(subjectId: String): Boolean +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/impl/DataMessageRepositoryImpl.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/impl/DataMessageRepositoryImpl.kt new file mode 100644 index 000000000..c80187de2 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/impl/DataMessageRepositoryImpl.kt @@ -0,0 +1,150 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.repository.impl + +import jakarta.inject.Provider +import jakarta.persistence.EntityManager +import jakarta.ws.rs.core.Context +import org.radarbase.appserver.jersey.entity.DataMessage +import org.radarbase.appserver.jersey.repository.DataMessageRepository +import org.radarbase.jersey.hibernate.HibernateRepository +import org.radarbase.jersey.service.AsyncCoroutineService +import java.time.Instant + +class DataMessageRepositoryImpl( + @Context em: Provider, + @Context asyncCoroutineService: AsyncCoroutineService, +) : HibernateRepository(em, asyncCoroutineService), DataMessageRepository { + override suspend fun find(id: Long): DataMessage? = transact { + find(DataMessage::class.java, id) + } + + override suspend fun exists(id: Long): Boolean = transact { + createQuery( + """SELECT COUNT(d) + FROM DataMessage d + WHERE d.id = :id + """.trimIndent(), + Long::class.java, + ).setParameter("id", id).singleResult > 0 + } + + override suspend fun add(entity: DataMessage): DataMessage = transact { + entity.apply(::persist) + } + + override suspend fun delete(entity: DataMessage) = transact { + remove(merge(entity)) + } + + override suspend fun findAll(): List = transact { + createQuery("SELECT d FROM DataMessage d", DataMessage::class.java).resultList + } + + override suspend fun update(entity: DataMessage): DataMessage? = transact { + merge(entity) + } + + override suspend fun findByUserId(userId: Long): List = transact { + createQuery( + "SELECT d FROM DataMessage d WHERE d.user.id = :userId", + DataMessage::class.java, + ) + .setParameter("userId", userId) + .resultList + } + + override suspend fun findByIdAndUserId( + id: Long, + userId: Long, + ): DataMessage? = transact { + createQuery( + "SELECT d FROM DataMessage d WHERE d.id = :id AND d.user.id = :userId", + DataMessage::class.java, + ) + .setParameter("id", id) + .setParameter("userId", userId) + .resultList + .firstOrNull() + } + + override suspend fun findByFcmMessageId(fcmMessageId: String): DataMessage? = transact { + createQuery("SELECT d FROM DataMessage d WHERE d.fcmMessageId = :fcmMessageId", DataMessage::class.java) + .setParameter("fcmMessageId", fcmMessageId) + .resultList + .firstOrNull() + } + + override suspend fun existsByUserIdAndSourceIdAndScheduledTimeAndTtlSeconds( + userId: Long, + sourceId: String, + scheduledTime: Instant, + ttlSeconds: Int, + ): Boolean = transact { + createQuery( + """SELECT COUNT(d) + FROM DataMessage d + WHERE d.user.id = :userId + AND d.sourceId = :sourceId + AND d.scheduledTime = :scheduledTime + AND d.ttlSeconds = :ttlSeconds""", + Long::class.java, + ) + .setParameter("userId", userId) + .setParameter("sourceId", sourceId) + .setParameter("scheduledTime", scheduledTime) + .setParameter("ttlSeconds", ttlSeconds) + .singleResult > 0 + } + + override suspend fun existsByIdAndUserId(id: Long, userId: Long): Boolean = transact { + createQuery( + """SELECT COUNT(d) + FROM DataMessage d + WHERE d.id = :id + AND d.user.id = :userId + """.trimIndent(), + Long::class.java, + ) + .setParameter("id", id) + .setParameter("userId", userId) + .singleResult > 0 + } + + override suspend fun deleteByUserId(userId: Long): Unit = transact { + createQuery("DELETE FROM DataMessage d WHERE d.user.id = :userId") + .setParameter("userId", userId) + .executeUpdate() + } + + override suspend fun deleteByIdAndUserId(id: Long, userId: Long): Unit = transact { + createQuery( + "DELETE FROM DataMessage d " + + "WHERE d.id = :id " + + "AND d.user.id = :userId", + ) + .setParameter("id", id) + .setParameter("userId", userId) + .executeUpdate() + } + + override suspend fun deleteByFcmMessageId(fcmMessageId: String): Unit = transact { + createQuery("DELETE FROM DataMessage d WHERE d.fcmMessageId = :fcmMessageId") + .setParameter("fcmMessageId", fcmMessageId) + .executeUpdate() + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/impl/DataMessageStateEventRepositoryImpl.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/impl/DataMessageStateEventRepositoryImpl.kt new file mode 100644 index 000000000..9b57d7a90 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/impl/DataMessageStateEventRepositoryImpl.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.repository.impl + +import jakarta.inject.Provider +import jakarta.persistence.EntityManager +import jakarta.ws.rs.core.Context +import org.radarbase.appserver.jersey.entity.DataMessageStateEvent +import org.radarbase.appserver.jersey.repository.DataMessageStateEventRepository +import org.radarbase.jersey.hibernate.HibernateRepository +import org.radarbase.jersey.service.AsyncCoroutineService + +class DataMessageStateEventRepositoryImpl( + @Context em: Provider, + @Context asyncCoroutineService: AsyncCoroutineService, +) : HibernateRepository(em, asyncCoroutineService), DataMessageStateEventRepository { + override suspend fun find(id: Long): DataMessageStateEvent? = transact { + find(DataMessageStateEvent::class.java, id) + } + + override suspend fun exists(id: Long): Boolean = find(id) != null + + override suspend fun add(entity: DataMessageStateEvent): DataMessageStateEvent = transact { + entity.apply(::persist) + } + + override suspend fun delete(entity: DataMessageStateEvent) = transact { + remove(merge(entity)) + } + + override suspend fun findAll(): List = transact { + createQuery( + "SELECT d from DataMessageStateEvent d", + DataMessageStateEvent::class.java, + ).resultList + } + + override suspend fun update(entity: DataMessageStateEvent): DataMessageStateEvent? = transact { + merge(entity) + } + + override suspend fun findByDataMessageId(dataMessageId: Long): List = transact { + createQuery( + """SELECT d + FROM DataMessageStateEvent d + WHERE d.dataMessage.id = :dataMessageId + """.trimIndent(), + DataMessageStateEvent::class.java, + ).setParameter("dataMessageId", dataMessageId) + .resultList + } + + override suspend fun countByDataMessageId(dataMessageId: Long): Long = transact { + createQuery( + """select count(d) + from DataMessageStateEvent d + where d.dataMessage.id = :dataMessageId + """.trimIndent(), + Long::class.java, + ).setParameter("dataMessageId", dataMessageId) + .singleResult + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/impl/NotificationRepositoryImpl.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/impl/NotificationRepositoryImpl.kt new file mode 100644 index 000000000..8f9a00bdd --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/impl/NotificationRepositoryImpl.kt @@ -0,0 +1,227 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.repository.impl + +import jakarta.inject.Provider +import jakarta.persistence.EntityManager +import jakarta.ws.rs.core.Context +import org.radarbase.appserver.jersey.entity.Notification +import org.radarbase.appserver.jersey.repository.NotificationRepository +import org.radarbase.jersey.hibernate.HibernateRepository +import org.radarbase.jersey.service.AsyncCoroutineService +import java.time.Instant + +class NotificationRepositoryImpl( + @Context em: Provider, + @Context private val asyncCoroutineService: AsyncCoroutineService, +) : HibernateRepository(em, asyncCoroutineService), NotificationRepository { + + override suspend fun find(id: Long): Notification? = transact { + find(Notification::class.java, id) + } + + override suspend fun exists(id: Long): Boolean = transact { + val count = createQuery( + "SELECT COUNT(n) FROM Notification n WHERE n.id = :id", + Long::class.java, + ) + .setParameter("id", id) + .singleResult + count > 0 + } + + override suspend fun add(entity: Notification): Notification = transact { + entity.apply(::persist) + } + + override suspend fun delete(entity: Notification): Unit = transact { + remove(merge(entity)) + } + + override suspend fun findAll(): List = transact { + createQuery("SELECT n FROM Notification n", Notification::class.java) + .resultList + } + + override suspend fun update(entity: Notification): Notification = transact { + merge(entity) + } + + override suspend fun findByUserId(userId: Long): List = transact { + createQuery( + "SELECT n FROM Notification n WHERE n.user.id = :userId", + Notification::class.java, + ).setParameter("userId", userId) + .resultList + } + + override suspend fun findByIdAndUserId(id: Long, userId: Long): Notification? = transact { + createQuery( + "SELECT n FROM Notification n WHERE n.id = :id AND n.user.id = :userId", + Notification::class.java, + ).setParameter("id", id) + .setParameter("userId", userId) + .resultList + .firstOrNull() + } + + override suspend fun findByUserIdAndTaskId(userId: Long, taskId: Long): List = transact { + createQuery( + "SELECT n FROM Notification n WHERE n.user.id = :userId AND n.task.id = :taskId", + Notification::class.java, + ).setParameter("userId", userId) + .setParameter("taskId", taskId) + .resultList + } + + override suspend fun findByTaskId(id: Long): List = transact { + createQuery( + "SELECT n FROM Notification n WHERE n.task.id = :taskId", + Notification::class.java, + ).setParameter("taskId", id) + .resultList + } + + override suspend fun findByFcmMessageId(fcmMessageId: String): Notification? = transact { + createQuery( + "SELECT n FROM Notification n WHERE n.fcmMessageId = :fcmMessageId", + Notification::class.java, + ).setParameter("fcmMessageId", fcmMessageId) + .resultList + .firstOrNull() + } + + override suspend fun findByUserIdAndSourceIdAndScheduledTimeAndTitleAndBodyAndTypeAndTtlSeconds( + userId: Long, + sourceId: String, + scheduledTime: Instant, + title: String, + body: String, + type: String, + ttlSeconds: Int, + ): Notification? = transact { + createQuery( + """SELECT n FROM Notification n + WHERE n.user.id = :userId + AND n.sourceId = :sourceId + AND n.scheduledTime = :scheduledTime + AND n.title = :title + AND n.body = :body + AND n.type = :type + AND n.ttlSeconds = :ttlSeconds""", + Notification::class.java, + ).setParameter("userId", userId) + .setParameter("sourceId", sourceId) + .setParameter("scheduledTime", scheduledTime) + .setParameter("title", title) + .setParameter("body", body) + .setParameter("type", type) + .setParameter("ttlSeconds", ttlSeconds) + .resultList + .firstOrNull() + } + + override suspend fun existsByUserIdAndSourceIdAndScheduledTimeAndTitleAndBodyAndTypeAndTtlSeconds( + userId: Long, + sourceId: String, + scheduledTime: Instant, + title: String, + body: String, + type: String, + ttlSeconds: Int, + ): Boolean = transact { + val count = createQuery( + """SELECT COUNT(n) + FROM Notification n + WHERE n.user.id = :userId + AND n.sourceId = :sourceId + AND n.scheduledTime = :scheduledTime + AND n.title = :title + AND n.body = :body + AND n.type = :type + AND n.ttlSeconds = :ttlSeconds""", + Long::class.java, + ).setParameter("userId", userId) + .setParameter("sourceId", sourceId) + .setParameter("scheduledTime", scheduledTime) + .setParameter("title", title) + .setParameter("body", body) + .setParameter("type", type) + .setParameter("ttlSeconds", ttlSeconds) + .singleResult + count > 0 + } + + override suspend fun existsByIdAndUserId(id: Long, userId: Long): Boolean = transact { + createQuery( + """SELECT COUNT(n) + FROM Notification n + WHERE n.id = :id + AND n.user.id = :userId + """.trimIndent(), + Long::class.java, + ).setParameter("id", id) + .setParameter("userId", userId) + .singleResult > 0 + } + + override suspend fun existsByTaskId(taskId: Long): Boolean = transact { + val count = createQuery( + "SELECT COUNT(n) FROM Notification n WHERE n.task.id = :taskId", + Long::class.java, + ).setParameter("taskId", taskId) + .singleResult + count > 0 + } + + override suspend fun deleteByUserId(userId: Long): Unit = transact { + createQuery( + "DELETE FROM Notification n WHERE n.user.id = :userId", + ).setParameter("userId", userId) + .executeUpdate() + } + + override suspend fun deleteByTaskId(taskId: Long): Unit = transact { + createQuery( + "DELETE FROM Notification n WHERE n.task.id = :taskId", + ).setParameter("taskId", taskId) + .executeUpdate() + } + + override suspend fun deleteByUserIdAndTaskId(userId: Long, taskId: Long): Unit = transact { + createQuery( + "DELETE FROM Notification n WHERE n.user.id = :userId AND n.task.id = :taskId", + ).setParameter("userId", userId) + .setParameter("taskId", taskId) + .executeUpdate() + } + + override suspend fun deleteByFcmMessageId(fcmMessageId: String): Unit = transact { + createQuery( + "DELETE FROM Notification n WHERE n.fcmMessageId = :fcmMessageId", + ).setParameter("fcmMessageId", fcmMessageId) + .executeUpdate() + } + + override suspend fun deleteByIdAndUserId(id: Long, userId: Long): Unit = transact { + createQuery( + "DELETE FROM Notification n WHERE n.id = :id AND n.user.id = :userId", + ).setParameter("id", id) + .setParameter("userId", userId) + .executeUpdate() + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/impl/NotificationStateEventRepositoryImpl.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/impl/NotificationStateEventRepositoryImpl.kt new file mode 100644 index 000000000..a8b9fa1ff --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/impl/NotificationStateEventRepositoryImpl.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.repository.impl + +import jakarta.inject.Provider +import jakarta.persistence.EntityManager +import jakarta.ws.rs.core.Context +import org.radarbase.appserver.jersey.entity.NotificationStateEvent +import org.radarbase.appserver.jersey.repository.NotificationStateEventRepository +import org.radarbase.jersey.hibernate.HibernateRepository +import org.radarbase.jersey.service.AsyncCoroutineService + +class NotificationStateEventRepositoryImpl( + @Context em: Provider, + @Context asyncCoroutineService: AsyncCoroutineService, +) : HibernateRepository(em, asyncCoroutineService), NotificationStateEventRepository { + override suspend fun find(id: Long): NotificationStateEvent? = transact { + find(NotificationStateEvent::class.java, id) + } + + override suspend fun exists(id: Long): Boolean = transact { + createQuery( + """SELECT COUNT(n) + FROM NotificationStateEvent n + WHERE n.id = :id + """.trimIndent(), + Long::class.java, + ).setParameter("id", id) + .singleResult > 0 + } + + override suspend fun add(entity: NotificationStateEvent): NotificationStateEvent = transact { + entity.apply(::persist) + } + + override suspend fun delete(entity: NotificationStateEvent) = transact { + remove(merge(entity)) + } + + override suspend fun findAll(): List = transact { + createQuery( + "SELECT n FROM NotificationStateEvent n", + NotificationStateEvent::class.java, + ).resultList + } + + override suspend fun update(entity: NotificationStateEvent): NotificationStateEvent? = transact { + merge(entity) + } + + override suspend fun findByNotificationId(notificationId: Long): List = transact { + createQuery( + """SELECT n FROM NotificationStateEvent n WHERE n.notification.id = :notificationId""", + NotificationStateEvent::class.java, + ).setParameter("notificationId", notificationId) + .resultList + } + + override suspend fun countByNotificationId(notificationId: Long): Long = transact { + createQuery( + """SELECT COUNT(n) FROM NotificationStateEvent n WHERE n.notification.id = :notificationId""", + Long::class.java, + ).setParameter("notificationId", notificationId) + .singleResult + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/impl/ProjectRepositoryImpl.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/impl/ProjectRepositoryImpl.kt new file mode 100644 index 000000000..e29e01c14 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/impl/ProjectRepositoryImpl.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.repository.impl + +import jakarta.inject.Provider +import jakarta.persistence.EntityManager +import jakarta.ws.rs.core.Context +import org.radarbase.appserver.jersey.dto.ProjectDto +import org.radarbase.appserver.jersey.entity.Project +import org.radarbase.appserver.jersey.exception.InvalidProjectDetailsException +import org.radarbase.appserver.jersey.repository.ProjectRepository +import org.radarbase.jersey.exception.HttpNotFoundException +import org.radarbase.jersey.hibernate.HibernateRepository +import org.radarbase.jersey.service.AsyncCoroutineService + +class ProjectRepositoryImpl( + @Context em: Provider, + @Context asyncCoroutineService: AsyncCoroutineService, +) : ProjectRepository, HibernateRepository(em, asyncCoroutineService) { + override suspend fun find(id: Long): Project? = transact { + find(Project::class.java, id) + } + + override suspend fun findByProjectId(projectId: String): Project? = transact { + createQuery( + "SELECT p FROM Project p WHERE p.projectId = :projectId", + Project::class.java, + ).setParameter("projectId", projectId).resultList.firstOrNull() + } + + override suspend fun exists(id: Long): Boolean = transact { + createQuery( + """SELECT COUNT(p) + FROM Project p + WHERE p.id = :id + """.trimIndent(), + Long::class.java, + ).setParameter( + "id", id, + ).singleResult > 0 + } + + override suspend fun existsByProjectId(projectId: String): Boolean = transact { + createQuery( + """SELECT COUNT(p) + FROM Project p + WHERE p.projectId = :projectId + """.trimIndent(), + Long::class.java, + ).setParameter( + "projectId", projectId, + ).singleResult > 0 + } + + override suspend fun add(entity: Project): Project = transact { + entity.apply(::persist) + } + + override suspend fun delete(entity: Project) = Unit // Not needed + + override suspend fun update(entity: Project): Project = transact { + merge(entity) + } + + /** + * Efficient way to update the project by retrieving and updating the retrieved persistent project entity Instead of doing it in two transactions. + */ + override suspend fun updateEfficiently(dto: ProjectDto): Project = transact { + val projectId = try { + requireNotNull(dto.id) + } catch (_: IllegalArgumentException) { + throw InvalidProjectDetailsException("The 'id' of the project must be supplied for updating project") + } + + val project = find(Project::class.java, projectId) ?: throw HttpNotFoundException( + "project_not_found", + "Project with id $projectId does not exists. Please create project first", + ) + + val projectExists = createQuery( + """SELECT COUNT(p) + FROM Project p + WHERE p.projectId = :projectId + """.trimIndent(), + Long::class.java, + ).setParameter( + "projectId", dto.projectId, + ).singleResult > 0 + + if (projectExists) { + throw InvalidProjectDetailsException("Project with id $projectId already exists.") + } + + project.apply { + this.projectId = dto.projectId + } + } + + override suspend fun findAll(): List = transact { + createQuery("SELECT p FROM Project p", Project::class.java).resultList + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/impl/TaskRepositoryImpl.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/impl/TaskRepositoryImpl.kt new file mode 100644 index 000000000..9ec20548c --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/impl/TaskRepositoryImpl.kt @@ -0,0 +1,154 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.repository.impl + +import jakarta.inject.Provider +import jakarta.persistence.EntityManager +import jakarta.persistence.criteria.CriteriaBuilder +import jakarta.persistence.criteria.CriteriaQuery +import jakarta.persistence.criteria.Predicate +import jakarta.persistence.criteria.Root +import jakarta.ws.rs.core.Context +import org.radarbase.appserver.jersey.dto.protocol.AssessmentType +import org.radarbase.appserver.jersey.entity.Task +import org.radarbase.appserver.jersey.repository.TaskRepository +import org.radarbase.appserver.jersey.search.QuerySpecification +import org.radarbase.jersey.hibernate.HibernateRepository +import org.radarbase.jersey.service.AsyncCoroutineService +import java.sql.Timestamp + +class TaskRepositoryImpl( + @Context em: Provider, + @Context asyncCoroutineService: AsyncCoroutineService, +) : HibernateRepository(em, asyncCoroutineService), TaskRepository { + override suspend fun find(id: Long): Task? = transact { + find(Task::class.java, id) + } + + override suspend fun exists(id: Long): Boolean = transact { + createQuery( + "SELECT COUNT(t) FROM Task t WHERE t.id = :id", + Long::class.java, + ).setParameter("id", id) + .singleResult > 0 + } + + override suspend fun add(entity: Task): Task = transact { + entity.apply(::persist) + } + + override suspend fun delete(entity: Task) = transact { + remove(merge(entity)) + } + + override suspend fun findAll(): List = transact { + createQuery("SELECT t FROM Task t", Task::class.java) + .resultList + } + + override suspend fun update(entity: Task): Task? = transact { + merge(entity) + } + + override suspend fun findByUserId(userId: Long): List = transact { + createQuery( + "SELECT t FROM Task t WHERE t.user.id = :userId", + Task::class.java, + ).setParameter("userId", userId) + .resultList + } + + override suspend fun findByUserIdAndType( + userId: Long, + type: AssessmentType, + ): List = transact { + createQuery( + "SELECT t FROM Task t WHERE t.user.id = :userId AND t.type = :type", + Task::class.java, + ) + .setParameter("userId", userId) + .setParameter("type", type) + .resultList + } + + override suspend fun deleteByUserId(userId: Long): Unit = transact { + createQuery("DELETE FROM Task t WHERE t.user.id = :userId") + .setParameter("userId", userId) + .executeUpdate() + } + + override suspend fun deleteByUserIdAndType( + userId: Long, + type: AssessmentType, + ): Unit = transact { + createQuery( + "DELETE FROM Task t WHERE t.user.id = :userId AND t.type = :type", + ) + .setParameter("userId", userId) + .setParameter("type", type) + .executeUpdate() + } + + override suspend fun existsByIdAndUserId(id: Long, userId: Long): Boolean = transact { + createQuery( + "SELECT COUNT(t) FROM Task t WHERE t.id = :id AND t.user.id = :userId", + Long::class.java, + ) + .setParameter("id", id) + .setParameter("userId", userId) + .singleResult > 0 + } + + override suspend fun existsByUserIdAndNameAndTimestamp( + userId: Long, + name: String, + timestamp: Timestamp, + ): Boolean = transact { + createQuery( + "SELECT COUNT(t) FROM Task t WHERE t.user.id = :userId AND t.name = :name AND t.timestamp = :timestamp", + Long::class.java, + ) + .setParameter("userId", userId) + .setParameter("name", name) + .setParameter("timestamp", timestamp) + .singleResult > 0 + } + + override suspend fun findByIdAndUserId(id: Long, userId: Long): Task? = transact { + createQuery( + "SELECT t FROM Task t WHERE t.id = :id AND t.user.id = :userId", + Task::class.java, + ) + .setParameter("id", id) + .setParameter("userId", userId) + .resultList + .firstOrNull() + } + + override suspend fun findAll(specification: QuerySpecification): List = transact { + val cb: CriteriaBuilder = criteriaBuilder + val cq: CriteriaQuery = cb.createQuery(Task::class.java) + val root: Root = cq.from(Task::class.java) + val predicate: Predicate = specification.toPredicate(root, cq, cb) + cq.select(root).where(predicate) + createQuery(cq).resultList + } + + override suspend fun deleteAll(tasks: List) = transact { + tasks.forEach { remove(merge(it)) } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/impl/TaskStateEventRepositoryImpl.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/impl/TaskStateEventRepositoryImpl.kt new file mode 100644 index 000000000..f84205086 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/impl/TaskStateEventRepositoryImpl.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.repository.impl + +import jakarta.inject.Provider +import jakarta.persistence.EntityManager +import jakarta.ws.rs.core.Context +import org.radarbase.appserver.jersey.entity.TaskStateEvent +import org.radarbase.appserver.jersey.repository.TaskStateEventRepository +import org.radarbase.jersey.hibernate.HibernateRepository +import org.radarbase.jersey.service.AsyncCoroutineService + +class TaskStateEventRepositoryImpl( + @Context em: Provider, + @Context asyncCoroutineService: AsyncCoroutineService, +) : TaskStateEventRepository, HibernateRepository(em, asyncCoroutineService) { + override suspend fun find(id: Long): TaskStateEvent? = transact { + find(TaskStateEvent::class.java, id) + } + + override suspend fun exists(id: Long): Boolean = transact { + createQuery( + """SELECT COUNT(e) + FROM TaskStateEvent e + WHERE e.id = :id + """.trimIndent(), + Long::class.java, + ).setParameter("id", id) + .singleResult > 0 + } + + override suspend fun add(entity: TaskStateEvent): TaskStateEvent = transact { + entity.apply(::persist) + } + + override suspend fun delete(entity: TaskStateEvent) = transact { + remove(merge(entity)) + } + + override suspend fun findAll(): List = transact { + createQuery("SELECT e FROM TaskStateEvent e", TaskStateEvent::class.java) + .resultList + } + + override suspend fun update(entity: TaskStateEvent): TaskStateEvent? = transact { + merge(entity) + } + + override suspend fun findByTaskId(taskId: Long): List = transact { + createQuery( + "SELECT e FROM TaskStateEvent e WHERE e.task.id = :taskId", + TaskStateEvent::class.java, + ).setParameter("taskId", taskId) + .resultList + } + + override suspend fun countByTaskId(taskId: Long): Long = transact { + createQuery("SELECT COUNT(e) FROM TaskStateEvent e WHERE e.task.id = :taskId", Long::class.java) + .setParameter("taskId", taskId) + .singleResult + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/impl/UserRepositoryImpl.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/impl/UserRepositoryImpl.kt new file mode 100644 index 000000000..bde5ac624 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/repository/impl/UserRepositoryImpl.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.repository.impl + +import jakarta.inject.Provider +import jakarta.persistence.EntityManager +import jakarta.ws.rs.core.Context +import org.radarbase.appserver.jersey.entity.User +import org.radarbase.appserver.jersey.repository.UserRepository +import org.radarbase.jersey.hibernate.HibernateRepository +import org.radarbase.jersey.service.AsyncCoroutineService + +class UserRepositoryImpl( + @Context em: Provider, + @Context asyncCoroutineService: AsyncCoroutineService, +) : HibernateRepository(em, asyncCoroutineService), UserRepository { + override suspend fun find(id: Long): User? = transact { + find(User::class.java, id) + } + + override suspend fun findBySubjectId(subjectId: String): User? = transact { + createQuery( + "SELECT u FROM User u WHERE u.subjectId = :subjectId", + User::class.java, + ).setParameter("subjectId", subjectId).resultList.firstOrNull() + } + + override suspend fun exists(id: Long): Boolean = transact { + createQuery( + """SELECT COUNT(u) + FROM User u + WHERE u.id = :id + """.trimIndent(), + Long::class.java, + ).setParameter("id", id).singleResult > 0 + } + + override suspend fun existsBySubjectId(subjectId: String): Boolean = findBySubjectId(subjectId) != null + + override suspend fun findByProjectId(projectId: Long): List = transact { + createQuery( + "SELECT u FROM User u WHERE u.project.id = :projectId", + User::class.java, + ) + .setParameter("projectId", projectId) + .resultList + } + + override suspend fun findBySubjectIdAndProjectId( + subjectId: String, + projectId: Long, + ): User? = transact { + createQuery( + """SELECT u + FROM User u + WHERE u.subjectId = :subjectId + AND u.project.id = :projectId + """.trimIndent(), + User::class.java, + ) + .setParameter("subjectId", subjectId) + .setParameter("projectId", projectId) + .resultList + .firstOrNull() + } + + override suspend fun findByFcmToken(fcmToken: String): User? = transact { + createQuery( + "SELECT u FROM User u WHERE u.fcmToken = :fcmToken", + User::class.java, + ) + .setParameter("fcmToken", fcmToken) + .resultList + .firstOrNull() + } + + override suspend fun add(entity: User): User = transact { + entity.apply(::persist) + } + + override suspend fun update(entity: User): User? = transact { + merge(entity) + } + + override suspend fun delete(entity: User): Unit = transact { + remove(merge(entity)) + } + + override suspend fun findAll(): List = transact { + createQuery("SELECT u FROM User u", User::class.java).resultList + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/FcmDataMessageResource.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/FcmDataMessageResource.kt new file mode 100644 index 000000000..e412f85de --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/FcmDataMessageResource.kt @@ -0,0 +1,269 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.resource + +import jakarta.inject.Inject +import jakarta.inject.Provider +import jakarta.validation.Valid +import jakarta.ws.rs.DELETE +import jakarta.ws.rs.GET +import jakarta.ws.rs.POST +import jakarta.ws.rs.PUT +import jakarta.ws.rs.Path +import jakarta.ws.rs.PathParam +import jakarta.ws.rs.Produces +import jakarta.ws.rs.QueryParam +import jakarta.ws.rs.container.AsyncResponse +import jakarta.ws.rs.container.Suspended +import jakarta.ws.rs.core.MediaType.APPLICATION_JSON +import jakarta.ws.rs.core.Response +import org.radarbase.appserver.jersey.config.AppserverConfig +import org.radarbase.appserver.jersey.dto.fcm.FcmDataMessageDto +import org.radarbase.appserver.jersey.dto.fcm.FcmDataMessages +import org.radarbase.appserver.jersey.service.FcmDataMessageService +import org.radarbase.appserver.jersey.utils.Paths.ALL_KEYWORD +import org.radarbase.appserver.jersey.utils.Paths.MESSAGING_DATA_PATH +import org.radarbase.appserver.jersey.utils.Paths.PROJECTS_PATH +import org.radarbase.appserver.jersey.utils.Paths.PROJECT_ID +import org.radarbase.appserver.jersey.utils.Paths.SUBJECT_ID +import org.radarbase.appserver.jersey.utils.Paths.USERS_PATH +import org.radarbase.appserver.jersey.utils.tokenForCurrentRequest +import org.radarbase.auth.authorization.EntityDetails +import org.radarbase.auth.authorization.Permission +import org.radarbase.auth.token.RadarToken +import org.radarbase.jersey.auth.AuthService +import org.radarbase.jersey.auth.Authenticated +import org.radarbase.jersey.auth.NeedsPermission +import org.radarbase.jersey.service.AsyncCoroutineService +import java.net.URI +import java.time.LocalDateTime +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +@Suppress("UnresolvedRestParam") +@Path("/") +class FcmDataMessageResource @Inject constructor( + private val fcmDataMessageService: FcmDataMessageService, + private val asyncService: AsyncCoroutineService, + private val authService: AuthService, + private val tokenProvider: Provider, + config: AppserverConfig, +) { + private val requestTimeout: Duration = config.server.requestTimeout.seconds + + @GET + @Path(MESSAGING_DATA_PATH) + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.PROJECT_READ) + fun getAllDataMessages( + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + Response.ok(fcmDataMessageService.getAllDataMessages()).build() + } + } + + @GET + @Path("$MESSAGING_DATA_PATH/{id}") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_READ) + fun getDataMessageUsingId( + @PathParam("id") id: Long, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + Response.ok(fcmDataMessageService.getDataMessageById(id)).build() + } + } + + @GET + @Path("$MESSAGING_DATA_PATH/filtered") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.PROJECT_READ) + fun getFilteredDataMessages( + @Valid @QueryParam("type") type: String?, + @Valid @QueryParam("delivered") delivered: Boolean?, + @Valid @QueryParam("ttlSeconds") ttlSeconds: Int?, + @Valid @QueryParam("startTime") startTimeStr: String?, + @Valid @QueryParam("endTime") endTimeStr: String?, + @Valid @QueryParam("limit") limit: Int?, + @Suspended asyncResponse: AsyncResponse, + ) { + val startTime = startTimeStr?.let { LocalDateTime.parse(it) } + val endTime = endTimeStr?.let { LocalDateTime.parse(it) } + + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + Response.ok( + fcmDataMessageService.getFilteredDataMessages( + type, + delivered, + ttlSeconds, + startTime, + endTime, + limit, + ), + ).build() + } + } + + @GET + @Path("$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID/$MESSAGING_DATA_PATH") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_READ, projectPathParam = "projectId", userPathParam = "subjectId") + fun getDataMessageUsingProjectIdAndSubjectId( + @Valid @PathParam("projectId") projectId: String, + @Valid @PathParam("subjectId") subjectId: String, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + fcmDataMessageService.getDataMessagesByProjectIdAndSubjectId(projectId, subjectId).let { + Response.ok(it).build() + } + } + } + + @GET + @Path("$PROJECTS_PATH/$PROJECT_ID/$MESSAGING_DATA_PATH") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_READ) + fun getDataMessageUsingProjectId( + @Valid @PathParam("projectId") projectId: String, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + val token = tokenForCurrentRequest(asyncService, tokenProvider) + authService.checkPermission( + Permission.SUBJECT_READ, + EntityDetails(project = projectId, subject = token.subject), + token, + ) + fcmDataMessageService.getDataMessagesByProjectId(projectId).let { + Response.ok(it).build() + } + } + } + + @POST + @Path("$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID/$MESSAGING_DATA_PATH") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_UPDATE, projectPathParam = "projectId", userPathParam = "subjectId") + fun addSingleDataMessage( + @Valid @PathParam("projectId") projectId: String, + @Valid @PathParam("subjectId") subjectId: String, + @Valid fcmDataMessage: FcmDataMessageDto, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + fcmDataMessageService.addDataMessage( + fcmDataMessage, + subjectId, + projectId, + ).let { + Response.created(URI("$MESSAGING_DATA_PATH/${it.id}")).entity(it).build() + } + } + } + + @POST + @Path("$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID/$MESSAGING_DATA_PATH/batch") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_UPDATE, projectPathParam = "projectId", userPathParam = "subjectId") + fun addBatchDataMessages( + @Valid @PathParam("projectId") projectId: String, + @Valid @PathParam("subjectId") subjectId: String, + @Valid fcmDataMessages: FcmDataMessages, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + fcmDataMessageService.addDataMessages( + fcmDataMessages, + subjectId, + projectId, + ).let { + Response.ok(it).build() + } + } + } + + @PUT + @Path("$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID/$MESSAGING_DATA_PATH") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_UPDATE, projectPathParam = "projectId", userPathParam = "subjectId") + fun updateDataMessage( + @Valid @PathParam("projectId") projectId: String, + @Valid @PathParam("subjectId") subjectId: String, + @Valid fcmDataMessage: FcmDataMessageDto, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + fcmDataMessageService.updateDataMessage( + fcmDataMessage, + subjectId, + projectId, + ).let { + Response.ok(it).build() + } + } + } + + @DELETE + @Path("$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID/$MESSAGING_DATA_PATH/$ALL_KEYWORD") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_UPDATE, projectPathParam = "projectId", userPathParam = "subjectId") + fun deleteDataMessageForUser( + @Valid @PathParam("projectId") projectId: String, + @Valid @PathParam("subjectId") subjectId: String, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + fcmDataMessageService.removeDataMessagesForUser(projectId, subjectId).let { + Response.ok().build() + } + } + } + + @DELETE + @Path("$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID/$MESSAGING_DATA_PATH/{id}") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_UPDATE, projectPathParam = "projectId", userPathParam = "subjectId") + fun deleteDataMessageUsingProjectIdAndSubjectIdAndDataMessageId( + @Valid @PathParam("projectId") projectId: String, + @Valid @PathParam("subjectId") subjectId: String, + @PathParam("id") id: Long, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + fcmDataMessageService.deleteDataMessageByProjectIdAndSubjectIdAndDataMessageId( + projectId, + subjectId, + id, + ) + + Response.ok().build() + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/FcmNotificationResource.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/FcmNotificationResource.kt new file mode 100644 index 000000000..3ebb47b48 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/FcmNotificationResource.kt @@ -0,0 +1,332 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.resource + +import jakarta.inject.Provider +import jakarta.validation.Valid +import jakarta.ws.rs.DELETE +import jakarta.ws.rs.DefaultValue +import jakarta.ws.rs.GET +import jakarta.ws.rs.POST +import jakarta.ws.rs.PUT +import jakarta.ws.rs.Path +import jakarta.ws.rs.PathParam +import jakarta.ws.rs.Produces +import jakarta.ws.rs.QueryParam +import jakarta.ws.rs.container.AsyncResponse +import jakarta.ws.rs.container.Suspended +import jakarta.ws.rs.core.MediaType.APPLICATION_JSON +import jakarta.ws.rs.core.Response +import org.radarbase.appserver.jersey.config.AppserverConfig +import org.radarbase.appserver.jersey.dto.fcm.FcmNotificationDto +import org.radarbase.appserver.jersey.dto.fcm.FcmNotifications +import org.radarbase.appserver.jersey.service.FcmNotificationService +import org.radarbase.appserver.jersey.utils.Paths.ALL_KEYWORD +import org.radarbase.appserver.jersey.utils.Paths.MESSAGING_NOTIFICATION_PATH +import org.radarbase.appserver.jersey.utils.Paths.NOTIFICATION_ID +import org.radarbase.appserver.jersey.utils.Paths.PROJECTS_PATH +import org.radarbase.appserver.jersey.utils.Paths.PROJECT_ID +import org.radarbase.appserver.jersey.utils.Paths.SUBJECT_ID +import org.radarbase.appserver.jersey.utils.Paths.TASKS_PATH +import org.radarbase.appserver.jersey.utils.Paths.USERS_PATH +import org.radarbase.appserver.jersey.utils.tokenForCurrentRequest +import org.radarbase.auth.authorization.EntityDetails +import org.radarbase.auth.authorization.Permission +import org.radarbase.auth.token.RadarToken +import org.radarbase.jersey.auth.AuthService +import org.radarbase.jersey.auth.Authenticated +import org.radarbase.jersey.auth.NeedsPermission +import org.radarbase.jersey.service.AsyncCoroutineService +import java.net.URI +import java.time.LocalDateTime +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +@Suppress("UnresolvedRestParam") +@Path("/") +class FcmNotificationResource( + private val asyncService: AsyncCoroutineService, + private val authService: AuthService, + private val tokenProvider: Provider, + private val fcmNotificationService: FcmNotificationService, + config: AppserverConfig, +) { + private val requestTimeout: Duration = config.server.requestTimeout.seconds + + @GET + @Path(MESSAGING_NOTIFICATION_PATH) + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.PROJECT_READ) + fun getAllNotifications( + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + fcmNotificationService.getAllNotifications().let { + Response.ok(it).build() + } + } + } + + @GET + @Path("$MESSAGING_NOTIFICATION_PATH/{id}") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_UPDATE) + fun getNotificationUsingId( + @Valid @PathParam("id") id: Long, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + fcmNotificationService.getNotificationById(id).let { + Response.ok(it).build() + } + } + } + + @GET + @Path("$MESSAGING_NOTIFICATION_PATH/filter") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.PROJECT_READ) + fun getFilteredNotifications( + @Valid @QueryParam("type") type: String?, + @Valid @QueryParam("delivered") delivered: Boolean?, + @Valid @QueryParam("ttlSeconds") ttlSeconds: Int?, + @Valid @QueryParam("startTime") startTimeStr: String?, + @Valid @QueryParam("endTime") endTimeStr: String?, + @Valid @QueryParam("limit") limit: Int?, + @Suspended asyncResponse: AsyncResponse, + ) { + val startTime = startTimeStr?.let { LocalDateTime.parse(it) } + val endTime = endTimeStr?.let { LocalDateTime.parse(it) } + + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + Response.ok( + fcmNotificationService.getFilteredNotifications( + type, + delivered, + ttlSeconds, + startTime, + endTime, + limit, + ), + ).build() + } + } + + @GET + @Path("$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID/$MESSAGING_NOTIFICATION_PATH") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_READ, projectPathParam = "projectId", userPathParam = "subjectId") + fun getNotificationsUsingProjectIdAndSubjectId( + @Valid @PathParam("projectId") projectId: String, + @Valid @PathParam("subjectId") subjectId: String, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + fcmNotificationService.getNotificationsByProjectIdAndSubjectId(projectId, subjectId).let { + Response.ok(it).build() + } + } + } + + @GET + @Path("$PROJECTS_PATH/$PROJECT_ID/$MESSAGING_NOTIFICATION_PATH") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_READ, projectPathParam = "projectId") + fun getNotificationsUsingProjectId( + @Valid @PathParam("projectId") projectId: String, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + val token = tokenForCurrentRequest(asyncService, tokenProvider) + authService.checkPermission( + Permission.SUBJECT_READ, + EntityDetails(project = projectId, subject = token.subject), + token, + ) + fcmNotificationService.getNotificationsByProjectId(projectId).let { + Response.ok(it).build() + } + } + } + + @POST + @Path("$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID/$MESSAGING_NOTIFICATION_PATH") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_UPDATE, projectPathParam = "projectId", userPathParam = "subjectId") + fun addSingleNotification( + @Valid @PathParam("projectId") projectId: String, + @Valid @PathParam("subjectId") subjectId: String, + @Valid fcmNotification: FcmNotificationDto, + @QueryParam("schedule") @DefaultValue("true") schedule: Boolean, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + fcmNotificationService.addNotification( + fcmNotification, + subjectId, + projectId, + schedule, + ).let { + Response.created(URI("$MESSAGING_NOTIFICATION_PATH/${it.id}")).entity(it).build() + } + } + } + + @POST + @Path("$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID/$MESSAGING_NOTIFICATION_PATH/schedule") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_UPDATE, projectPathParam = "projectId", userPathParam = "subjectId") + fun scheduleUserNotifications( + @Valid @PathParam("projectId") projectId: String, + @Valid @PathParam("subjectId") subjectId: String, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + fcmNotificationService.scheduleAllUserNotifications(subjectId, projectId).let { + Response.ok(it).build() + } + } + } + + @POST + @Path("$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID/$MESSAGING_NOTIFICATION_PATH/$NOTIFICATION_ID/schedule") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_UPDATE, projectPathParam = "projectId", userPathParam = "subjectId") + fun scheduleUserNotification( + @Valid @PathParam("projectId") projectId: String, + @Valid @PathParam("subjectId") subjectId: String, + @Valid @PathParam("notificationId") notificationId: Long, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + fcmNotificationService.scheduleNotification(subjectId, projectId, notificationId).let { + Response.ok(it).build() + } + } + } + + @POST + @Path("$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID/$MESSAGING_NOTIFICATION_PATH/batch") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_UPDATE, projectPathParam = "projectId", userPathParam = "subjectId") + fun addBatchNotifications( + @Valid @PathParam("projectId") projectId: String, + @Valid @PathParam("subjectId") subjectId: String, + @QueryParam("schedule") @DefaultValue("false") schedule: Boolean, + @Valid fcmNotification: FcmNotifications, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + fcmNotificationService.addNotifications( + fcmNotification, + subjectId, + projectId, + schedule, + ).let { + Response.ok(it).build() + } + } + } + + @PUT + @Path("$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID/$MESSAGING_NOTIFICATION_PATH") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_UPDATE, projectPathParam = "projectId", userPathParam = "subjectId") + fun updateNotification( + @Valid @PathParam("projectId") projectId: String, + @Valid @PathParam("subjectId") subjectId: String, + @Valid fcmNotification: FcmNotificationDto, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + fcmNotificationService.updateNotification( + fcmNotification, + subjectId, + projectId, + ).let { + Response.ok(it).build() + } + } + } + + @DELETE + @Path("$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID/$MESSAGING_NOTIFICATION_PATH/$ALL_KEYWORD") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_UPDATE, projectPathParam = "projectId", userPathParam = "subjectId") + fun deleteNotificationsForUser( + @Valid @PathParam("projectId") projectId: String, + @Valid @PathParam("subjectId") subjectId: String, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + fcmNotificationService.removeNotificationsForUser(projectId, subjectId).let { + Response.ok().build() + } + } + } + + @DELETE + @Path("$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID/$MESSAGING_NOTIFICATION_PATH/$NOTIFICATION_ID") + @Authenticated + @NeedsPermission(Permission.SUBJECT_UPDATE, projectPathParam = "projectId", userPathParam = "subjectId") + fun deleteNotificationUsingProjectIdAndSubjectIdAndNotificationId( + @Valid @PathParam("projectId") projectId: String, + @Valid @PathParam("subjectId") subjectId: String, + @PathParam("notificationId") id: Long, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + fcmNotificationService.deleteNotificationByProjectIdAndSubjectIdAndNotificationId( + projectId, + subjectId, + id, + ) + Response.ok().build() + } + } + + @DELETE + @Path("$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID/$MESSAGING_NOTIFICATION_PATH/$TASKS_PATH/{id}") + @Authenticated + @NeedsPermission(Permission.SUBJECT_UPDATE, projectPathParam = "projectId", userPathParam = "subjectId") + fun deleteNotificationUsingProjectIdAndSubjectIdAndTaskId( + @Valid @PathParam("projectId") projectId: String, + @Valid @PathParam("subjectId") subjectId: String, + @PathParam("id") id: Long, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + fcmNotificationService.removeNotificationsForUserUsingTaskId( + projectId, + subjectId, + id, + ) + Response.ok().build() + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/GithubResource.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/GithubResource.kt new file mode 100644 index 000000000..06b47595c --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/GithubResource.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.resource + +import jakarta.inject.Inject +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.Produces +import jakarta.ws.rs.QueryParam +import jakarta.ws.rs.container.AsyncResponse +import jakarta.ws.rs.container.Suspended +import jakarta.ws.rs.core.MediaType.TEXT_PLAIN +import jakarta.ws.rs.core.Response +import org.radarbase.appserver.jersey.config.AppserverConfig +import org.radarbase.appserver.jersey.service.github.GithubService +import org.radarbase.appserver.jersey.utils.Paths.GITHUB_CONTENT_PATH +import org.radarbase.appserver.jersey.utils.Paths.GITHUB_PATH +import org.radarbase.auth.authorization.Permission +import org.radarbase.jersey.auth.Authenticated +import org.radarbase.jersey.auth.NeedsPermission +import org.radarbase.jersey.service.AsyncCoroutineService +import java.net.MalformedURLException +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +@Path("/$GITHUB_PATH") +class GithubResource @Inject constructor( + private val githubService: GithubService, + private val asyncService: AsyncCoroutineService, + appserverConfig: AppserverConfig, +) { + private val requestTimeout: Duration = appserverConfig.server.requestTimeout.seconds + + @GET + @Path("/$GITHUB_CONTENT_PATH") + @Produces(TEXT_PLAIN) + @Authenticated + @NeedsPermission(Permission.SUBJECT_READ) + fun getGithubContent( + @QueryParam("url") url: String, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + try { + Response.ok(githubService.getGithubContent(url)).build() + } catch (ex: MalformedURLException) { + Response.status(Response.Status.BAD_REQUEST).entity(ex.message).build() + } catch (ex: Exception) { + Response.status(Response.Status.BAD_GATEWAY).entity( + "Error while fetching content from github: ${ex.message}", + ).build() + } + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/NotificationStateEventResource.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/NotificationStateEventResource.kt new file mode 100644 index 000000000..0acafb216 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/NotificationStateEventResource.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.resource + +import jakarta.inject.Inject +import jakarta.ws.rs.GET +import jakarta.ws.rs.POST +import jakarta.ws.rs.Path +import jakarta.ws.rs.PathParam +import jakarta.ws.rs.Produces +import jakarta.ws.rs.container.AsyncResponse +import jakarta.ws.rs.container.Suspended +import jakarta.ws.rs.core.MediaType.APPLICATION_JSON +import jakarta.ws.rs.core.Response +import org.radarbase.appserver.jersey.config.AppserverConfig +import org.radarbase.appserver.jersey.dto.NotificationStateEventDto +import org.radarbase.appserver.jersey.service.NotificationStateEventService +import org.radarbase.appserver.jersey.utils.Paths.MESSAGING_NOTIFICATION_PATH +import org.radarbase.appserver.jersey.utils.Paths.NOTIFICATION_ID +import org.radarbase.appserver.jersey.utils.Paths.NOTIFICATION_STATE_EVENTS_PATH +import org.radarbase.appserver.jersey.utils.Paths.PROJECTS_PATH +import org.radarbase.appserver.jersey.utils.Paths.PROJECT_ID +import org.radarbase.appserver.jersey.utils.Paths.SUBJECT_ID +import org.radarbase.appserver.jersey.utils.Paths.USERS_PATH +import org.radarbase.auth.authorization.Permission +import org.radarbase.jersey.auth.Authenticated +import org.radarbase.jersey.auth.NeedsPermission +import org.radarbase.jersey.service.AsyncCoroutineService +import kotlin.time.Duration.Companion.seconds + +@Suppress("UnresolvedRestParam") +@Path("/") +class NotificationStateEventResource @Inject constructor( + private val notificationStateEventService: NotificationStateEventService, + private val asyncService: AsyncCoroutineService, + appserverConfig: AppserverConfig, +) { + private val requestTimeout = appserverConfig.server.requestTimeout.seconds + + @GET + @Path("/$MESSAGING_NOTIFICATION_PATH/$NOTIFICATION_ID/$NOTIFICATION_STATE_EVENTS_PATH") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.PROJECT_READ) + fun getNotificationStateEventsByNotificationId( + @PathParam("notificationId") notificationId: Long, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + notificationStateEventService.getNotificationStateEventsByNotificationId(notificationId).let { + Response.ok(it).build() + } + } + } + + @GET + @Path("/$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID/$MESSAGING_NOTIFICATION_PATH/$NOTIFICATION_ID/$NOTIFICATION_STATE_EVENTS_PATH") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_READ, projectPathParam = "projectId", userPathParam = "subjectId") + fun getNotificationStateEvents( + @PathParam("projectId") projectId: String, + @PathParam("subjectId") subjectId: String, + @PathParam("notificationId") notificationId: Long, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + notificationStateEventService.getNotificationStateEvents( + projectId, + subjectId, + notificationId, + ).let { + Response.ok(it).build() + } + } + } + + @POST + @Path("/$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID/$MESSAGING_NOTIFICATION_PATH/$NOTIFICATION_ID/$NOTIFICATION_STATE_EVENTS_PATH") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_UPDATE, projectPathParam = "projectId", userPathParam = "subjectId") + fun postNotificationStateEvent( + @PathParam("projectId") projectId: String, + @PathParam("subjectId") subjectId: String, + @PathParam("notificationId") notificationId: Long, + notificationStateEventDto: NotificationStateEventDto, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + notificationStateEventService.publishNotificationStateEventExternal( + projectId, + subjectId, + notificationId, + notificationStateEventDto, + ) + notificationStateEventService.getNotificationStateEvents( + projectId, + subjectId, + notificationId, + ).let { + Response.ok(it).build() + } + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/ProjectResource.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/ProjectResource.kt new file mode 100644 index 000000000..63a681fa8 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/ProjectResource.kt @@ -0,0 +1,184 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.resource + +import jakarta.inject.Inject +import jakarta.inject.Provider +import jakarta.validation.Valid +import jakarta.ws.rs.Consumes +import jakarta.ws.rs.GET +import jakarta.ws.rs.POST +import jakarta.ws.rs.PUT +import jakarta.ws.rs.Path +import jakarta.ws.rs.PathParam +import jakarta.ws.rs.Produces +import jakarta.ws.rs.QueryParam +import jakarta.ws.rs.container.AsyncResponse +import jakarta.ws.rs.container.Suspended +import jakarta.ws.rs.core.MediaType.APPLICATION_JSON +import jakarta.ws.rs.core.Response +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.toList +import org.radarbase.appserver.jersey.config.AppserverConfig +import org.radarbase.appserver.jersey.dto.ProjectDto +import org.radarbase.appserver.jersey.dto.ProjectDtos +import org.radarbase.appserver.jersey.service.ProjectService +import org.radarbase.appserver.jersey.utils.Paths.PROJECTS_PATH +import org.radarbase.appserver.jersey.utils.Paths.PROJECT_ID +import org.radarbase.appserver.jersey.utils.tokenForCurrentRequest +import org.radarbase.auth.authorization.EntityDetails +import org.radarbase.auth.authorization.Permission +import org.radarbase.auth.token.RadarToken +import org.radarbase.jersey.auth.AuthService +import org.radarbase.jersey.auth.Authenticated +import org.radarbase.jersey.auth.NeedsPermission +import org.radarbase.jersey.service.AsyncCoroutineService +import java.net.URI +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +@Suppress("UnresolvedRestParam") +@Path("/") +class ProjectResource @Inject constructor( + private val projectService: ProjectService, + private val asyncService: AsyncCoroutineService, + private val authService: AuthService, + private val tokenProvider: Provider, + config: AppserverConfig, +) { + private val requestTimeout: Duration = config.server.requestTimeout.seconds + + @POST + @Path(PROJECTS_PATH) + @Consumes(APPLICATION_JSON) + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_READ) + fun addProject( + @Valid projectDto: ProjectDto, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + val token = tokenForCurrentRequest(asyncService, tokenProvider) + authService.checkPermission( + Permission.SUBJECT_READ, + EntityDetails(project = projectDto.projectId, subject = token.subject), + token, + ) + projectService.addProject(projectDto).let { + Response + .created(URI("/projects/project?id=${it.id}")) + .entity(it) + .build() + } + } + } + + @PUT + @Path("$PROJECTS_PATH/$PROJECT_ID") + @Consumes(APPLICATION_JSON) + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_UPDATE) + fun updateProject( + @Valid @PathParam("projectId") projectId: String, + @Valid projectDto: ProjectDto, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + val token = tokenForCurrentRequest(asyncService, tokenProvider) + authService.checkPermission( + Permission.SUBJECT_UPDATE, + EntityDetails(project = projectId, subject = token.subject), + token, + ) + projectService.updateProject(projectDto).let { + Response.ok(it).build() + } + } + } + + @GET + @Path(PROJECTS_PATH) + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.PROJECT_READ) + fun getAllProjects( + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + projectService.getAllProjects().projects + .asFlow() + .filter { + authService.hasPermission( + Permission.PROJECT_READ, + EntityDetails(project = it.projectId), + tokenForCurrentRequest(asyncService, tokenProvider), + ) + }.toList() + .toMutableList() + .let { + ProjectDtos(it) + }.let { + Response.ok(it).build() + } + } + } + + @GET + @Path("$PROJECTS_PATH/project") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.PROJECT_READ) + fun getProjectUsingId( + @QueryParam("id") id: Long, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + val project = projectService.getProjectById(id) + val token = tokenForCurrentRequest(asyncService, tokenProvider) + authService.checkPermission( + Permission.PROJECT_READ, + EntityDetails(project = project.projectId), + token, + ) + Response.ok(project).build() + } + } + + @GET + @Path("$PROJECTS_PATH/$PROJECT_ID") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_READ) + fun getProjectUsingProjectId( + @Valid @PathParam("projectId") projectId: String, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + val project = projectService.getProjectByProjectId(projectId) + val token = tokenForCurrentRequest(asyncService, tokenProvider) + authService.checkPermission( + Permission.SUBJECT_READ, + EntityDetails(project = project.projectId, subject = token.subject), + token, + ) + Response.ok(project).build() + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/ProtocolResource.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/ProtocolResource.kt new file mode 100644 index 000000000..f53230add --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/ProtocolResource.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.resource + +import jakarta.inject.Inject +import jakarta.validation.Valid +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.PathParam +import jakarta.ws.rs.Produces +import jakarta.ws.rs.container.AsyncResponse +import jakarta.ws.rs.container.Suspended +import jakarta.ws.rs.core.MediaType.APPLICATION_JSON +import jakarta.ws.rs.core.Response +import org.radarbase.appserver.jersey.config.AppserverConfig +import org.radarbase.appserver.jersey.service.github.protocol.ProtocolGenerator +import org.radarbase.appserver.jersey.utils.Paths.PROJECTS_PATH +import org.radarbase.appserver.jersey.utils.Paths.PROJECT_ID +import org.radarbase.appserver.jersey.utils.Paths.PROTOCOLS_PATH +import org.radarbase.appserver.jersey.utils.Paths.SUBJECT_ID +import org.radarbase.appserver.jersey.utils.Paths.USERS_PATH +import org.radarbase.auth.authorization.Permission +import org.radarbase.jersey.auth.Authenticated +import org.radarbase.jersey.auth.NeedsPermission +import org.radarbase.jersey.service.AsyncCoroutineService +import kotlin.time.Duration.Companion.seconds + +@Suppress("UnresolvedRestParam") +@Path("/") +class ProtocolResource @Inject constructor( + private val protocolGenerator: ProtocolGenerator, + private val asyncService: AsyncCoroutineService, + appserverConfig: AppserverConfig, +) { + private val requestTimeout = appserverConfig.server.requestTimeout.seconds + + @GET + @Path(PROTOCOLS_PATH) + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.PROJECT_READ) + fun getProtocols( + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + protocolGenerator.retrieveAllProtocols() + } + } + + @Suppress("UNUSED_PARAMETER") + @GET + @Path("$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID/$PROTOCOLS_PATH") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.PROJECT_READ, projectPathParam = "projectId", userPathParam = "subjectId") + fun getProtocolsUsingProjectIdAndSubjectId( + @Valid @PathParam("projectId") projectId: String, + @Valid @PathParam("subjectId") subjectId: String, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + protocolGenerator.getProtocolForSubject(subjectId) + } + } + + @GET + @Path("$PROJECTS_PATH/$PROJECT_ID/$PROTOCOLS_PATH") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.PROJECT_READ, projectPathParam = "projectId") + fun getProtocolsUsingProjectId( + @Valid @PathParam("projectId") projectId: String, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + try { + protocolGenerator.getProtocol(projectId).let { + Response.ok(it).build() + } + } catch (ex: Exception) { + Response.status(Response.Status.BAD_GATEWAY).entity(ex.message).build() + } + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/QuestionnaireScheduleResource.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/QuestionnaireScheduleResource.kt new file mode 100644 index 000000000..60485fe6e --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/QuestionnaireScheduleResource.kt @@ -0,0 +1,185 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("UnresolvedRestParam") + +package org.radarbase.appserver.jersey.resource + +import jakarta.inject.Inject +import jakarta.validation.Valid +import jakarta.ws.rs.DELETE +import jakarta.ws.rs.DefaultValue +import jakarta.ws.rs.GET +import jakarta.ws.rs.POST +import jakarta.ws.rs.PUT +import jakarta.ws.rs.Path +import jakarta.ws.rs.PathParam +import jakarta.ws.rs.QueryParam +import jakarta.ws.rs.container.AsyncResponse +import jakarta.ws.rs.container.Suspended +import jakarta.ws.rs.core.Response +import org.radarbase.appserver.jersey.config.AppserverConfig +import org.radarbase.appserver.jersey.dto.protocol.Assessment +import org.radarbase.appserver.jersey.dto.protocol.AssessmentType +import org.radarbase.appserver.jersey.service.questionnaire.schedule.QuestionnaireScheduleService +import org.radarbase.appserver.jersey.utils.Paths.PROJECTS_PATH +import org.radarbase.appserver.jersey.utils.Paths.PROJECT_ID +import org.radarbase.appserver.jersey.utils.Paths.QUESTIONNAIRE_SCHEDULE +import org.radarbase.appserver.jersey.utils.Paths.SUBJECT_ID +import org.radarbase.appserver.jersey.utils.Paths.USERS_PATH +import org.radarbase.auth.authorization.Permission +import org.radarbase.jersey.auth.Authenticated +import org.radarbase.jersey.auth.NeedsPermission +import org.radarbase.jersey.service.AsyncCoroutineService +import java.net.MalformedURLException +import java.net.URI +import java.time.Instant +import java.util.Locale +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +@Path("/") +class QuestionnaireScheduleResource @Inject constructor( + private val scheduleService: QuestionnaireScheduleService, + private val asyncService: AsyncCoroutineService, + appserverConfig: AppserverConfig, +) { + private val requestTimeout: Duration = appserverConfig.server.requestTimeout.seconds + + @POST + @Path("$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID/$QUESTIONNAIRE_SCHEDULE") + @Authenticated + @NeedsPermission(Permission.SUBJECT_UPDATE, projectPathParam = "projectId", userPathParam = "subjectId") + fun generateScheduleUsingProjectIdAndSubjectId( + @PathParam("projectId") projectId: String, + @PathParam("subjectId") subjectId: String, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + try { + scheduleService.generateScheduleUsingProjectIdAndSubjectId( + projectId, + subjectId, + ) + Response.created( + URI("$PROJECTS_PATH/$projectId/$USERS_PATH/$subjectId/$QUESTIONNAIRE_SCHEDULE"), + ).build() + } catch (ex: MalformedURLException) { + Response.status(Response.Status.BAD_REQUEST).entity( + "Error while generating schedule: ${ex.message}", + ) + } + } + } + + @PUT + @Path("$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID/$QUESTIONNAIRE_SCHEDULE") + @Authenticated + @NeedsPermission(Permission.SUBJECT_UPDATE, projectPathParam = "projectId", userPathParam = "subjectId") + fun generateScheduleUsingProtocol( + @Valid assessment: Assessment, + @PathParam("projectId") projectId: String, + @PathParam("subjectId") subjectId: String, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + try { + scheduleService.generateScheduleUsingProjectIdAndSubjectIdAndAssessment( + projectId, + subjectId, + assessment, + ) + Response.created( + URI("$PROJECTS_PATH/$projectId/$USERS_PATH/$subjectId/$QUESTIONNAIRE_SCHEDULE"), + ).build() + } catch (ex: MalformedURLException) { + Response.status(Response.Status.BAD_REQUEST).entity( + "Error while generating schedule: ${ex.message}", + ) + } + } + } + + @GET + @Path("$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID/$QUESTIONNAIRE_SCHEDULE") + @Authenticated + @NeedsPermission(Permission.SUBJECT_READ, projectPathParam = "projectId", userPathParam = "subjectId") + fun getScheduleUsingProjectIdAndSubjectId( + @Valid @PathParam("projectId") projectId: String, + @Valid @PathParam("subjectId") subjectId: String, + @QueryParam("type") @DefaultValue("all") type: String, + @QueryParam("search") @DefaultValue("") search: String, + @QueryParam("startTime") startTimeStr: String?, + @QueryParam("endTime") endTimeStr: String?, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + val startTime: Instant? = startTimeStr?.let { Instant.parse(it) } + val endTime: Instant? = endTimeStr?.let { Instant.parse(it) } + val assessmentType = AssessmentType.valueOf(type.uppercase(Locale.getDefault())) + + if (startTime != null && endTime != null) { + scheduleService.getTasksForDateUsingProjectIdAndSubjectId( + projectId, + subjectId, + startTime, + endTime, + ).let { + Response.ok(it).build() + } + } else if (assessmentType != AssessmentType.ALL) { + Response.ok( + scheduleService.getTasksByTypeUsingProjectIdAndSubjectId( + projectId, + subjectId, + assessmentType, + search, + ), + ).build() + } else { + Response.ok( + scheduleService.getTasksUsingProjectIdAndSubjectId( + projectId, + subjectId, + ), + ).build() + } + } + } + + @DELETE + @Path("$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID/$QUESTIONNAIRE_SCHEDULE") + @Authenticated + @NeedsPermission(Permission.SUBJECT_UPDATE, projectPathParam = "projectId", userPathParam = "subjectId") + fun deleteScheduleForUser( + @PathParam("projectId") projectId: String, + @PathParam("subjectId") subjectId: String, + @QueryParam("type") @DefaultValue("all") type: String, + @QueryParam("search") @DefaultValue("") search: String, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + val assessmentType = AssessmentType.valueOf(type.uppercase(Locale.getDefault())) + scheduleService.removeScheduleForUserUsingSubjectIdAndType( + projectId, + subjectId, + assessmentType, + search, + ) + Response.ok().build() + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/TaskStateEventResource.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/TaskStateEventResource.kt new file mode 100644 index 000000000..877c3547c --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/TaskStateEventResource.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.resource + +import jakarta.inject.Inject +import jakarta.ws.rs.GET +import jakarta.ws.rs.POST +import jakarta.ws.rs.Path +import jakarta.ws.rs.PathParam +import jakarta.ws.rs.Produces +import jakarta.ws.rs.container.AsyncResponse +import jakarta.ws.rs.container.Suspended +import jakarta.ws.rs.core.MediaType.APPLICATION_JSON +import jakarta.ws.rs.core.Response +import org.radarbase.appserver.jersey.config.AppserverConfig +import org.radarbase.appserver.jersey.dto.TaskStateEventDto +import org.radarbase.appserver.jersey.service.TaskStateEventService +import org.radarbase.appserver.jersey.utils.Paths.PROJECTS_PATH +import org.radarbase.appserver.jersey.utils.Paths.PROJECT_ID +import org.radarbase.appserver.jersey.utils.Paths.QUESTIONNAIRE_SCHEDULE +import org.radarbase.appserver.jersey.utils.Paths.QUESTIONNAIRE_STATE_EVENTS_PATH +import org.radarbase.appserver.jersey.utils.Paths.SUBJECT_ID +import org.radarbase.appserver.jersey.utils.Paths.TASK_ID +import org.radarbase.appserver.jersey.utils.Paths.USERS_PATH +import org.radarbase.auth.authorization.Permission +import org.radarbase.jersey.auth.Authenticated +import org.radarbase.jersey.auth.NeedsPermission +import org.radarbase.jersey.service.AsyncCoroutineService +import kotlin.time.Duration.Companion.seconds + +@Suppress("UnresolvedRestParam") +@Path("/") +class TaskStateEventResource @Inject constructor( + private val taskStateEventService: TaskStateEventService, + private val asyncService: AsyncCoroutineService, + appserverConfig: AppserverConfig, +) { + private val requestTimeout = appserverConfig.server.requestTimeout.seconds + + @GET + @Path("/$QUESTIONNAIRE_SCHEDULE/$TASK_ID/$QUESTIONNAIRE_STATE_EVENTS_PATH") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_READ) + fun getTaskStateEventsByTaskId( + @PathParam("taskId") taskId: Long, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + taskStateEventService.getTaskStateEventsByTaskId(taskId).let { + Response.ok(it).build() + } + } + } + + @GET + @Path("/$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID/$QUESTIONNAIRE_SCHEDULE/$TASK_ID/$QUESTIONNAIRE_STATE_EVENTS_PATH") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_READ, projectPathParam = "projectId", userPathParam = "subjectId") + fun getTaskStateEvents( + @PathParam("projectId") projectId: String, + @PathParam("subjectId") subjectId: String, + @PathParam("taskId") taskId: Long, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + taskStateEventService.getTaskStateEvents(projectId, subjectId, taskId).let { + Response.ok(it).build() + } + } + } + + @POST + @Path("/$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID/$QUESTIONNAIRE_SCHEDULE/$TASK_ID/$QUESTIONNAIRE_STATE_EVENTS_PATH") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_UPDATE, projectPathParam = "projectId", userPathParam = "subjectId") + fun postTaskStateEvents( + @PathParam("projectId") projectId: String, + @PathParam("subjectId") subjectId: String, + @PathParam("taskId") taskId: Long, + taskStateEventDto: TaskStateEventDto, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + taskStateEventService.publishNotificationStateEventExternal( + projectId, + subjectId, + taskId, + taskStateEventDto, + ) + + taskStateEventService.getTaskStateEvents(projectId, subjectId, taskId).let { + Response.ok(it).build() + } + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/UserResource.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/UserResource.kt new file mode 100644 index 000000000..153e6f2ca --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/resource/UserResource.kt @@ -0,0 +1,245 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.resource + +import jakarta.inject.Inject +import jakarta.inject.Provider +import jakarta.validation.Valid +import jakarta.ws.rs.Consumes +import jakarta.ws.rs.DELETE +import jakarta.ws.rs.DefaultValue +import jakarta.ws.rs.GET +import jakarta.ws.rs.POST +import jakarta.ws.rs.PUT +import jakarta.ws.rs.Path +import jakarta.ws.rs.PathParam +import jakarta.ws.rs.Produces +import jakarta.ws.rs.QueryParam +import jakarta.ws.rs.container.AsyncResponse +import jakarta.ws.rs.container.Suspended +import jakarta.ws.rs.core.MediaType.APPLICATION_JSON +import jakarta.ws.rs.core.Response +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.toList +import org.radarbase.appserver.jersey.config.AppserverConfig +import org.radarbase.appserver.jersey.dto.fcm.FcmUserDto +import org.radarbase.appserver.jersey.dto.fcm.FcmUsers +import org.radarbase.appserver.jersey.service.UserService +import org.radarbase.appserver.jersey.utils.Paths.PROJECTS_PATH +import org.radarbase.appserver.jersey.utils.Paths.PROJECT_ID +import org.radarbase.appserver.jersey.utils.Paths.SUBJECT_ID +import org.radarbase.appserver.jersey.utils.Paths.USERS_PATH +import org.radarbase.appserver.jersey.utils.tokenForCurrentRequest +import org.radarbase.auth.authorization.EntityDetails +import org.radarbase.auth.authorization.Permission +import org.radarbase.auth.token.RadarToken +import org.radarbase.jersey.auth.AuthService +import org.radarbase.jersey.auth.Authenticated +import org.radarbase.jersey.auth.NeedsPermission +import org.radarbase.jersey.service.AsyncCoroutineService +import java.net.URI +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +@Suppress("UnresolvedRestParam") +@Path("/") +class UserResource @Inject constructor( + private val userService: UserService, + private val asyncService: AsyncCoroutineService, + private val authService: AuthService, + private val tokenProvider: Provider, + config: AppserverConfig, +) { + private val requestTimeout: Duration = config.server.requestTimeout.seconds + + @POST + @Path("$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH") + @Consumes(APPLICATION_JSON) + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_UPDATE) + fun addUserToProject( + @Valid fcmUserDto: FcmUserDto, + @Valid @PathParam("projectId") projectId: String, + @QueryParam("forceFcmToken") @DefaultValue("false") forceFcmToken: Boolean, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + fcmUserDto.projectId = projectId + val token = tokenForCurrentRequest(asyncService, tokenProvider) + authService.checkPermission( + Permission.SUBJECT_UPDATE, + EntityDetails(project = projectId, subject = token.subject), + token, + ) + if (forceFcmToken) userService.checkFcmTokenExistsAndReplace(fcmUserDto) + userService.saveUserInProject(fcmUserDto).let { + Response.created(URI("/projects/$projectId/users/?id=${it.id}")).entity(it).build() + } + } + } + + @PUT + @Path("$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID") + @Consumes(APPLICATION_JSON) + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_UPDATE, projectPathParam = "projectId", userPathParam = "subjectId") + fun updateUserInProject( + @Valid userDto: FcmUserDto, + @Valid @PathParam("projectId") projectId: String, + @Valid @PathParam("subjectId") subjectId: String, + @QueryParam("forceFcmToken") @DefaultValue("false") forceFcmToken: Boolean, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + userDto.apply { + this.subjectId = subjectId + this.projectId = projectId + } + if (forceFcmToken) userService.checkFcmTokenExistsAndReplace(userDto) + userService.updateUser(userDto).let { + Response.ok(it).build() + } + } + } + + @GET + @Path(USERS_PATH) + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_READ) + fun getAllRadarUsers( + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + userService.getAllRadarUsers() + .users + .asFlow() + .filter { + authService.hasPermission( + Permission.SUBJECT_READ, + EntityDetails(project = it.projectId, subject = it.subjectId), + tokenForCurrentRequest(asyncService, tokenProvider), + ) + } + .toList() + .toMutableList().let { + FcmUsers(it) + }.let { + Response.ok(it).build() + } + } + } + + @GET + @Path("$USERS_PATH/user") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_READ) + fun getRadarUserUsingId( + @QueryParam("id") id: Long, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + val user: FcmUserDto = userService.getUserById(id) + fcmUserDtoAsResponseIfAuthorized(user) + } + } + + @GET + @Path("$USERS_PATH/$SUBJECT_ID") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_READ) + fun getRadarUserUsingSubjectId( + @PathParam("subjectId") subjectId: String, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + val user: FcmUserDto = userService.getUserBySubjectId(subjectId) + fcmUserDtoAsResponseIfAuthorized(user) + } + } + + suspend fun fcmUserDtoAsResponseIfAuthorized( + fcmUserDto: FcmUserDto, + ): Response { + authService.checkPermission( + Permission.SUBJECT_READ, + EntityDetails(project = fcmUserDto.projectId, subject = fcmUserDto.subjectId), + tokenForCurrentRequest(asyncService, tokenProvider), + ) + + return Response.ok(fcmUserDto).build() + } + + @GET + @Path("$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_READ) + fun getUsersUsingProjectId( + @Valid @PathParam("projectId") projectId: String, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + val users = userService.getUsersByProjectId(projectId) + val token = tokenForCurrentRequest(asyncService, tokenProvider) + authService.checkPermission( + Permission.SUBJECT_READ, + EntityDetails(project = projectId, subject = token.subject), + token, + ) + Response.ok(users).build() + } + } + + @GET + @Path("$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_READ, projectPathParam = "projectId", userPathParam = "subjectId") + fun getUsersUsingProjectIdAndSubjectId( + @Valid @PathParam("projectId") projectId: String, + @Valid @PathParam("subjectId") subjectId: String, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + userService.getUserByProjectIdAndSubjectId(projectId, subjectId).let { + Response.ok(it).build() + } + } + } + + @DELETE + @Path("$PROJECTS_PATH/$PROJECT_ID/$USERS_PATH/$SUBJECT_ID") + @Produces(APPLICATION_JSON) + @Authenticated + @NeedsPermission(Permission.SUBJECT_UPDATE, projectPathParam = "projectId", userPathParam = "subjectId") + fun deleteUserUsingProjectIdAndSubjectId( + @Valid @PathParam("projectId") projectId: String, + @Valid @PathParam("subjectId") subjectId: String, + @Suspended asyncResponse: AsyncResponse, + ) { + asyncService.runAsCoroutine(asyncResponse, requestTimeout) { + userService.deleteUserByProjectIdAndSubjectId(projectId, subjectId) + Response.ok().build() + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/search/QuerySpecification.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/search/QuerySpecification.kt new file mode 100644 index 000000000..65934e31b --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/search/QuerySpecification.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.search + +import jakarta.persistence.criteria.CriteriaBuilder +import jakarta.persistence.criteria.CriteriaQuery +import jakarta.persistence.criteria.Predicate +import jakarta.persistence.criteria.Root + +fun interface QuerySpecification { + fun toPredicate(root: Root, criteriaQuery: CriteriaQuery<*>, criteriaBuilder: CriteriaBuilder): Predicate +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/search/SearchCriteria.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/search/SearchCriteria.kt new file mode 100644 index 000000000..1dc83ef99 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/search/SearchCriteria.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.search + +data class SearchCriteria( + val key: String, + val operation: String, + val value: Any, +) { + /*** + * Only AND supported in the first instance. Later we can add a new query param that can provide this value + */ + fun isOrPredicate(): Boolean = false +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/search/TaskSpecification.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/search/TaskSpecification.kt new file mode 100644 index 000000000..087d6a5d7 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/search/TaskSpecification.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.search + +import jakarta.persistence.criteria.CriteriaBuilder +import jakarta.persistence.criteria.CriteriaQuery +import jakarta.persistence.criteria.Predicate +import jakarta.persistence.criteria.Root +import org.radarbase.appserver.jersey.entity.Task + +@Suppress("UNCHECKED_CAST") +class TaskSpecification( + private val searchCriteria: SearchCriteria, +) : QuerySpecification { + + override fun toPredicate( + root: Root, + criteriaQuery: CriteriaQuery<*>, + criteriaBuilder: CriteriaBuilder, + ): Predicate { + val path = root.get(searchCriteria.key) + return when (searchCriteria.operation) { + ">" -> criteriaBuilder.greaterThanOrEqualTo( + root.get(searchCriteria.key), + searchCriteria.value as Comparable, + ) + + "<" -> criteriaBuilder.lessThanOrEqualTo( + root.get(searchCriteria.key), + searchCriteria.value as Comparable, + ) + + ":" -> if (path.javaType == String::class.java) { + criteriaBuilder.like( + root.get(searchCriteria.key), + "%${searchCriteria.value}%", + ) + } else { + criteriaBuilder.equal(path, searchCriteria.value) + } + + else -> throw IllegalArgumentException("Unknown operation: ${searchCriteria.operation}") + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/search/TaskSpecificationsBuilder.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/search/TaskSpecificationsBuilder.kt new file mode 100644 index 000000000..475193e0c --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/search/TaskSpecificationsBuilder.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.search + +import jakarta.persistence.criteria.CriteriaBuilder +import jakarta.persistence.criteria.CriteriaQuery +import jakarta.persistence.criteria.Predicate +import jakarta.persistence.criteria.Root +import org.radarbase.appserver.jersey.entity.Task + +class TaskSpecificationsBuilder { + private val params = mutableListOf() + + fun with(key: String, op: String, value: Any): TaskSpecificationsBuilder { + params.add(SearchCriteria(key, op, value)) + return this + } + + fun build(): QuerySpecification { + return QuerySpecification { root: Root, query: CriteriaQuery<*>, builder: CriteriaBuilder -> + if (params.isEmpty()) return@QuerySpecification builder.conjunction() + + val predicates: List = params.map { + TaskSpecification(it).toPredicate(root, query, builder) + } + + var result = predicates.first() + for (i in 1 until predicates.size) { + result = if (params[i].isOrPredicate()) { + builder.or(result, predicates[i]) + } else { + builder.and(result, predicates[i]) + } + } + result + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/serialization/Base64AsStringSerializer.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/serialization/Base64AsStringSerializer.kt new file mode 100644 index 000000000..33337f1fc --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/serialization/Base64AsStringSerializer.kt @@ -0,0 +1,30 @@ +package org.radarbase.appserver.jersey.serialization + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import java.util.Base64 + +object Base64AsStringSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("Base64AsString", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: String) { + // when writing JSON, we must re-encode into Base64 + val encoded = Base64.getEncoder().encodeToString(value.toByteArray()) + encoder.encodeString(encoded) + } + + override fun deserialize(decoder: Decoder): String { + val raw = decoder.decodeString().replace(Regex("[\n\r]"), "") + return try { + val decodedBytes = Base64.getDecoder().decode(raw) + String(decodedBytes) + } catch (e: IllegalArgumentException) { + throw IllegalArgumentException("Invalid base64 value: $raw", e) + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/serialization/ReferenceTimestampSerializer.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/serialization/ReferenceTimestampSerializer.kt new file mode 100644 index 000000000..48a3467ac --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/serialization/ReferenceTimestampSerializer.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.serialization + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import org.radarbase.appserver.jersey.dto.protocol.ReferenceTimestamp +import org.radarbase.appserver.jersey.dto.protocol.ReferenceTimestampType + +object ReferenceTimestampSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor( + "ReferenceTimestamp", + ) + + override fun serialize(encoder: Encoder, value: ReferenceTimestamp) { + encoder.encodeString(value.timestamp.toString()) + } + + override fun deserialize(decoder: Decoder): ReferenceTimestamp { + val input = decoder as? JsonDecoder + ?: throw IllegalStateException("Only works with JSON") + + val element = input.decodeJsonElement() + return when (element) { + is JsonObject -> { + input.json.decodeFromJsonElement(ReferenceTimestamp.serializer(), element) + } + is JsonPrimitive -> { + ReferenceTimestamp(element.content, ReferenceTimestampType.DATETIMEUTC) + } + else -> throw IllegalArgumentException("Unexpected JSON for ReferenceTimestamp: $element") + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/serialization/URISerializer.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/serialization/URISerializer.kt new file mode 100644 index 000000000..5f3e204e8 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/serialization/URISerializer.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.serialization + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import java.net.URI + +object URISerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor( + "java.net.URI", + PrimitiveKind.STRING, + ) + + override fun serialize(encoder: Encoder, value: URI) { + encoder.encodeString(value.toString()) + } + + override fun deserialize(decoder: Decoder): URI { + return URI(decoder.decodeString()) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/DataMessageService.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/DataMessageService.kt new file mode 100644 index 000000000..9baa8f9a0 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/DataMessageService.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service + +interface DataMessageService diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/DataMessageStateEventService.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/DataMessageStateEventService.kt new file mode 100644 index 000000000..cf91f49d7 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/DataMessageStateEventService.kt @@ -0,0 +1,164 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service + +import com.google.common.eventbus.EventBus +import jakarta.inject.Inject +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.json.Json +import org.glassfish.hk2.api.ServiceLocator +import org.radarbase.appserver.jersey.dto.DataMessageStateEventDto +import org.radarbase.appserver.jersey.entity.DataMessage +import org.radarbase.appserver.jersey.entity.DataMessageStateEvent +import org.radarbase.appserver.jersey.event.state.MessageState +import org.radarbase.appserver.jersey.repository.DataMessageStateEventRepository +import java.io.IOException + +@Suppress("unused") +class DataMessageStateEventService @Inject constructor( + private val dataMessageStateEventRepository: DataMessageStateEventRepository, + private val dataMessageService: FcmDataMessageService, + private val serviceLocator: ServiceLocator, +) { + private var dataMessageEventBus: EventBus? = null + get() { + if (field == null) { + return serviceLocator.getService(EventBus::class.java) + ?.also { field = it } + } + return field + } + + suspend fun addDataMessageStateEvent(dataMessageStateEvent: DataMessageStateEvent) { + dataMessageStateEventRepository.add(dataMessageStateEvent) + } + + suspend fun getDataMessageStateEvents( + projectId: String, + subjectId: String, + dataMessageId: Long, + ): List { + dataMessageService.getDataMessageByProjectIdAndSubjectIdAndDataMessageId( + projectId, + subjectId, + dataMessageId, + ) + + val stateEvents: List = + dataMessageStateEventRepository.findByDataMessageId(dataMessageId) + return stateEvents.map { stateEvent: DataMessageStateEvent -> + DataMessageStateEventDto( + stateEvent.id, + nonNullDataMessage(stateEvent).id, + stateEvent.state, + stateEvent.time, + stateEvent.associatedInfo, + ) + } + } + + suspend fun getDataMessageStateEventsByDataMessageId( + dataMessageId: Long, + ): List { + val stateEvents: List = + dataMessageStateEventRepository.findByDataMessageId(dataMessageId) + return stateEvents.map { stateEvent: DataMessageStateEvent -> + DataMessageStateEventDto( + stateEvent.id, + nonNullDataMessage(stateEvent).id, + stateEvent.state, + stateEvent.time, + stateEvent.associatedInfo, + ) + } + } + + suspend fun publishDataMessageStateEventExternal( + projectId: String, + subjectId: String, + dataMessageId: Long, + dataMessageStateEventDto: DataMessageStateEventDto, + ) { + checkState(dataMessageId, dataMessageStateEventDto.state) + val dataMessage = dataMessageService.getDataMessageByProjectIdAndSubjectIdAndDataMessageId( + projectId, + subjectId, + dataMessageId, + ) + + var additionalInfo: Map? = null + + if (!dataMessageStateEventDto.associatedInfo.isNullOrEmpty()) { + try { + additionalInfo = Json.decodeFromString( + MapSerializer(String.serializer(), String.serializer()), + dataMessageStateEventDto.associatedInfo!!, + ) + } catch (_: IOException) { + throw IllegalStateException( + "Cannot convert additionalInfo to Map. Please check its format.", + ) + } + } + + val messageState = requireNotNull(dataMessageStateEventDto.state) { + "Data Message state event's state can't be null." + } + val messageTime = requireNotNull(dataMessageStateEventDto.time) { + "Data Message state event's time can't be null." + } + + val stateEvent = org.radarbase.appserver.jersey.event.state.dto.DataMessageStateEventDto( + dataMessage, + messageState, + additionalInfo, + messageTime, + ) + dataMessageEventBus?.post(stateEvent) ?: logger.error("Event bus is not initialized.") + } + + @Throws(IllegalStateException::class) + private suspend fun checkState(dataMessageId: Long, state: MessageState?) { + if (EXTERNAL_EVENTS.contains(state)) { + if (dataMessageStateEventRepository.countByDataMessageId(dataMessageId) >= MAX_NUMBER_OF_STATES) { + throw IllegalStateException( + ("The max limit of state changes($MAX_NUMBER_OF_STATES) has been reached. Cannot add new states."), + ) + } + } else { + throw IllegalStateException(("The state $state is not an external state and cannot be updated by this endpoint.")) + } + } + + companion object { + private val logger = org.slf4j.LoggerFactory.getLogger(DataMessageStateEventService::class.java) + private val EXTERNAL_EVENTS = setOf( + MessageState.DELIVERED, + MessageState.DISMISSED, + MessageState.OPENED, + MessageState.UNKNOWN, + MessageState.ERRORED, + ) + private const val MAX_NUMBER_OF_STATES = 20 + + private fun nonNullDataMessage(stateEvent: DataMessageStateEvent): DataMessage = + checkNotNull(stateEvent.dataMessage) { + "DataMessage in state event data can't be null" + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/FcmDataMessageService.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/FcmDataMessageService.kt new file mode 100644 index 000000000..0d972db52 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/FcmDataMessageService.kt @@ -0,0 +1,377 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service + +import com.google.common.eventbus.EventBus +import jakarta.inject.Inject +import jakarta.inject.Named +import org.radarbase.appserver.jersey.dto.fcm.FcmDataMessageDto +import org.radarbase.appserver.jersey.dto.fcm.FcmDataMessages +import org.radarbase.appserver.jersey.enhancer.AppserverResourceEnhancer.Companion.DATA_MESSAGE_MAPPER +import org.radarbase.appserver.jersey.entity.DataMessage +import org.radarbase.appserver.jersey.entity.Project +import org.radarbase.appserver.jersey.entity.User +import org.radarbase.appserver.jersey.event.state.MessageState +import org.radarbase.appserver.jersey.event.state.dto.DataMessageStateEventDto +import org.radarbase.appserver.jersey.exception.AlreadyExistsException +import org.radarbase.appserver.jersey.exception.InvalidNotificationDetailsException +import org.radarbase.appserver.jersey.exception.InvalidUserDetailsException +import org.radarbase.appserver.jersey.mapper.Mapper +import org.radarbase.appserver.jersey.repository.DataMessageRepository +import org.radarbase.appserver.jersey.repository.ProjectRepository +import org.radarbase.appserver.jersey.repository.UserRepository +import org.radarbase.appserver.jersey.service.TaskService.Companion.nonNullUserId +import org.radarbase.appserver.jersey.service.questionnaire.schedule.MessageSchedulerService +import org.radarbase.appserver.jersey.utils.checkInvalidDetails +import org.radarbase.appserver.jersey.utils.checkPresence +import org.radarbase.appserver.jersey.utils.requireNotNullField +import org.radarbase.jersey.exception.HttpNotFoundException +import java.time.Instant +import java.time.LocalDateTime +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +@Suppress("unused") +class FcmDataMessageService @Inject constructor( + private val dataMessageRepository: DataMessageRepository, + private val userRepository: UserRepository, + private val projectRepository: ProjectRepository, + private val schedulerService: MessageSchedulerService, + @field:Named(DATA_MESSAGE_MAPPER) private val dataMessageMapper: Mapper, + private val dataMessageStateEventPublisher: EventBus, +) : DataMessageService { + + // TODO Add option to specify a scheduling provider (default will be fcm) + // TODO: Use strategy pattern for handling data messages for scheduling and adding to database + + suspend fun getAllDataMessages(): FcmDataMessages { + val dataMessages = dataMessageRepository.findAll() + return FcmDataMessages(dataMessageMapper.entitiesToDtos(dataMessages).toMutableList()) + } + + suspend fun getDataMessageById(id: Long): FcmDataMessageDto { + val dataMessage = dataMessageRepository.find(id) + return dataMessageMapper.entityToDto(dataMessage ?: DataMessage()) + } + + suspend fun getDataMessagesBySubjectId(subjectId: String): FcmDataMessages { + val user = this.userRepository.findBySubjectId(subjectId) + checkPresenceOfUser(user) + + val dataMessages = dataMessageRepository.findByUserId(nonNullUserId(user)) + return FcmDataMessages( + dataMessageMapper.entitiesToDtos(dataMessages).toMutableList(), + ) + } + + suspend fun getDataMessagesByProjectIdAndSubjectId( + projectId: String, + subjectId: String, + ): FcmDataMessages { + val user = subjectAndProjectExistElseThrow(subjectId, projectId) + + val dataMessages = dataMessageRepository.findByUserId(nonNullUserId(user)) + return FcmDataMessages( + dataMessageMapper.entitiesToDtos(dataMessages).toMutableList(), + ) + } + + suspend fun getDataMessagesByProjectId(projectId: String): FcmDataMessages { + val project = projectRepository.findByProjectId(projectId) + + checkPresence(project, "project_not_found") { + "Project not found with projectId $projectId" + } + + val users: List = this.userRepository.findByProjectId(nonNullProjectId(project)) + val dataMessages: MutableSet = hashSetOf() + users.flatMapTo(dataMessages) { user -> + this.dataMessageRepository.findByUserId(nonNullUserId(user)) + } + return FcmDataMessages( + dataMessageMapper.entitiesToDtos(dataMessages).toMutableList(), + ) + } + + suspend fun checkIfDataMessageExists(dataMessageDto: FcmDataMessageDto, subjectId: String): Boolean { + val user = this.userRepository.findBySubjectId(subjectId) + checkPresence(user, "user_not_found") { + INVALID_SUBJECT_ID_MESSAGE + } + val dataMessage = DataMessage.DataMessageBuilder( + dataMessageMapper.dtoToEntity(dataMessageDto), + ).user(user).build() + + val dataMessages = this.dataMessageRepository.findByUserId(nonNullUserId(user)) + return dataMessages.contains(dataMessage) + } + + // TODO : WIP + @Suppress("UNUSED_PARAMETER") + fun getFilteredDataMessages( + type: String?, + delivered: Boolean?, + ttlSeconds: Int?, + startTime: LocalDateTime?, + endTime: LocalDateTime?, + limit: Int?, + ): FcmDataMessages? = null + + suspend fun addDataMessage( + dataMessageDto: FcmDataMessageDto, + subjectId: String, + projectId: String, + ): FcmDataMessageDto { + val user = subjectAndProjectExistElseThrow(subjectId, projectId) + + if (!dataMessageRepository + .existsByUserIdAndSourceIdAndScheduledTimeAndTtlSeconds( + nonNullUserId(user), + requireNotNullField(dataMessageDto.sourceId, "Data Message source id"), + requireNotNullField(dataMessageDto.scheduledTime, "Data Message scheduled time"), + requireNotNullField(dataMessageDto.ttlSeconds, "Data Message ttl seconds"), + ) + ) { + val dataMessageSaved = dataMessageRepository.add( + DataMessage.DataMessageBuilder(dataMessageMapper.dtoToEntity(dataMessageDto)).user(user).build(), + ) + requireNotNullField(user.usermetrics, "User's user metrics").lastOpened = Instant.now() + this.userRepository.update(user) + addDataMessageStateEvent( + dataMessageSaved, + MessageState.ADDED, + requireNotNullField( + dataMessageSaved.createdAt, + "Data message creation timestamp", + ).toInstant(), + ) + this.schedulerService.schedule(dataMessageSaved) + return dataMessageMapper.entityToDto(dataMessageSaved) + } else { + throw AlreadyExistsException( + "data_message_already_exists", + "The Data Message Already exists. Please Use update endpoint", + ) + } + } + + private fun addDataMessageStateEvent( + dataMessage: DataMessage, + state: MessageState, + time: Instant, + ) { + val dataMessageStateEvent = DataMessageStateEventDto( + dataMessage, + state, + null, + time, + ) + dataMessageStateEventPublisher.post(dataMessageStateEvent) + } + + suspend fun updateDataMessage( + dataMessageDto: FcmDataMessageDto, + subjectId: String, + projectId: String, + ): FcmDataMessageDto { + val dmDtoId = dataMessageDto.id ?: throw InvalidNotificationDetailsException( + "ID must be supplied for updating the data message", + ) + val user = subjectAndProjectExistElseThrow(subjectId, projectId) + val dataMessage = this.dataMessageRepository.find(dmDtoId) + + checkPresence(dataMessage, "data_message_not_found") { + "Data message does not exist. Please create first" + } + + val newDataMessage = DataMessage.DataMessageBuilder(dataMessage) + .scheduledTime(dataMessageDto.scheduledTime) + .sourceId(dataMessageDto.sourceId) + .ttlSeconds(dataMessageDto.ttlSeconds) + .user(user) + .fcmMessageId(dataMessageDto.hashCode().toString()) + .build() + + val dataMessageSaved = this.dataMessageRepository.update(newDataMessage) ?: throw IllegalStateException( + "Data message cannot be updated", + ) + addDataMessageStateEvent( + dataMessageSaved, + MessageState.UPDATED, + requireNotNullField( + dataMessageSaved.updatedAt, + "Data message update timestamp", + ).toInstant(), + ) + if (!dataMessage.delivered) { + this.schedulerService.updateScheduled(dataMessageSaved) + } + return dataMessageMapper.entityToDto(dataMessageSaved) + } + + suspend fun removeDataMessagesForUser(projectId: String, subjectId: String) { + val userId = nonNullUserId(subjectAndProjectExistElseThrow(subjectId, projectId)) + val dataMessages = this.dataMessageRepository.findByUserId( + userId, + ) + this.schedulerService.deleteScheduledMultiple(dataMessages) + this.dataMessageRepository.deleteByUserId( + userId, + ) + } + + suspend fun updateDeliveryStatus(fcmMessageId: String, isDelivered: Boolean) { + val dataMessage = this.dataMessageRepository.findByFcmMessageId(fcmMessageId) + checkInvalidDetails( + { + dataMessage == null + }, + { + "Data message with the provided FCM message ID does not exist." + }, + ) + + val newDataMessage = DataMessage.DataMessageBuilder(dataMessage).delivered(isDelivered).build() + this.dataMessageRepository.update(newDataMessage) + } + + // TODO: Investigate if data messages/notifications can be marked in the state CANCELLED when deleted. + suspend fun deleteDataMessageByProjectIdAndSubjectIdAndDataMessageId( + projectId: String, + subjectId: String, + id: Long, + ) { + val userId = nonNullUserId(subjectAndProjectExistElseThrow(subjectId, projectId)) + if (dataMessageRepository.existsByIdAndUserId(id, userId)) { + this.dataMessageRepository.deleteByIdAndUserId(id, userId) + } else { + throw InvalidNotificationDetailsException( + "Data message with the provided ID does not exist.", + ) + } + } + + suspend fun removeDataMessagesForUserUsingFcmToken(fcmToken: String) { + val user = this.userRepository.findByFcmToken(fcmToken) + if (user == null) { + throw InvalidUserDetailsException("The user with the given Fcm Token does not exist") + } else { + this.dataMessageRepository.deleteByUserId(nonNullUserId(user)) + /*User newUser = user1.setFcmToken(""); + this.userRepository.save(newUser);*/ + } + } + + suspend fun addDataMessages( + dataMessageDtos: FcmDataMessages, + subjectId: String, + projectId: String, + ): FcmDataMessages { + val user = subjectAndProjectExistElseThrow(subjectId, projectId) + val dataMessages = dataMessageRepository.findByUserId(nonNullUserId(user)) + + val newDataMessages = dataMessageDtos.dataMessages.map { dto -> + dataMessageMapper.dtoToEntity(dto) + }.map { dm -> + DataMessage.DataMessageBuilder(dm).user(user).build() + }.filter { dataMessage: DataMessage? -> + !dataMessages.contains(dataMessage) + } + + val savedDataMessages: List = newDataMessages.map { + this.dataMessageRepository.add(it) + } + + savedDataMessages.forEach { dm -> + addDataMessageStateEvent( + dm, + MessageState.ADDED, + requireNotNullField(dm.createdAt, "Data message creation timestamp").toInstant(), + ) + } + + this.schedulerService.scheduleMultiple(savedDataMessages) + return FcmDataMessages( + dataMessageMapper.entitiesToDtos(dataMessages).toMutableList(), + ) + } + + suspend fun subjectAndProjectExistElseThrow(subjectId: String, projectId: String): User { + val project = this.projectRepository.findByProjectId(projectId) + if (project == null || project.id == null) { + throw HttpNotFoundException( + "project_not_found", + "Project Id does not exist. Please create a project with the ID first", + ) + } + val user: User = this.userRepository.findBySubjectIdAndProjectId(subjectId, project.id!!)!! + checkPresence(user, "user_not_found") { + INVALID_SUBJECT_ID_MESSAGE + } + return user + } + + suspend fun getDataMessageByProjectIdAndSubjectIdAndDataMessageId( + projectId: String, + subjectId: String, + dataMessageId: Long, + ): DataMessage { + val user = subjectAndProjectExistElseThrow(subjectId, projectId) + val dataMessage = dataMessageRepository.findByIdAndUserId(dataMessageId, nonNullUserId(user)) + checkInvalidDetails( + { + dataMessage == null + }, + { + "The Data message with Id $dataMessageId does not exist in project $projectId for user $subjectId" + }, + ) + return dataMessage!! + } + + suspend fun getDataMessageByMessageId(messageId: String): DataMessage { + val dataMessage = this.dataMessageRepository.findByFcmMessageId(messageId) + checkInvalidDetails( + { + dataMessage == null + }, + { + "The Data message with FCM Message Id $messageId does not exist." + }, + ) + return dataMessage!! + } + + companion object { + private const val INVALID_SUBJECT_ID_MESSAGE = + "The supplied Subject ID is invalid. No user found. Please Create a User First." + + @OptIn(ExperimentalContracts::class) + private fun checkPresenceOfUser(user: User?) { + contract { + returns() implies (user != null) + } + checkPresence(user, "user_not_found") { + INVALID_SUBJECT_ID_MESSAGE + } + } + + fun nonNullProjectId(project: Project): Long = checkNotNull(project.id) { + "User id cannot be null" + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/FcmNotificationService.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/FcmNotificationService.kt new file mode 100644 index 000000000..6245bdd4c --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/FcmNotificationService.kt @@ -0,0 +1,501 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service + +import com.google.common.eventbus.EventBus +import jakarta.inject.Inject +import jakarta.inject.Named +import org.radarbase.appserver.jersey.dto.fcm.FcmNotificationDto +import org.radarbase.appserver.jersey.dto.fcm.FcmNotifications +import org.radarbase.appserver.jersey.enhancer.AppserverResourceEnhancer.Companion.NOTIFICATION_MAPPER +import org.radarbase.appserver.jersey.entity.Notification +import org.radarbase.appserver.jersey.entity.Project +import org.radarbase.appserver.jersey.entity.Task +import org.radarbase.appserver.jersey.entity.User +import org.radarbase.appserver.jersey.event.state.MessageState +import org.radarbase.appserver.jersey.event.state.dto.NotificationStateEventDto +import org.radarbase.appserver.jersey.exception.AlreadyExistsException +import org.radarbase.appserver.jersey.exception.InvalidNotificationDetailsException +import org.radarbase.appserver.jersey.exception.InvalidUserDetailsException +import org.radarbase.appserver.jersey.mapper.Mapper +import org.radarbase.appserver.jersey.repository.NotificationRepository +import org.radarbase.appserver.jersey.repository.ProjectRepository +import org.radarbase.appserver.jersey.repository.UserRepository +import org.radarbase.appserver.jersey.service.TaskService.Companion.nonNullUserId +import org.radarbase.appserver.jersey.service.questionnaire.schedule.MessageSchedulerService +import org.radarbase.appserver.jersey.utils.checkInvalidDetails +import org.radarbase.appserver.jersey.utils.checkPresence +import org.radarbase.appserver.jersey.utils.requireNotNullField +import java.time.Instant +import java.time.LocalDateTime +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +@Suppress("unused") +class FcmNotificationService @Inject constructor( + private val notificationRepository: NotificationRepository, + private val userRepository: UserRepository, + private val projectRepository: ProjectRepository, + private val schedulerService: MessageSchedulerService, + @param:Named(NOTIFICATION_MAPPER) private val notificationMapper: Mapper, + private val notificationStateEventPublisher: EventBus, +) : NotificationService { + + // TODO Add option to specify a scheduling provider (default will be fcm) + // TODO: Use strategy pattern for handling notifications for scheduling and adding to database + + suspend fun getAllNotifications(): FcmNotifications { + val notifications: List = notificationRepository.findAll() + return FcmNotifications( + notificationMapper.entitiesToDtos(notifications).toMutableList(), + ) + } + + suspend fun getNotificationById(id: Long): FcmNotificationDto { + val notification: Notification? = notificationRepository.find(id) + return notificationMapper.entityToDto(notification ?: Notification()) + } + + suspend fun getNotificationsBySubjectId(subjectId: String): FcmNotifications { + val user = this.userRepository.findBySubjectId(subjectId) + checkPresenceOfUser(user) + val notifications: List = notificationRepository.findByUserId(nonNullUserId(user)) + return FcmNotifications( + notificationMapper.entitiesToDtos(notifications).toMutableList(), + ) + } + + suspend fun getNotificationsByProjectIdAndSubjectId( + projectId: String, + subjectId: String, + ): FcmNotifications { + return subjectAndProjectExistElseThrow(subjectId, projectId).let { user -> + notificationRepository.findByUserId(nonNullUserId(user)) + }.let { notifications -> + FcmNotifications( + notificationMapper.entitiesToDtos(notifications).toMutableList(), + ) + } + } + + suspend fun getNotificationsByProjectId(projectId: String): FcmNotifications { + return checkPresence(projectRepository.findByProjectId(projectId), "project_not_found") { + "Project not found with projectId $projectId" + }.let { project -> + this.userRepository.findByProjectId(nonNullProjectId(project)) + }.let { users -> + hashSetOf().also { notifications -> + users.map { user -> + notificationRepository.findByUserId(nonNullUserId(user)) + }.forEach { userNotifications: List -> + notifications.addAll(userNotifications) + } + } + }.let { notifications -> + FcmNotifications( + notificationMapper.entitiesToDtos(notifications).toMutableList(), + ) + } + } + + suspend fun checkIfNotificationExists(notificationDto: FcmNotificationDto, subjectId: String): Boolean { + checkPresence(this.userRepository.findBySubjectId(subjectId), "user_not_found") { + INVALID_SUBJECT_ID_MESSAGE + }.let { user -> + val notification = Notification.NotificationBuilder( + notificationMapper.dtoToEntity(notificationDto), + ).user(user).build() + val notifications: List = this.notificationRepository.findByUserId(nonNullUserId(user)) + return notifications.contains(notification) + } + } + + // TODO : WIP + @Suppress("UNUSED_PARAMETER") + fun getFilteredNotifications( + type: String?, + delivered: Boolean?, + ttlSeconds: Int?, + startTime: LocalDateTime?, + endTime: LocalDateTime?, + limit: Int?, + ): FcmNotifications? = null + + suspend fun addNotification( + notificationDto: FcmNotificationDto, + subjectId: String, + projectId: String, + schedule: Boolean, + ): FcmNotificationDto { + val user = subjectAndProjectExistElseThrow(subjectId, projectId) + val notificationExists: Boolean = checkNotificationExists(notificationDto, subjectId, projectId) + + if (!notificationExists) { + val notificationSaved = addNotificationAndItsStateEvent(notificationDto, user) + if (schedule) { + this.schedulerService.schedule(notificationSaved) + } + return notificationMapper.entityToDto(notificationSaved) + } else { + throw AlreadyExistsException( + "notifications.already_exists", + "The Notification Already exists. Please Use update endpoint", + ) + } + } + + suspend fun addNotification( + notificationDto: FcmNotificationDto, + subjectId: String, + projectId: String, + ): FcmNotificationDto { + val notificationExists = checkNotificationExists(notificationDto, subjectId, projectId) + val user = subjectAndProjectExistElseThrow(subjectId, projectId) + + if (!notificationExists) { + val savedNotification = addNotificationAndItsStateEvent(notificationDto, user) + this.schedulerService.schedule(savedNotification) + return notificationMapper.entityToDto(savedNotification) + } else { + throw AlreadyExistsException( + "notifications.already_exists", + "The Notification Already exists. Please Use update endpoint", + ) + } + } + + suspend fun addNotificationAndItsStateEvent( + notificationDto: FcmNotificationDto, + user: User, + ): Notification { + val savedNotification = this.notificationRepository.add( + Notification.NotificationBuilder(notificationMapper.dtoToEntity(notificationDto)).user(user).build(), + ) + requireNotNullField(user.usermetrics, "User's user metrics").lastOpened = Instant.now() + this.userRepository.update(user) + addNotificationStateEvent( + savedNotification, + MessageState.ADDED, + requireNotNullField( + savedNotification.createdAt, + "Notification creation timestamp", + ).toInstant(), + ) + + return savedNotification + } + + suspend fun checkNotificationExists( + notificationDto: FcmNotificationDto, + subjectId: String, + projectId: String, + ): Boolean { + return subjectAndProjectExistElseThrow(subjectId, projectId).let { user -> + notificationRepository.existsByUserIdAndSourceIdAndScheduledTimeAndTitleAndBodyAndTypeAndTtlSeconds( + nonNullUserId(user), + requireNotNullField(notificationDto.sourceId, "Notification Source Id"), + requireNotNullField(notificationDto.scheduledTime, "Notification Scheduled time"), + requireNotNullField(notificationDto.title, "Notification Title"), + requireNotNullField(notificationDto.body, "Notification Body"), + requireNotNullField(notificationDto.type, "Notification Type"), + requireNotNullField(notificationDto.ttlSeconds, "Notification TTL seconds"), + ) + } + } + + private fun addNotificationStateEvent( + notification: Notification, + state: MessageState, + time: Instant, + ) { + val notificationStateEvent = NotificationStateEventDto(notification, state, null, time) + notificationStateEventPublisher.post(notificationStateEvent) + } + + suspend fun updateNotification( + notificationDto: FcmNotificationDto, + subjectId: String, + projectId: String, + ): FcmNotificationDto { + val notificationId = notificationDto.id + ?: throw InvalidNotificationDetailsException("ID must be supplied for updating the notification") + + val user = subjectAndProjectExistElseThrow(subjectId, projectId) + + val notification = checkPresence(this.notificationRepository.find(notificationId), "notification_not_found") { + "Notification does not exist. Please create one first" + } + + val newNotification = Notification.NotificationBuilder(notification).body(notificationDto.body) + .scheduledTime(notificationDto.scheduledTime).sourceId(notificationDto.sourceId) + .title(notificationDto.title).ttlSeconds(notificationDto.ttlSeconds).type(notificationDto.type).user(user) + .fcmMessageId(notificationDto.hashCode().toString()).build() + val notificationSaved = this.notificationRepository.update(newNotification) ?: throw IllegalStateException( + "Returned notification is null. Notification didn't updated successfully in the database.", + ) + + addNotificationStateEvent( + notificationSaved, + MessageState.UPDATED, + requireNotNullField( + notificationSaved.updatedAt, + "Notification update timestamp", + ).toInstant(), + ) + if (!notification.delivered) { + this.schedulerService.updateScheduled(notificationSaved) + } + return notificationMapper.entityToDto(notificationSaved) + } + + suspend fun scheduleAllUserNotifications(subjectId: String, projectId: String): FcmNotifications { + val user = subjectAndProjectExistElseThrow(subjectId, projectId) + val notifications: List = notificationRepository.findByUserId(nonNullUserId(user)) + this.schedulerService.scheduleMultiple(notifications) + return FcmNotifications( + notificationMapper.entitiesToDtos(notifications).toMutableList(), + ) + } + + suspend fun scheduleNotification(subjectId: String, projectId: String, notificationId: Long): FcmNotificationDto { + val user = subjectAndProjectExistElseThrow(subjectId, projectId) + val notification = notificationRepository.findByIdAndUserId(notificationId, nonNullUserId(user)) + checkPresence(notification, "notification_not_found") { + "The Notification with Id $notificationId does not exist in project $projectId for user $subjectId" + } + this.schedulerService.schedule(notification) + return notificationMapper.entityToDto(notification) + } + + suspend fun removeNotificationsForUser(projectId: String, subjectId: String) { + val userId = nonNullUserId(subjectAndProjectExistElseThrow(subjectId, projectId)) + val notifications: List = this.notificationRepository.findByUserId(userId) + this.schedulerService.deleteScheduledMultiple(notifications) + + this.notificationRepository.deleteByUserId(userId) + } + + suspend fun updateDeliveryStatus(fcmMessageId: String, isDelivered: Boolean) { + val notification = this.notificationRepository.findByFcmMessageId(fcmMessageId) + + checkInvalidDetails( + { notification == null }, + { + "Notification with the provided FCM message ID does not exist." + }, + ) + val newNotification = Notification.NotificationBuilder(notification).delivered(isDelivered).build() + this.notificationRepository.update(newNotification) + } + + // TODO: Investigate if notifications can be marked in the state CANCELLED when deleted. + suspend fun deleteNotificationByProjectIdAndSubjectIdAndNotificationId( + projectId: String, + subjectId: String, + id: Long, + ) { + val userId = nonNullUserId(subjectAndProjectExistElseThrow(subjectId, projectId)) + + if (this.notificationRepository.existsByIdAndUserId(id, userId)) { + this.schedulerService.deleteScheduled( + this.notificationRepository.findByIdAndUserId(id, userId)!!, + ) + this.notificationRepository.deleteByIdAndUserId(id, userId) + } else { + throw InvalidNotificationDetailsException( + "Notification with the provided ID does not exist.", + ) + } + } + + suspend fun removeNotificationsForUserUsingTaskId(projectId: String, subjectId: String, taskId: Long) { + val userId = nonNullUserId(subjectAndProjectExistElseThrow(subjectId, projectId)) + + val notifications: List = this.notificationRepository.findByUserIdAndTaskId(userId, taskId) + this.schedulerService.deleteScheduledMultiple(notifications) + + this.notificationRepository.deleteByUserIdAndTaskId(userId, taskId) + } + + suspend fun removeNotificationsForUserUsingFcmToken(fcmToken: String) { + val user = this.userRepository.findByFcmToken(fcmToken) + ?: throw InvalidUserDetailsException("The user with the given Fcm Token does not exist") + val userId = nonNullUserId(user) + this.schedulerService.deleteScheduledMultiple( + this.notificationRepository.findByUserId(userId), + ) + this.notificationRepository.deleteByUserId(userId) + } + + suspend fun deleteNotificationsByTaskId(task: Task) { + val taskId = task.id ?: return + if (notificationRepository.existsByTaskId(taskId)) { + val notifications: List = notificationRepository.findByTaskId(taskId) + schedulerService.deleteScheduledMultiple(notifications) + notificationRepository.deleteByTaskId(taskId) + } + } + + suspend fun addNotifications( + notificationDtos: FcmNotifications, + subjectId: String, + projectId: String, + schedule: Boolean, + ): FcmNotifications { + val savedNotifications = addNewNotifications(notificationDtos, subjectId, projectId) + savedNotifications.forEach { n: Notification -> + addNotificationStateEvent( + n, + MessageState.ADDED, + requireNotNullField(n.createdAt, "Notification creation timestamp").toInstant(), + ) + } + + if (schedule) { + this.schedulerService.scheduleMultiple(savedNotifications) + } + return FcmNotifications( + notificationMapper.entitiesToDtos(savedNotifications).toMutableList(), + ) + } + + suspend fun addNotifications(notifications: List?, user: User): List { + notifications ?: return listOf() + val newNotifications: List = notifications.filter { notification: Notification -> + !notificationRepository.existsByUserIdAndSourceIdAndScheduledTimeAndTitleAndBodyAndTypeAndTtlSeconds( + requireNotNullField(user.id, "User id"), + requireNotNullField(notification.sourceId, "Notification Source Id"), + requireNotNullField(notification.scheduledTime, "Notification Scheduled time"), + requireNotNullField(notification.title, "Notification Title"), + requireNotNullField(notification.body, "Notification Body"), + requireNotNullField(notification.type, "Notification Type"), + requireNotNullField(notification.ttlSeconds, "Notification TTL seconds"), + ) + } + + val savedNotifications: List = newNotifications.map { + this.notificationRepository.add(it) + } + savedNotifications.forEach { n: Notification? -> + addNotificationStateEvent( + n!!, + MessageState.ADDED, + requireNotNullField(n.createdAt, "Notification creation timestamp").toInstant(), + ) + } + this.schedulerService.scheduleMultiple(savedNotifications) + return savedNotifications + } + + suspend fun addNotifications( + notificationDtos: FcmNotifications, + subjectId: String, + projectId: String, + ): FcmNotifications { + val savedNotifications = addNewNotifications(notificationDtos, subjectId, projectId) + savedNotifications.forEach { n: Notification -> + addNotificationStateEvent( + n, + MessageState.ADDED, + requireNotNullField(n.createdAt, "Notification creation timestamp").toInstant(), + ) + } + + this.schedulerService.scheduleMultiple(savedNotifications) + return FcmNotifications( + notificationMapper.entitiesToDtos(savedNotifications).toMutableList(), + ) + } + + suspend fun addNewNotifications( + notificationDtos: FcmNotifications, + subjectId: String, + projectId: String, + ): List { + val newNotifications: List = subjectAndProjectExistElseThrow(subjectId, projectId).let { user -> + notificationRepository.findByUserId(nonNullUserId(user)).let { notifications -> + notificationDtos.notifications.map { dto: FcmNotificationDto -> + notificationMapper.dtoToEntity(dto) + }.map { notification -> + Notification.NotificationBuilder(notification).user(user).build() + }.filter { notification -> + !notifications.contains(notification) + } + } + } + + return newNotifications.map { + this.notificationRepository.add(it) + } + } + + suspend fun subjectAndProjectExistElseThrow(subjectId: String, projectId: String): User { + return checkPresence(this.projectRepository.findByProjectId(projectId), "project_not_found") { + "Project Id does not exist. Please create a project with the ID first" + }.let { project -> + checkPresence(this.userRepository.findBySubjectIdAndProjectId(subjectId, project.id!!), "user_not_found") { + INVALID_SUBJECT_ID_MESSAGE + } + } + } + + suspend fun getNotificationByProjectIdAndSubjectIdAndNotificationId( + projectId: String, + subjectId: String, + notificationId: Long, + ): Notification { + val user = subjectAndProjectExistElseThrow(subjectId, projectId) + val notification = notificationRepository.findByIdAndUserId(notificationId, nonNullUserId(user)) + ?: throw InvalidNotificationDetailsException( + "The Notification with Id $notificationId does not exist in project $projectId for user $subjectId", + ) + + return notification + } + + suspend fun getNotificationByMessageId(messageId: String): Notification { + val notification = this.notificationRepository.findByFcmMessageId(messageId) + checkInvalidDetails( + { + notification == null + }, + { + "The Notification with FCM Message Id $messageId does not exist." + }, + ) + return notification!! + } + + companion object { + private const val INVALID_SUBJECT_ID_MESSAGE = + "The supplied Subject ID is invalid. No user found. Please Create a User First." + } + + @OptIn(ExperimentalContracts::class) + private fun checkPresenceOfUser(user: User?) { + contract { + returns() implies (user != null) + } + checkPresence(user, "user_not_found") { + INVALID_SUBJECT_ID_MESSAGE + } + } + + fun nonNullProjectId(project: Project): Long = checkNotNull(project.id) { + "User id cannot be null" + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/NotificationService.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/NotificationService.kt new file mode 100644 index 000000000..44294c8b3 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/NotificationService.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service + +interface NotificationService diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/NotificationStateEventService.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/NotificationStateEventService.kt new file mode 100644 index 000000000..f7333c928 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/NotificationStateEventService.kt @@ -0,0 +1,166 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service + +import com.google.common.eventbus.EventBus +import jakarta.inject.Inject +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.json.Json +import org.glassfish.hk2.api.ServiceLocator +import org.radarbase.appserver.jersey.dto.NotificationStateEventDto +import org.radarbase.appserver.jersey.entity.Notification +import org.radarbase.appserver.jersey.entity.NotificationStateEvent +import org.radarbase.appserver.jersey.event.state.MessageState +import org.radarbase.appserver.jersey.repository.NotificationStateEventRepository +import java.io.IOException + +@Suppress("unused") +class NotificationStateEventService @Inject constructor( + private val notificationStateEventRepository: NotificationStateEventRepository, + private val notificationService: FcmNotificationService, + private val serviceLocator: ServiceLocator, +) { + private var notificationStateEventBus: EventBus? = null + get() { + if (field == null) { + return serviceLocator.getService(EventBus::class.java) + ?.also { field = it } + } + return field + } + + suspend fun addNotificationStateEvent(notificationStateEvent: NotificationStateEvent) { + if (notificationStateEvent.state == MessageState.CANCELLED) { + // the notification will be removed shortly + return + } + notificationStateEventRepository.add(notificationStateEvent) + } + + suspend fun getNotificationStateEvents( + projectId: String, + subjectId: String, + notificationId: Long, + ): List { + notificationService.getNotificationByProjectIdAndSubjectIdAndNotificationId( + projectId, + subjectId, + notificationId, + ) + val stateEvents: List = + notificationStateEventRepository.findByNotificationId(notificationId) + return stateEvents.map { notificationStateEvent: NotificationStateEvent -> + NotificationStateEventDto( + notificationStateEvent.id, + nonNullNotification(notificationStateEvent).id, + notificationStateEvent.state, + notificationStateEvent.time, + notificationStateEvent.associatedInfo, + ) + } + } + + suspend fun getNotificationStateEventsByNotificationId( + notificationId: Long, + ): List { + val stateEvents = notificationStateEventRepository.findByNotificationId(notificationId) + return stateEvents.map { notificationStateEvent: NotificationStateEvent -> + NotificationStateEventDto( + notificationStateEvent.id, + nonNullNotification(notificationStateEvent).id, + notificationStateEvent.state, + notificationStateEvent.time, + notificationStateEvent.associatedInfo, + ) + } + } + + suspend fun publishNotificationStateEventExternal( + projectId: String, + subjectId: String, + notificationId: Long, + notificationStateEventDto: NotificationStateEventDto, + ) { + checkState(notificationId, notificationStateEventDto.state) + val notification = notificationService.getNotificationByProjectIdAndSubjectIdAndNotificationId( + projectId, + subjectId, + notificationId, + ) + + var additionalInfo: Map? = null + if (!notificationStateEventDto.associatedInfo.isNullOrEmpty()) { + try { + additionalInfo = Json.decodeFromString( + MapSerializer(String.serializer(), String.serializer()), + notificationStateEventDto.associatedInfo!!, + ) + } catch (_: IOException) { + throw IllegalStateException( + "Cannot convert additionalInfo to Map. Please check its format.", + ) + } + } + + val messageState = requireNotNull(notificationStateEventDto.state) { + "Notification state event's state can't be null." + } + val messageTime = requireNotNull(notificationStateEventDto.time) { + "Notification state event's time can't be null." + } + + val stateEvent = org.radarbase.appserver.jersey.event.state.dto.NotificationStateEventDto( + notification, + messageState, + additionalInfo, + messageTime, + ) + notificationStateEventBus?.post(stateEvent) ?: log.error("Event bus is not initialized") + } + + @Throws(IllegalStateException::class) + private suspend fun checkState(notificationId: Long, state: MessageState?) { + if (EXTERNAL_EVENTS.contains(state)) { + if (notificationStateEventRepository.countByNotificationId(notificationId) + >= MAX_NUMBER_OF_STATES + ) { + throw IllegalStateException("The max limit of state changes($MAX_NUMBER_OF_STATES) has been reached. Cannot add new states.") + } + } else { + throw IllegalStateException("The state $state is not an external state and cannot be updated by this endpoint.") + } + } + + companion object { + private val log = org.slf4j.LoggerFactory.getLogger(NotificationStateEventService::class.java) + private const val MAX_NUMBER_OF_STATES = 20 + + private val EXTERNAL_EVENTS = setOf( + MessageState.DELIVERED, + MessageState.DISMISSED, + MessageState.OPENED, + MessageState.UNKNOWN, + MessageState.ERRORED, + ) + + private fun nonNullNotification(stateEvent: NotificationStateEvent): Notification = + checkNotNull(stateEvent.notification) { + "DataMessage in state event data can't be null" + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/ProjectService.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/ProjectService.kt new file mode 100644 index 000000000..05c36d4e9 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/ProjectService.kt @@ -0,0 +1,150 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service + +import jakarta.inject.Inject +import jakarta.inject.Named +import org.radarbase.appserver.jersey.dto.ProjectDto +import org.radarbase.appserver.jersey.dto.ProjectDtos +import org.radarbase.appserver.jersey.enhancer.AppserverResourceEnhancer.Companion.PROJECT_MAPPER +import org.radarbase.appserver.jersey.entity.Project +import org.radarbase.appserver.jersey.exception.AlreadyExistsException +import org.radarbase.appserver.jersey.mapper.Mapper +import org.radarbase.appserver.jersey.mapper.ProjectMapper +import org.radarbase.appserver.jersey.repository.ProjectRepository +import org.radarbase.appserver.jersey.utils.checkInvalidProjectDetails +import org.radarbase.appserver.jersey.utils.checkPresence +import org.slf4j.LoggerFactory + +/** + * Service class for managing projects. + * + * [Projects][Project] represent the same entities as in the [ManagementPortal](https://github.com/RADAR-base/ManagementPortal) and their names + * should align with the projects in the [ManagementPortal](https://github.com/RADAR-base/ManagementPortal). + * + * It uses [ProjectRepository] for persistence operations and [ProjectMapper] + * for converting between entity and DTO objects. + */ +@Suppress("unused") +class ProjectService @Inject constructor( + private val projectRepository: ProjectRepository, + @param:Named(PROJECT_MAPPER) private val projectMapper: Mapper, +) { + /** + * Retrieves all projects from the repository. + * + * @return [ProjectDtos] object containing a list of all projects as DTOs. + */ + suspend fun getAllProjects(): ProjectDtos { + return projectRepository.findAll().let { + projectMapper.entitiesToDtos(it).run { + ProjectDtos(this.toMutableList()) + } + } + } + + /** + * Retrieves a project by its unique identifier (ID). + * + * @param id the unique ID of the project + * @return the [ProjectDto] of the project + * @throws org.radarbase.jersey.exception.HttpNotFoundException if no project with the given ID exists + */ + suspend fun getProjectById(id: Long): ProjectDto { + val project: Project = checkPresence(projectRepository.find(id), "project_not_found") { + "Project with id $id not found" + } + + return projectMapper.entityToDto(project) + } + + /** + * Retrieves a project by its ManagementPortal project ID. + * + * @param projectId the unique project ID in the ManagementPortal + * @return the [ProjectDto] of the project + * @throws org.radarbase.jersey.exception.HttpNotFoundException if no project with the given project ID exists + */ + suspend fun getProjectByProjectId(projectId: String): ProjectDto { + val project = checkPresence( + projectRepository.findByProjectId(projectId), + "project_not_found", + ) { "Project with projectId $projectId not found" } + + return projectMapper.entityToDto(project) + } + + /** + * Creates a new project in the repository. + * + * @param projectDTO the [ProjectDto] containing details of the project to create + * @return the [ProjectDto] of the newly created project + * @throws org.radarbase.appserver.jersey.exception.InvalidProjectDetailsException if the input contains invalid data + * @throws AlreadyExistsException if the project is already present + */ + suspend fun addProject(projectDTO: ProjectDto): ProjectDto { + val projectId: String? = projectDTO.projectId + + checkInvalidProjectDetails( + projectDTO, + { projectDTO.id != null }, + { "'id' must not be supplied when creating a project, it is autogenerated" }, + ) + + checkInvalidProjectDetails( + projectDTO, + { projectId == null }, + { "At least 'project id' must be supplied" }, + ) + + if (projectRepository.existsByProjectId(projectId!!)) { + throw AlreadyExistsException( + "project_already_exists", + "The project with specified project-id (${projectDTO.projectId}) already exists. Use Update endpoint if need to update the project.", + ) + } + + return projectMapper.dtoToEntity(projectDTO).run { + projectRepository.add(this) + }.let { + projectMapper.entityToDto(it) + }.also { + logger.info("Project $projectId added to project") + } + } + + /** + * Updates an existing project in the repository. + * + * @param projectDto the [ProjectDto] containing updated details of the project + * @return the updated [ProjectDto] + * @throws org.radarbase.appserver.jersey.exception.InvalidProjectDetailsException if the input contains invalid project data + * @throws org.radarbase.jersey.exception.HttpNotFoundException if the project to update does not exist + */ + suspend fun updateProject(projectDto: ProjectDto): ProjectDto { + return projectRepository.updateEfficiently(projectDto).let { savedProject -> + println("Project updated successfully: $savedProject") + projectMapper.entityToDto(savedProject).also { + println("Sending: $it") + } + } + } + + companion object { + private val logger = LoggerFactory.getLogger(ProjectService::class.java) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/TaskService.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/TaskService.kt new file mode 100644 index 000000000..e5b9ffc80 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/TaskService.kt @@ -0,0 +1,179 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service + +import com.google.common.eventbus.EventBus +import jakarta.inject.Inject +import org.radarbase.appserver.jersey.dto.protocol.AssessmentType +import org.radarbase.appserver.jersey.entity.Task +import org.radarbase.appserver.jersey.entity.User +import org.radarbase.appserver.jersey.event.state.TaskState +import org.radarbase.appserver.jersey.event.state.dto.TaskStateEventDto +import org.radarbase.appserver.jersey.exception.AlreadyExistsException +import org.radarbase.appserver.jersey.repository.TaskRepository +import org.radarbase.appserver.jersey.repository.UserRepository +import org.radarbase.appserver.jersey.search.QuerySpecification +import org.radarbase.appserver.jersey.utils.checkPresence +import java.sql.Timestamp +import java.time.Instant + +@Suppress("unused") +class TaskService @Inject constructor( + private val taskRepository: TaskRepository, + private val userRepository: UserRepository, + private val eventPublisher: EventBus, +) { + suspend fun getAllTasks(): List { + return taskRepository.findAll() + } + + suspend fun getTaskById(id: Long): Task { + val task = taskRepository.find(id) + + return checkPresence(task, "task_not_found") { + "Task not found with id $id" + } + } + + suspend fun getTasksBySubjectId(subjectId: String): List { + val user = this.userRepository.findBySubjectId(subjectId) + checkPresence(user, "user_not_found") { + INVALID_SUBJECT_ID_MESSAGE + } + + return taskRepository.findByUserId(user.let(::nonNullUserId)) + } + + suspend fun getTasksBySubjectIdAndType(subjectId: String, type: AssessmentType): List { + val user = this.userRepository.findBySubjectId(subjectId) + checkPresence(user, "user_not_found") { + INVALID_SUBJECT_ID_MESSAGE + } + + return taskRepository.findByUserIdAndType(nonNullUserId(user), type) + } + + suspend fun getTasksByUser(user: User): List { + return taskRepository.findByUserId(nonNullUserId(user)) + } + + suspend fun getTasksBySpecification(spec: QuerySpecification): List { + return taskRepository.findAll(spec) + } + + suspend fun deleteTasksBySpecification(spec: QuerySpecification) { + val tasks = taskRepository.findAll(spec) + taskRepository.deleteAll(tasks) + } + + suspend fun deleteTasksByUserId(userId: Long) { + taskRepository.deleteByUserId(userId) + } + + suspend fun addTask(task: Task): Task { + val (user, taskName, taskTimestamp) = validateUserTaskNameAndTaskTimestamp(task) + val alreadyExists = this.taskRepository.existsByUserIdAndNameAndTimestamp( + nonNullUserId(user), + taskName, + taskTimestamp, + ) + + if (!alreadyExists) { + val saved = this.taskRepository.add(task) + val userMetrics = checkNotNull(user.usermetrics) { "User metrics cannot be null" } + userMetrics.lastOpened = Instant.now() + this.userRepository.update(user) + val taskCreationTimestamp = checkNotNull(saved.createdAt) { "Task creation timestamp cannot be null" } + addTaskStateEvent(saved, TaskState.ADDED, taskCreationTimestamp.toInstant()) + return saved + } else { + throw AlreadyExistsException( + "task_already_exists", + "The Task Already exists. Please Use update endpoint", + ) + } + } + + suspend fun addTasks(tasks: List, user: User): List { + val newTasks = tasks.filter { task -> + val taskName = checkNotNull(task.name) { "Task name cannot be null" } + val taskTimestamp = checkNotNull(task.timestamp) { "Task timestamp cannot be null" } + + !this.taskRepository.existsByUserIdAndNameAndTimestamp( + nonNullUserId(user), + taskName, + taskTimestamp, + ) + } + + val saved = newTasks.map { task -> + taskRepository.add(task) + } + saved.forEach { t -> + val taskCreationTimestamp = checkNotNull(t.createdAt) { "Task creation timestamp cannot be null" } + addTaskStateEvent(t, TaskState.ADDED, taskCreationTimestamp.toInstant()) + } + + return saved + } + + private fun addTaskStateEvent(t: Task?, @Suppress("SameParameterValue") state: TaskState, time: Instant) { + val taskStateEventDto = TaskStateEventDto(t, state, null, time) + eventPublisher.post(taskStateEventDto) + } + + suspend fun updateTaskStatus(oldTask: Task, state: TaskState): Task? { + val (user, taskName, taskTimestamp) = validateUserTaskNameAndTaskTimestamp(oldTask) + + val doesntExists = !this.taskRepository.existsByUserIdAndNameAndTimestamp( + nonNullUserId(user), + taskName, + taskTimestamp, + ) + + checkPresence(doesntExists, "task_not_found") { + "The Task ${oldTask.id} does not exist to set to state $state Please Use add endpoint" + } + + if (state == TaskState.COMPLETED) { + oldTask.completed = true + oldTask.timeCompleted = Timestamp.from(Instant.now()) + } + oldTask.status = state + return this.taskRepository.update(oldTask) + } + + companion object { + private const val INVALID_SUBJECT_ID_MESSAGE = + "The supplied Subject ID is invalid. No user found. Please Create a User First." + + fun nonNullUserId(user: User): Long = checkNotNull(user.id) { + "User id cannot be null" + } + + fun validateUserTaskNameAndTaskTimestamp(task: Task): Triple { + val user = task.user + checkPresence(user, "user_not_found") { + INVALID_SUBJECT_ID_MESSAGE + } + val taskName = checkNotNull(task.name) { "Task name cannot be null" } + val taskTimestamp = checkNotNull(task.timestamp) { "Task timestamp cannot be null" } + + return Triple(user, taskName, taskTimestamp) + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/TaskStateEventService.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/TaskStateEventService.kt new file mode 100644 index 000000000..3b81ba71e --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/TaskStateEventService.kt @@ -0,0 +1,158 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service + +import com.google.common.eventbus.EventBus +import jakarta.inject.Inject +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.json.Json +import org.glassfish.hk2.api.ServiceLocator +import org.radarbase.appserver.jersey.dto.TaskStateEventDto +import org.radarbase.appserver.jersey.entity.Task +import org.radarbase.appserver.jersey.entity.TaskStateEvent +import org.radarbase.appserver.jersey.event.state.TaskState +import org.radarbase.appserver.jersey.repository.TaskStateEventRepository +import org.slf4j.LoggerFactory +import java.io.IOException +import javax.naming.SizeLimitExceededException + +@Suppress("unused") +class TaskStateEventService @Inject constructor( + private val taskStateEventRepository: TaskStateEventRepository, + private val taskService: TaskService, + private val notificationService: FcmNotificationService, + private val serviceLocator: ServiceLocator, +) { + private var taskStateEventBus: EventBus? = null + get() { + if (field == null) { + return serviceLocator.getService(EventBus::class.java) + ?.also { field = it } + } + return field + } + + suspend fun addTaskStateEvent(taskStateEvent: TaskStateEvent) { + taskStateEventRepository.add(taskStateEvent) + val task = checkNotNull(taskStateEvent.task) { "Task in task state event can't be null" } + val state = checkNotNull(taskStateEvent.state) { "State in task state event can't be null" } + + taskService.updateTaskStatus(task, state) + if (taskStateEvent.state == TaskState.COMPLETED) { + notificationService.deleteNotificationsByTaskId(task) + } + } + + @Suppress("UNUSED_PARAMETER") + suspend fun getTaskStateEvents( + projectId: String?, + subjectId: String?, + taskId: Long, + ): List { + val task: Task = taskService.getTaskById(taskId) + val stateEvents: List = taskStateEventRepository.findByTaskId(taskId) + return stateEvents.map { ts -> + TaskStateEventDto( + id = ts.id, + taskId = task.id, + state = ts.state, + time = ts.time, + associatedInfo = ts.associatedInfo, + ) + } + } + + suspend fun getTaskStateEventsByTaskId( + taskId: Long, + ): List { + val stateEvents: List = taskStateEventRepository.findByTaskId(taskId) + return stateEvents.map { ts -> + TaskStateEventDto( + id = ts.id, + taskId = ts.task?.id, + state = ts.state, + time = ts.time, + associatedInfo = ts.associatedInfo, + ) + } + } + + @Suppress("UNUSED_PARAMETER") + @Throws(SizeLimitExceededException::class) + suspend fun publishNotificationStateEventExternal( + projectId: String, + subjectId: String, + taskId: Long, + taskStateEventDto: TaskStateEventDto, + ) { + val taskState = requireNotNull(taskStateEventDto.state) { "State is missing" } + checkState(taskId, taskState) + val task = taskService.getTaskById(taskId) + + val additionalInfo: Map? = if (!taskStateEventDto.associatedInfo.isNullOrEmpty()) { + try { + Json.decodeFromString( + MapSerializer(String.serializer(), String.serializer()), + taskStateEventDto.associatedInfo!!, + ) + } catch (exc: IOException) { + throw IllegalStateException( + "Cannot convert additionalInfo to Map. Please check its format.", + exc, + ) + } + } else { + null + } + + val stateEvent = org.radarbase.appserver.jersey.event.state.dto.TaskStateEventDto( + task, + taskStateEventDto.state, + additionalInfo, + taskStateEventDto.time, + ) + taskStateEventBus?.post(stateEvent) ?: logger.warn("EventBus is not initialized.") + } + + @Throws(SizeLimitExceededException::class, IllegalStateException::class) + private suspend fun checkState(taskId: Long, state: TaskState) { + if (state in EXTERNAL_EVENTS) { + if (taskStateEventRepository.countByTaskId(taskId) >= MAX_NUMBER_OF_STATES) { + throw SizeLimitExceededException( + "The max limit of state changes($MAX_NUMBER_OF_STATES) has been reached. Cannot add new states.", + ) + } + } else { + throw IllegalStateException( + "The state $state is not an external state and cannot be updated by this endpoint.", + ) + } + } + + companion object { + private val logger = LoggerFactory.getLogger(TaskStateEventService::class.java) + + private val EXTERNAL_EVENTS: Set = setOf( + TaskState.COMPLETED, + TaskState.UNKNOWN, + TaskState.ERRORED, + ) + + private const val MAX_NUMBER_OF_STATES = 20 + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/UserService.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/UserService.kt new file mode 100644 index 000000000..41ea4fe55 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/UserService.kt @@ -0,0 +1,332 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service + +import jakarta.inject.Inject +import jakarta.inject.Named +import org.radarbase.appserver.jersey.config.AppserverConfig +import org.radarbase.appserver.jersey.dto.fcm.FcmUserDto +import org.radarbase.appserver.jersey.dto.fcm.FcmUsers +import org.radarbase.appserver.jersey.enhancer.AppserverResourceEnhancer.Companion.USER_MAPPER +import org.radarbase.appserver.jersey.entity.Project +import org.radarbase.appserver.jersey.entity.User +import org.radarbase.appserver.jersey.exception.InvalidUserDetailsException +import org.radarbase.appserver.jersey.mapper.Mapper +import org.radarbase.appserver.jersey.mapper.UserMapper +import org.radarbase.appserver.jersey.repository.ProjectRepository +import org.radarbase.appserver.jersey.repository.UserRepository +import org.radarbase.appserver.jersey.service.questionnaire.schedule.QuestionnaireScheduleService +import org.radarbase.appserver.jersey.utils.checkInvalidDetails +import org.radarbase.appserver.jersey.utils.checkPresence +import org.radarbase.jersey.exception.HttpNotFoundException +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.time.Instant + +@Suppress("unused") +class UserService @Inject constructor( + @Named(USER_MAPPER) val userMapper: Mapper, + val userRepository: UserRepository, + val projectRepository: ProjectRepository, + val scheduleService: QuestionnaireScheduleService, + config: AppserverConfig, +) { + private val sendEmailNotifications: Boolean = config.email.enabled ?: false + + /** + * Retrieves all users associated with the projects. + * + * @return a list of [FcmUsers]. + */ + suspend fun getAllRadarUsers(): FcmUsers { + return FcmUsers(userMapper.entitiesToDtos(userRepository.findAll())) + } + + /** + * Retrieves a user by their id. + * + * @param id the unique identifier of the user to be retrieved + * @return the user details as [FcmUserDto] if the user is found + * @throws [HttpNotFoundException] if no user with the given id exists + */ + suspend fun getUserById(id: Long): FcmUserDto { + val user: User = checkPresence(userRepository.find(id), "user_not_found") { + "User with id $id not found" + } + return userMapper.entityToDto(user) + } + + /** + * Retrieves a user by subject ID. + * If a user with the specified subject ID cannot be found, a [HttpNotFoundException] is thrown. + * + * @param subjectId subject id of user. + * @return A data transfer object ([FcmUserDto]) representing the user information. + */ + suspend fun getUserBySubjectId(subjectId: String): FcmUserDto { + val user = + checkPresence(userRepository.findBySubjectId(subjectId), "user_not_found") { + "User with subjectId $subjectId not found" + } + return userMapper.entityToDto(user) + } + + /** + * Retrieves all users associated with the specified project ID. + * + * @param projectId project id of a project whose users should be retrieved + * @return [FcmUsers] that belong to the specified project + * @throws [HttpNotFoundException] if the project with the given ID does not exist + */ + suspend fun getUsersByProjectId(projectId: String): FcmUsers { + val project: Project = + checkPresence(projectRepository.findByProjectId(projectId), "project_not_found") { + "Project with id $projectId not found" + } + + val users: List = userRepository.findByProjectId( + requireNotNull(project.id) { "Project id for project ${project.projectId} is null when fetching users by projectId" }, + ) + + return FcmUsers(userMapper.entitiesToDtos(users)) + } + + /** + * Retrieves a user associated with a specific project and subject ID. + * + * This method first verifies the existence of the project identified by the given project ID. + * It then retrieves the user associated with the provided subject ID and project ID. If either + * the project or user is not found, an exception will be thrown. + * + * @param projectId The unique identifier of the project with which the user is associated. + * @param subjectId The unique identifier of the subject (user) to be retrieved. + * @return An instance of [FcmUserDto] representing the details of the user. + * @throws HttpNotFoundException If the specified project or user is not found in the database. + */ + suspend fun getUserByProjectIdAndSubjectId(projectId: String, subjectId: String): FcmUserDto { + val project: Project = + checkPresence( + projectRepository.findByProjectId(projectId), + "project_not_found", + ) { "Project with id $projectId not found" } + + return checkPresence( + userRepository.findBySubjectIdAndProjectId( + subjectId, + requireNotNull(project.id) { "Project id for project ${project.projectId} is null when fetching users by projectId" }, + ), + "user_not_found", + ) { "User with subjectId $subjectId not found" }.let { user -> + userMapper.entityToDto(user) + } + } + + /** + * Checks if a given FCM token exists in the database. If a user with the token exists but is associated + * with a different subject ID, the token is replaced with a new value. The updated user is then saved. + * + * @param userDto The user data transfer object containing the FCM token and subject ID to be verified and updated. + */ + suspend fun checkFcmTokenExistsAndReplace(userDto: FcmUserDto) { + userDto.fcmToken?.also { fcmToken -> + val user: User? = userRepository.findByFcmToken(fcmToken) + user?.apply { + if (!subjectId.equals(userDto.subjectId)) { + user.fcmToken = FCM_TOKEN_PREFIX + Instant.now().toString() + } + }?.also { + userRepository.update(it) + } + } + } + + /** + * Saves a user to an existing project in the system. If the user already exists in the project, + * an exception will be thrown. Additionally, generates a schedule for the newly created user. + * + * @param userDto The Data Transfer Object containing the details of the user to be saved, + * including the associated project ID and other user-specific data. + * @return Returns the saved user's Data Transfer Object with updated or assigned values after persistence. + * @throws HttpNotFoundException If the specified project is not found in the system. + * @throws InvalidUserDetailsException If a user with the same subject ID already exists in the specified project. + */ + suspend fun saveUserInProject(userDto: FcmUserDto): FcmUserDto { + // TODO: Future -- If any value is null get them using the MP api using others. (eg only subject + // id, then get project id and source ids from MP) + // TODO: Make the above pluggable so can use others or none. + logger.debug("Saving user: {}", userDto) + + checkInvalidDetails( + { userDto.id != null }, + { + "'id' must not be supplied when creating a project, it is autogenerated" + }, + ) + + val project: Project = checkPresence( + projectRepository.findByProjectId( + checkNotNull(userDto.projectId) { "Project ID must not be null" }, + ), + "project_not_found", + ) { + "Project with id ${userDto.projectId} not found. Please create the project first." + } + + val user: User? = userRepository.findBySubjectIdAndProjectId( + requireNotNull(userDto.subjectId) { "Subject id must not be null" }, + requireNotNull(project.id) { "Project id must not be null" }, + ) + + checkInvalidDetails( + { user != null }, + { + "User with subjectId ${userDto.subjectId} already exists with projectId ${userDto.projectId}. " + + "Please use update endpoint if you need to update user" + }, + ) + + val email: String? = userDto.email + if (sendEmailNotifications && email.isNullOrEmpty()) { + logger.warn( + "No email address was provided for new subject '{}'. The option to send notifications via email " + + "('email.enabled') will not work for this subject. Consider to provide a valid email " + + "address for subject", + userDto.subjectId, + ) + } + + val savedUser: User = userMapper.dtoToEntity(userDto).also { newUser -> + newUser.usermetrics?.let { + // maintain a bidirectional relationship + it.user = newUser + } + newUser.project = project + }.run { + userRepository.add(this) + } + + this.scheduleService.generateScheduleForUser(savedUser) + + return userMapper.entityToDto(savedUser) + } + + /** + * Updates an existing user within a specified project. The method ensures the project exists + * and validates that the user is already associated with the project, updating their details + * and regenerating their schedule if relevant changes are detected. + * + * @param userDto The data transfer object containing updated user information + * (e.g., FCM token, metrics, enrolment date, timezone, language, attributes). + * @return The updated user data transfer object reflecting the saved changes. + * @throws HttpNotFoundException If the project associated with the given projectID does not exist. + * @throws InvalidUserDetailsException If the user with the specified subject ID does not exist within the project. + */ + suspend fun updateUser(userDto: FcmUserDto): FcmUserDto { + val project: Project = checkPresence( + projectRepository.findByProjectId( + checkNotNull(userDto.projectId) { "Project ID must not be null" }, + ), + "project_not_found", + ) { + "Project with id ${userDto.projectId} not found. Please create the project first." + } + + val user: User? = userRepository.findBySubjectIdAndProjectId( + requireNotNull(userDto.subjectId) { "Subject id must be non-null" }, + requireNotNull(project.id) { "Project `id` must be non-null" }, + ) + + checkInvalidDetails( + user == null, + ) { + "The user with specified subject ID ${userDto.subjectId} does not exist in project ID " + "${userDto.projectId} Please use post endpoint to create the user." + } + + user.apply { + this.fcmToken = userDto.fcmToken + this.usermetrics = UserMapper.getValidUserMetrics(userDto) + this.enrolmentDate = userDto.enrolmentDate + this.timezone = userDto.timezone + this.language = userDto.language + this.attributes = userDto.attributes + // maintain a bidirectional relationship + this.usermetrics?.let { + it.user = this@apply + } + } + + val savedUser: User = userRepository.update(user) ?: throw HttpNotFoundException( + "user_not_found", + "User with id ${user.id} not found.", + ) + // Generate schedule for user + if (user.attributes != userDto.attributes || user.timezone != userDto.timezone || user.enrolmentDate != userDto.enrolmentDate || user.language != userDto.language) { + this.scheduleService.generateScheduleForUser(savedUser) + } + + return userMapper.entityToDto(savedUser) + } + + suspend fun updateLastDelivered(fcmToken: String, lastDelivered: Instant?) { + val user: User = checkPresence( + userRepository.findByFcmToken(fcmToken), + "user_not_found", + ) { + "User with the fcm-token $fcmToken doesn't exists" + } + user.usermetrics?.let { + it.lastDelivered = lastDelivered + } + userRepository.update(user) + } + + /** + * Deletes a user associated with a specific project and subject ID. + * This method verifies the existence of the project and the user in the + * specified project before deletion. + * + * @param projectId The unique identifier of the project. + * @param subjectId The unique identifier of the user (subject) within the project. + * @throws HttpNotFoundException If the project with the specified projectId doesn't exist. + * @throws InvalidUserDetailsException If the user with the specified subjectId does not exist in the project. + */ + suspend fun deleteUserByProjectIdAndSubjectId(projectId: String, subjectId: String) { + val project: Project = checkPresence(projectRepository.findByProjectId(projectId), "project_not_found") { + "Project with id $projectId not found" + } + + val user = userRepository.findBySubjectIdAndProjectId( + subjectId, + requireNotNull(project.id) { "Project id for project ${project.projectId} is null when fetching users by projectId" }, + ) + + checkInvalidDetails( + user == null, + ) { + "The user with specified subject ID $subjectId does not exist in project ID $projectId. Please specify a valid user for deleting." + } + + this.userRepository.delete(user) + } + + companion object { + private const val FCM_TOKEN_PREFIX = "unregistered_" + + private val logger: Logger = LoggerFactory.getLogger(UserService::class.java) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/github/GithubClient.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/github/GithubClient.kt new file mode 100644 index 000000000..34b6996d4 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/github/GithubClient.kt @@ -0,0 +1,169 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.github + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.defaultRequest +import io.ktor.client.request.header +import io.ktor.client.request.prepareGet +import io.ktor.http.HttpHeaders +import jakarta.inject.Inject +import jakarta.ws.rs.WebApplicationException +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.core.UriBuilder +import org.radarbase.appserver.jersey.config.AppserverConfig +import org.slf4j.LoggerFactory +import java.io.IOException +import java.net.MalformedURLException +import java.net.URI + +/** + * A singleton component responsible for interacting with the GitHub API. + * Provides utility methods for authorized and unauthorized access to GitHub content. + * The configuration for this client can be customized through application properties. + * + * @property httpTimeout The timeout in seconds for HTTP requests. + * @property githubToken The personal access token for GitHub API authentication. + */ +class GithubClient @Inject constructor( + config: AppserverConfig, +) { + private val githubToken: String? + private val httpTimeout: Long + private val maxContentLength: Long + + init { + config.github.client.let { githubClientConfig -> + githubToken = githubClientConfig.githubToken + httpTimeout = githubClientConfig.timeout + maxContentLength = githubClientConfig.maxContentLength + } + + logger.info("Github client initialized with settings (httpTimeout: {}, maxContentLength: {})", httpTimeout, maxContentLength) + } + + private val authorizationHeader: String + get() { + return if (githubToken.isNullOrEmpty()) { + "" + } else { + "Bearer " + githubToken.trim { it <= ' ' } + } + } + + /** + * An HTTP client configured to interact with the GitHub API using the CIO engine. + * This client sets default request headers, specifies request timeout, and + * enables automatic redirection following. + */ + private val client = HttpClient(CIO) { + defaultRequest { + header(HttpHeaders.Accept, GITHUB_API_ACCEPT_HEADER) + } + engine { + requestTimeout = httpTimeout * 1000L + } + followRedirects = true + } + + /** + * Retrieves the content from a specified GitHub URL. This method attempts to make an authenticated request + * if authentication is enabled. If unauthorized access occurs, it retries without authentication. + * + * @param url The URL of the GitHub resource to fetch content from. + * @param authenticated Indicates whether the request should include an authentication header (default is true). + * @return The content retrieved from the specified GitHub URL as a String. + * @throws WebApplicationException If the response indicates an error or the content size is too large. + */ + suspend fun getGithubContent(url: String, authenticated: Boolean = true): String { + val request = client.prepareGet(getValidGithubUri(url)) { + if (authenticated && authorizationHeader.isNotEmpty()) { + header(HttpHeaders.Authorization, authorizationHeader) + } + } + val response = request.execute() + if (response.status.value in 200..299) { + val contentLengthHeader = response.headers[HttpHeaders.ContentLength]?.toIntOrNull() + contentLengthHeader?.let { checkContentLength(it) } + + val contentStream: ByteArray = response.body() + checkContentLength(contentStream.size) + return String(contentStream) + } else if (response.status.value == 401 && authenticated) { + logger.warn("Unauthorized access to Github content from URL {}, retrying..", url) + return getGithubContent(url, false) + } else { + logger.error("Error getting Github content from URL {} : {}", url, response) + throw WebApplicationException( + "Github content could not be retrieved", + Response.status(response.status.value).build(), + ) + } + } + + suspend fun getGithubContent(url: String): String = getGithubContent(url, true) + + /** + * Validates the given content length against a predefined maximum content length. + * Throws a `ResponseStatusException` if the content length exceeds the allowed maximum. + * + * @param contentLength the length of the content to be validated + */ + private fun checkContentLength(contentLength: Int) { + if (contentLength > maxContentLength) { + throw WebApplicationException( + "Github content is too large", + Response.Status.BAD_REQUEST, + ) + } + } + + /** + * Validates and converts a given GitHub API URI into a standardized format. + * + * @param uri The GitHub API URI to validate and format. + * @return The validated and properly formatted GitHub API URI. + * @throws IOException If the provided URI is invalid or does not meet the required GitHub API standards. + */ + @Throws(IOException::class) + fun getValidGithubUri(uri: String): String { + val parsedUri = URI(uri) + if (parsedUri.host.equals(GITHUB_API_URI, ignoreCase = true) && + parsedUri.scheme.equals("https", ignoreCase = true) && + (parsedUri.port == -1 || parsedUri.port == 443) + ) { + return UriBuilder.newInstance() + .scheme("https") + .host(parsedUri.host) + .path(parsedUri.path) + .replaceQuery(parsedUri.query) + .build() + .toString() + } else { + throw MalformedURLException("Invalid Github URL: $uri") + } + } + + companion object { + private val logger = LoggerFactory.getLogger(GithubClient::class.java) + + private const val GITHUB_API_URI = "api.github.com" + private const val GITHUB_API_ACCEPT_HEADER = "application/vnd.github.v3+json" + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/github/GithubService.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/github/GithubService.kt new file mode 100644 index 000000000..b3afa32d7 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/github/GithubService.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.github + +import io.ktor.utils.io.errors.IOException +import jakarta.inject.Inject +import jakarta.ws.rs.WebApplicationException +import org.radarbase.appserver.jersey.config.AppserverConfig +import org.radarbase.appserver.jersey.utils.cache.CachedFunction +import java.time.Duration + +/** + * A service component responsible for wrapping GitHub content-fetching functionality + * with caching capabilities. This service allows efficient fetching of GitHub content, + * reducing redundant calls to the GitHub API by using a local cache for frequently accessed content. + * + * Configuration for caching behavior can be customized via application properties. + * + * @property githubClient The [GithubClient] used for API interaction. + * @property cacheTime The duration, in seconds, for which the content should be cached. + * @property retryTime The duration, in seconds, for which retries are attempted in case of errors. + * @property maxSize The maximum size of the cache. + */ +class GithubService @Inject constructor( + private val githubClient: GithubClient, + config: AppserverConfig, +) { + private val cacheTime: Long + private val retryTime: Long + private val maxSize: Int + + init { + config.github.cache.let { githubCacheConfig -> + cacheTime = checkNotNull(githubCacheConfig.cacheDuration) { "Github cache duration cannot be null in config" } + retryTime = checkNotNull(githubCacheConfig.retryDuration) { "Github retry duration cannot be null in config" } + maxSize = githubCacheConfig.maxCacheSize + } + } + + private val cachedGithubContent: CachedFunction = CachedFunction( + githubClient::getGithubContent, + Duration.ofSeconds(cacheTime), + Duration.ofSeconds(retryTime), + maxSize, + ) + + /** + * Retrieves the content from a given GitHub URL. The result is cached to optimize repeated calls. + * + * @param url The URL of the GitHub resource to retrieve the content from. + * @return The content retrieved from the specified GitHub URL as a string. + * @throws IOException If an I/O error occurs during the request. + * @throws WebApplicationException If an HTTP response indicates a failure. + * @throws IllegalStateException If an unknown exception occurs during execution. + */ + @Throws(IOException::class) + suspend fun getGithubContent(url: String): String = try { + this.cachedGithubContent.applyWithException(url) + } catch (ex: IOException) { + throw ex + } catch (ex: WebApplicationException) { + throw ex + } catch (ex: Exception) { + throw IllegalStateException("Unknown exception $ex", ex) + } + + /** + * Fetches the content of the given GitHub URL directly without the need of the caching mechanism. + * + * @param url The URL of the GitHub resource to be fetched. + * @return The content of the GitHub resource as a string. + * @throws IOException If an error occurs during the request. + */ + @Throws(IOException::class) + suspend fun getGithubContentWithoutCache(url: String): String { + return githubClient.getGithubContent(url) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/github/protocol/ProtocolFetcherStrategy.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/github/protocol/ProtocolFetcherStrategy.kt new file mode 100644 index 000000000..60e168330 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/github/protocol/ProtocolFetcherStrategy.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.github.protocol + +import io.ktor.utils.io.errors.IOException +import org.radarbase.appserver.jersey.dto.protocol.Protocol + +/** + * Strategy interface for fetching protocol-related data. Implementations of this + * interface provide mechanisms to retrieve protocol configurations either globally + * or per project to be used within the application. + */ +interface ProtocolFetcherStrategy { + /** + * Fetches all available protocols mapped to their respective user-id's. + * + * @return A map where the keys are user identifiers (String) and the values are their + * corresponding Protocol objects. + * @throws IOException If an I/O error occurs while retrieving the protocols. + */ + @Throws(IOException::class) + suspend fun fetchProtocols(): Map + + /** + * Fetches protocols associated with each project. + * + * @return A map where the keys represent project identifiers as strings and the values + * represent the corresponding protocols. + * @throws IOException if there is an issue during the retrieval of the protocols. + */ + @Throws(IOException::class) + suspend fun fetchProtocolsPerProject(): Map +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/github/protocol/ProtocolGenerator.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/github/protocol/ProtocolGenerator.kt new file mode 100644 index 000000000..f35d13c97 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/github/protocol/ProtocolGenerator.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.github.protocol + +import org.radarbase.appserver.jersey.dto.protocol.Protocol +import java.io.IOException + +/** + * Interface responsible for generating and retrieving protocol configurations. + */ +interface ProtocolGenerator { + /** + * Retrieves all available protocols mapped by their identifiers. + * + * @return A map where the key is a protocol identifier (as a String) and the value is + * the corresponding Protocol object. + */ + suspend fun retrieveAllProtocols(): Map + + /** + * Retrieves the protocol associated with a specific project ID. + * + * @param projectId the identifier of the project whose associated protocol is to be retrieved. + * @return the protocol corresponding to the given project ID. + * @throws IOException if an error occurs while retrieving the protocol. + */ + @Throws(IOException::class) + suspend fun getProtocol(projectId: String): Protocol + + /** + * Retrieves the protocol associated with a given subject. + * + * @param subjectId The identifier of the subject for which the protocol is to be retrieved. + * @return The protocol corresponding to the specified subject. + */ + suspend fun getProtocolForSubject(subjectId: String): Protocol? +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/github/protocol/impl/DefaultProtocolGenerator.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/github/protocol/impl/DefaultProtocolGenerator.kt new file mode 100644 index 000000000..6c6a0098d --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/github/protocol/impl/DefaultProtocolGenerator.kt @@ -0,0 +1,175 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.github.protocol.impl + +import io.ktor.utils.io.errors.IOException +import jakarta.inject.Inject +import org.radarbase.appserver.jersey.dto.protocol.Protocol +import org.radarbase.appserver.jersey.service.github.protocol.ProtocolFetcherStrategy +import org.radarbase.appserver.jersey.service.github.protocol.ProtocolGenerator +import org.radarbase.appserver.jersey.utils.cache.CachedMap +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.time.Duration + +/** + * Default implementation of the ProtocolGenerator interface, responsible for + * retrieving and caching protocol configurations. This class interacts with a + * ProtocolFetcherStrategy to fetch protocol-related data and uses caching + * techniques to minimize redundant network calls. + * + * @param protocolFetcherStrategy A [ProtocolFetcherStrategy][org.radarbase.appserver.jersey.service.github.protocol.ProtocolFetcherStrategy] interface implementation used for + * retrieving protocol configurations. + */ +class DefaultProtocolGenerator @Inject constructor( + private val protocolFetcherStrategy: ProtocolFetcherStrategy, +) : ProtocolGenerator { + /** + * Caches a mapping of user identifiers to their corresponding protocol configurations. + */ + private var cachedProtocolMap: CachedMap = CachedMap( + protocolFetcherStrategy::fetchProtocols, + CACHE_INVALIDATE_DEFAULT, + CACHE_RETRY_DEFAULT, + ) + + /** + * Caches a mapping of project identifiers to their corresponding protocol configurations. + */ + @Transient + private var cachedProjectProtocolMap: CachedMap = CachedMap( + protocolFetcherStrategy::fetchProtocolsPerProject, + CACHE_INVALIDATE_DEFAULT, + CACHE_RETRY_DEFAULT, + ) + + /** + * Retrieves all available protocols from the cache or previously fetched data. + * If an error occurs during protocol retrieval, cached values are used as a fallback. + * + * @return A map where the key is a protocol identifier (as a String) and the value is + * the corresponding Protocol object. + */ + override suspend fun retrieveAllProtocols(): Map { + return try { + cachedProtocolMap.get() + } catch (ex: IOException) { + logger.warn("Cannot retrieve Protocols, using cached values if available.", ex) + cachedProtocolMap.getCachedMap() + } + } + + /** + * Retrieves the protocol associated with the specified project ID. + * + * @param projectId the identifier of the project whose associated protocol is to be retrieved. + * @return the protocol corresponding to the given project ID. + * @throws IOException if an error occurs while retrieving the protocol, or if the protocol cannot be fetched. + */ + @Throws(IOException::class) + override suspend fun getProtocol(projectId: String): Protocol { + try { + return cachedProjectProtocolMap.getByKey(projectId)!! + } catch (ex: IOException) { + logger.warn( + "Cannot retrieve Protocols for project {} : {}, Using cached values.", + projectId, + ex.toString(), + ) + return cachedProjectProtocolMap.getCachedMap()[projectId]!! + } catch (ex: Exception) { + logger.warn( + "Exception while fetching protocols for project {}", + projectId, + ex, + ) + throw IOException("Exception while fetching protocols for project $projectId", ex) + } + } + + /** + * Retrieves the protocol associated with the given subject ID. + * + * @param subjectId The identifier of the subject for which the protocol is being retrieved. + * @return The corresponding Protocol object for the specified subject ID. + * @throws IOException If an error occurs while retrieving protocols or handling cached values. + */ + @Throws(IOException::class) + private suspend fun forceGetProtocolForSubject(subjectId: String): Protocol? { + try { + return cachedProtocolMap.get(true)[subjectId] + } catch (ex: IOException) { + logger.warn("Cannot retrieve Protocols, using cached values if available.", ex) + return cachedProtocolMap.getCachedMap()[subjectId] + } catch (ex: Exception) { + logger.warn( + "Exception while fetching protocols for subject {}", + subjectId, + ex, + ) + throw IOException("Exception while fetching protocols for subject $subjectId", ex) + } + } + + /** + * Retrieves the protocol associated with the specified subject. If the protocol is + * not available in the cache, it attempts to fetch it using a fallback strategy. + * + * @param subjectId The identifier of the subject for which the protocol is being retrieved. + * @return The protocol corresponding to the given subject. + * @throws IOException If an error occurs during protocol retrieval and no cached value is available. + */ + override suspend fun getProtocolForSubject(subjectId: String): Protocol? { + try { + val protocol = cachedProtocolMap.getByKey(subjectId) ?: return forceGetProtocolForSubject(subjectId) + return protocol + } catch (ex: IOException) { + logger.warn( + "Cannot retrieve Protocols for subject {} : {}, Using cached values.", + subjectId, + ex.toString(), + ) + return cachedProtocolMap.getCachedMap()[subjectId]!! + } catch (_: NoSuchElementException) { + logger.warn("Subject does not exist in map. Fetching..") + return forceGetProtocolForSubject(subjectId) + } catch (ex: Exception) { + logger.warn( + "Exception while fetching protocols for subject {}", + subjectId, + ex, + ) + throw IOException("Exception while fetching protocols for subject $subjectId. ${ex.message}", ex) + } + } + + companion object { + private val logger: Logger = LoggerFactory.getLogger(DefaultProtocolGenerator::class.java) + + /** + * Represents the default duration after which cached protocol data is considered invalid + * and must be refreshed. + */ + private val CACHE_INVALIDATE_DEFAULT: Duration = Duration.ofHours(1) + + /** + * The default duration to retry caching operations. + * Used when no specific retry interval is configured. + */ + private val CACHE_RETRY_DEFAULT: Duration = Duration.ofHours(2) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/github/protocol/impl/GithubProtocolFetcherStrategy.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/github/protocol/impl/GithubProtocolFetcherStrategy.kt new file mode 100644 index 000000000..3a3631145 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/github/protocol/impl/GithubProtocolFetcherStrategy.kt @@ -0,0 +1,306 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.github.protocol.impl + +import io.ktor.utils.io.errors.IOException +import jakarta.inject.Inject +import jakarta.ws.rs.WebApplicationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import org.radarbase.appserver.jersey.config.AppserverConfig +import org.radarbase.appserver.jersey.dto.protocol.GithubContent +import org.radarbase.appserver.jersey.dto.protocol.Protocol +import org.radarbase.appserver.jersey.dto.protocol.ProtocolCacheEntry +import org.radarbase.appserver.jersey.entity.User +import org.radarbase.appserver.jersey.repository.ProjectRepository +import org.radarbase.appserver.jersey.repository.UserRepository +import org.radarbase.appserver.jersey.service.github.GithubService +import org.radarbase.appserver.jersey.service.github.protocol.ProtocolFetcherStrategy +import org.radarbase.appserver.jersey.utils.cache.CachedMap +import org.radarbase.appserver.jersey.utils.mapParallel +import org.radarbase.appserver.jersey.utils.requireNotNullField +import org.radarbase.appserver.jersey.utils.withReentrantLock +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.net.URI +import java.time.Duration + +/** + * A strategy for fetching protocols stored in a GitHub repository. This implementation interacts + * with a GitHub repository to retrieve protocol files for users and projects. It includes caching + * and utilities for constructing file paths and parsing data from GitHub responses. + * + * + * @property protocolRepo The configured GitHub repository path where protocols are stored. + * @property protocolFileName The name of the protocol file used to identify relevant files in the repository. + * @property protocolBranch The branch of the repository from which protocols should be retrieved. + * @property userRepository Repository for accessing User data from the database. + * @property projectRepository Repository for accessing Project data from the database. + * @property githubService A service for interacting with the GitHub API. + */ +class GithubProtocolFetcherStrategy @Inject constructor( + config: AppserverConfig, + private val userRepository: UserRepository, + private val projectRepository: ProjectRepository, + private val githubService: GithubService, +) : ProtocolFetcherStrategy { + + private val protocolRepo: String + private val protocolFileName: String + private val protocolBranch: String + + init { + config.protocol.also { questionnaireProtocolConfig -> + protocolRepo = checkNotNull(questionnaireProtocolConfig.githubProtocolRepo) { + "Github protocol repository is null in protocol fetcher" + } + protocolFileName = checkNotNull(questionnaireProtocolConfig.protocolFileName) { + "Github protocol file name is null in protocol fetcher" + } + protocolBranch = checkNotNull(questionnaireProtocolConfig.githubBranch) { + "Github repository branch is null in protocol fetcher" + } + } + } + + private val fetchLock = Mutex() + + @OptIn(ExperimentalSerializationApi::class) + private val localJson = Json { + ignoreUnknownKeys = true + isLenient = true + explicitNulls = false + encodeDefaults = true + prettyPrint = false + } + + /** + * A [CachedMap] that associates each project with its corresponding GitHub protocol URI. + */ + @Transient + private val projectProtocolUriMap: CachedMap = CachedMap( + ::retrieveProtocolDirectories, + Duration.ofHours(3), + Duration.ofMinutes(4), + ) + + /** + * Fetches protocol configurations for all users stored in the user repository and associates each + * user with their respective protocol based on a set of predefined paths. + * + * @return A map where the keys are user IDs and the values are the corresponding protocol objects. + */ + override suspend fun fetchProtocols(): Map = fetchLock.withReentrantLock { + val users: List = userRepository.findAll() + val protocolPaths: Set = getProtocolPaths() + + users.mapParallel(Dispatchers.Default) { + val project = requireNotNullField(it.project, "User's project") + fetchProtocolForSingleUser(it, requireNotNullField(project.projectId, "Project Id"), protocolPaths) + }.filter { it.protocol != null }.associate { it.id to it.protocol!! }.also { + logger.debug("Fetched Protocols from Github") + } + } + + /** + * Fetches a protocol for a single user based on the user's attributes, a specific project ID, + * and a set of protocol paths. + * + * @param user The user whose protocol is to be fetched. The user's attributes are used to find a matching protocol. + * @param projectId The ID of the project associated with the requested protocol. + * @param protocolPaths A set of potential protocol paths to be filtered and matched based on the user's attributes and project ID. + * @return A [ProtocolCacheEntry] containing the user's ID and the fetched protocol, or null if no matching protocol is found. + */ + private suspend fun fetchProtocolForSingleUser( + user: User, + projectId: String, + protocolPaths: Set, + ): ProtocolCacheEntry { + val attributes: Map = user.attributes ?: emptyMap() + val subjectId: String = requireNotNullField(user.subjectId, "User subject ID") + + val attributeMap: Map = protocolPaths.filter { + it.contains(projectId) + }.map { + convertPathToAttributeMap(it, projectId).filter { entry -> + attributes[entry.key] == entry.value + } + }.maxByOrNull { it.size } ?: emptyMap() + + return try { + val attributePath = convertAttributeMapToPath(attributeMap, projectId) + projectProtocolUriMap.get()[attributePath]?.let { + ProtocolCacheEntry(subjectId, getProtocolFromUrl(it)) + } ?: ProtocolCacheEntry(subjectId, null) + } catch (_: Exception) { + ProtocolCacheEntry(subjectId, null) + } + } + + /** + * Fetches and maps protocols for each project based on their identifiers. + * + * @return a map where the key is the project ID (String) and the value is + * the associated Protocol object. If a project does not have a corresponding + * protocol, it will not be included in the map. + */ + override suspend fun fetchProtocolsPerProject(): Map { + val protocolPaths = getProtocolPaths() + if (protocolPaths.isEmpty()) return emptyMap() + return projectRepository.findAll().mapParallel(Dispatchers.Default) { project -> + val projectId = requireNotNullField(project.projectId, "Project Id") + val protocol = protocolPaths.lastOrNull { it.contains(projectId) }?.let { path -> + try { + val uri = projectProtocolUriMap.get()[path] ?: return@let null + getProtocolFromUrl(uri) + } catch (e: Exception) { +// null + throw e + } + } + ProtocolCacheEntry(projectId, protocol) + }.filter { it.protocol != null }.associate { it.id to it.protocol!! } + .also { logger.debug("Refreshed Protocols from Github") } + } + + /** + * Retrieves a set of protocol paths available from the underlying project protocol URI map. + * If an error occurs while fetching the map, a cached version is used instead. + * + * @return a set of strings representing protocol paths + */ + private suspend fun getProtocolPaths(): Set { + val uriMap: Map = try { + projectProtocolUriMap.get() + } catch (_: IOException) { + // Failed to get the Uri Map. Using the cached values + projectProtocolUriMap.getCachedMap() + } + return uriMap.keys + } + + /** + * Converts a path string into a map of attributes based on key-value pairs extracted from the path. + * Keys and values are determined by splitting the path and pairing the elements. + * The `projectId` and `protocolFileName` are excluded from the results. + * + * @param path The input path strings to be converted into a map of attributes. + * @param projectId The project identifier used to remove matching segments from the path. + * @return A map containing key-value pairs derived from the path. + */ + fun convertPathToAttributeMap(path: String, projectId: String): Map { + val pathMap = mutableMapOf() + val pathParts: List = path.split("/") + + return pathMap.apply { + pathParts.filter { it != projectId && it != protocolFileName }.chunked(2) { attr -> + if (attr.size == 2) this[attr[0]] = attr[1] + } + } + } + + /** + * Converts a map of attributes into a path string by appending each key-value pair, prefixed + * by the given project ID and ending with a specified protocol file name. + * + * @param attributeMap A map containing attribute keys and their corresponding values to be converted into a path. + * @param projectId The ID of the project with which the generated path is associated. + * @return A string representing the constructed path using the given map and project ID. + */ + fun convertAttributeMapToPath(attributeMap: Map, projectId: String): String = buildString { + append(projectId).append("/") + attributeMap.forEach { (k, v) -> + append(k).append("/").append(v).append("/") + } + append(protocolFileName) + } + + /** + * Retrieves a map containing protocol file paths as keys and their corresponding URLs as values + * from a specified GitHub repository. + * + * @throws IOException if there is an issue in fetching or processing the content from GitHub. + * @return a map where keys are the paths of protocol files and values are their respective URIs. + */ + @Throws(IOException::class) + private suspend fun retrieveProtocolDirectories(): Map = fetchLock.withReentrantLock { + val protocolUriMap = mutableMapOf() + + try { + val branchJson = githubService.getGithubContentWithoutCache( + "$GITHUB_API_URI$protocolRepo/branches/$protocolBranch", + ) + + val branchElement = Json.parseToJsonElement(branchJson).jsonObject + val treeSha = branchElement["commit"] + ?.jsonObject?.get("commit") + ?.jsonObject?.get("tree") + ?.jsonObject?.get("sha") + ?.jsonPrimitive?.content + ?: throw IOException("Missing tree sha in branch JSON") + + val treeJson = githubService.getGithubContent( + "$GITHUB_API_URI$protocolRepo/git/trees/$treeSha?recursive=true", + ) + val treeElement = Json.parseToJsonElement(treeJson).jsonObject + + val treeArray = treeElement["tree"]?.jsonArray + ?: throw IOException("Missing tree array in tree JSON") + + for (node in treeArray) { + val obj = node.jsonObject + val path = obj["path"]?.jsonPrimitive?.content ?: continue + if (path.contains(this.protocolFileName)) { + val url = obj["url"]?.jsonPrimitive?.content ?: continue + protocolUriMap[path] = URI.create(url) + } + } + } catch (e: WebApplicationException) { + throw IOException("Failed to retrieve protocols URIs from github", e) + } catch (e: Exception) { + throw IOException("Exception when retrieving protocol uri map info", e) + } + protocolUriMap + } + + /** + * Fetches a `Protocol` object from a specified URI. The method retrieves the content from the given URI, + * parses it into a `GithubContent` object, and extracts the `Protocol` data from it. + * + * @param uri The URI from which the protocol data will be fetched. + * @return The [Protocol] object retrieved and deserialized from the URI content. + * @throws IOException If an error occurs while fetching or parsing the content from the URI. + */ + @Throws(IOException::class) + private suspend fun getProtocolFromUrl(uri: URI): Protocol { + val contentString = githubService.getGithubContent(uri.toString()) + val protocol = localJson.decodeFromString(contentString).content + ?: throw IOException("Protocol content is null") + return localJson.decodeFromString(protocol) + } + + companion object { + private val logger: Logger = LoggerFactory.getLogger(GithubProtocolFetcherStrategy::class.java) + + private const val GITHUB_API_URI = "https://api.github.com/repos/" + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/ProtocolHandler.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/ProtocolHandler.kt new file mode 100644 index 000000000..26d9791bc --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/ProtocolHandler.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.protocol.handler + +import org.radarbase.appserver.jersey.dto.protocol.Assessment +import org.radarbase.appserver.jersey.dto.questionnaire.AssessmentSchedule +import org.radarbase.appserver.jersey.entity.User + +/** + * Interface for handling protocol-related tasks. Implementations are responsible for processing + * and updating an [AssessmentSchedule] based on the associated [protocol][Assessment] details. + */ +interface ProtocolHandler { + /** + * Processes the given assessment schedule and updates it with appropriate data based on the input parameters. + * + * @param assessmentSchedule The assessment schedule that will be updated. + * @param assessment The assessment containing protocol details and metadata used for processing. + * @param user The user whose information, such as timezone and enrolment date, is utilized in the processing. + * @return The updated [AssessmentSchedule] containing the results of the handling process. + */ + suspend fun handle(assessmentSchedule: AssessmentSchedule, assessment: Assessment, user: User): AssessmentSchedule +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/CompletedQuestionnaireHandlerFactory.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/CompletedQuestionnaireHandlerFactory.kt new file mode 100644 index 000000000..f34ffc2ce --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/CompletedQuestionnaireHandlerFactory.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.protocol.handler.factory + +import org.radarbase.appserver.jersey.entity.Task +import org.radarbase.appserver.jersey.service.protocol.handler.ProtocolHandler +import org.radarbase.appserver.jersey.service.protocol.handler.impl.CompletedQuestionnaireHandler + +object CompletedQuestionnaireHandlerFactory { + fun getCompletedQuestionnaireHandler(prevTasks: List, prevTimezone: String): ProtocolHandler { + return CompletedQuestionnaireHandler(prevTasks, prevTimezone) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/NotificationHandlerFactory.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/NotificationHandlerFactory.kt new file mode 100644 index 000000000..d8b92efd4 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/NotificationHandlerFactory.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.protocol.handler.factory + +import org.radarbase.appserver.jersey.dto.protocol.NotificationProtocol +import org.radarbase.appserver.jersey.dto.protocol.NotificationProtocolMode +import org.radarbase.appserver.jersey.service.protocol.handler.ProtocolHandler +import org.radarbase.appserver.jersey.service.protocol.handler.impl.DisabledNotificationHandler +import org.radarbase.appserver.jersey.service.protocol.handler.impl.SimpleNotificationHandler +import java.io.IOException + +object NotificationHandlerFactory { + @Throws(IOException::class) + fun getNotificationHandler(protocol: NotificationProtocol): ProtocolHandler { + return when (protocol.mode) { + NotificationProtocolMode.DISABLED -> DisabledNotificationHandler() + NotificationProtocolMode.COMBINED -> throw IOException("Combined Notification Protocol Mode is not supported yet") + else -> SimpleNotificationHandler() + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/ProtocolHandlerFactory.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/ProtocolHandlerFactory.kt new file mode 100644 index 000000000..f179dd3e4 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/ProtocolHandlerFactory.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.protocol.handler.factory + +import org.radarbase.appserver.jersey.service.protocol.handler.ProtocolHandler +import org.radarbase.appserver.jersey.service.protocol.handler.impl.ClinicalProtocolHandler +import org.radarbase.appserver.jersey.service.protocol.handler.impl.SimpleProtocolHandler + +object ProtocolHandlerFactory { + fun getProtocolHandler(protocolType: ProtocolHandlerType): ProtocolHandler { + return when (protocolType) { + ProtocolHandlerType.CLINICAL -> ClinicalProtocolHandler() + else -> SimpleProtocolHandler() + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/ProtocolHandlerType.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/ProtocolHandlerType.kt new file mode 100644 index 000000000..7c6fca7ca --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/ProtocolHandlerType.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.protocol.handler.factory + +enum class ProtocolHandlerType { + SIMPLE, + CLINICAL, + OTHER, +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/ReminderHandlerFactory.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/ReminderHandlerFactory.kt new file mode 100644 index 000000000..34125a609 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/ReminderHandlerFactory.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.protocol.handler.factory + +import org.radarbase.appserver.jersey.service.protocol.handler.ProtocolHandler +import org.radarbase.appserver.jersey.service.protocol.handler.impl.SimpleReminderHandler + +object ReminderHandlerFactory { + val reminderHandler: ProtocolHandler + get() = SimpleReminderHandler() +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/RepeatProtocolHandlerFactory.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/RepeatProtocolHandlerFactory.kt new file mode 100644 index 000000000..dec6c3086 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/RepeatProtocolHandlerFactory.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.protocol.handler.factory + +import org.radarbase.appserver.jersey.service.protocol.handler.ProtocolHandler +import org.radarbase.appserver.jersey.service.protocol.handler.impl.SimpleRepeatProtocolHandler + +@Suppress("UNUSED_PARAMETER") +object RepeatProtocolHandlerFactory { + fun getRepeatProtocolHandler(protocolHandlerType: RepeatProtocolHandlerType): ProtocolHandler { + return SimpleRepeatProtocolHandler() + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/RepeatProtocolHandlerType.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/RepeatProtocolHandlerType.kt new file mode 100644 index 000000000..66699b2be --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/RepeatProtocolHandlerType.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.protocol.handler.factory + +enum class RepeatProtocolHandlerType { + SIMPLE, + DAYOFWEEK, + OTHER, +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/RepeatQuestionnaireHandlerFactory.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/RepeatQuestionnaireHandlerFactory.kt new file mode 100644 index 000000000..c3d9c8366 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/RepeatQuestionnaireHandlerFactory.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.protocol.handler.factory + +import org.radarbase.appserver.jersey.service.protocol.handler.ProtocolHandler +import org.radarbase.appserver.jersey.service.protocol.handler.impl.RandomRepeatQuestionnaireHandler +import org.radarbase.appserver.jersey.service.protocol.handler.impl.SimpleRepeatQuestionnaireHandler + +object RepeatQuestionnaireHandlerFactory { + fun getRepeatQuestionnaireHandler(questionnaireHandlerType: RepeatQuestionnaireHandlerType): ProtocolHandler { + return when (questionnaireHandlerType) { + RepeatQuestionnaireHandlerType.RANDOM -> RandomRepeatQuestionnaireHandler() + else -> SimpleRepeatQuestionnaireHandler() + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/RepeatQuestionnaireHandlerType.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/RepeatQuestionnaireHandlerType.kt new file mode 100644 index 000000000..b376ce875 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/factory/RepeatQuestionnaireHandlerType.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.protocol.handler.factory + +enum class RepeatQuestionnaireHandlerType { + SIMPLE, + DAYOFWEEKMAP, + RANDOM, + OTHER, +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/ClinicalProtocolHandler.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/ClinicalProtocolHandler.kt new file mode 100644 index 000000000..30fb46254 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/ClinicalProtocolHandler.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.protocol.handler.impl + +import org.radarbase.appserver.jersey.dto.protocol.Assessment +import org.radarbase.appserver.jersey.dto.questionnaire.AssessmentSchedule +import org.radarbase.appserver.jersey.entity.User +import org.radarbase.appserver.jersey.service.protocol.handler.ProtocolHandler + +/** + * Handles the processing and management of clinical protocols. + */ +class ClinicalProtocolHandler : ProtocolHandler { + /** + * Handles the processing of an assessment schedule by updating its name based on the provided assessment. + * + * @param assessmentSchedule The assessment schedule to be updated. + * @param assessment The assessment containing details used to update the schedule. + * @param user The user associated with the assessment. This parameter may be null. + * @return The updated assessment schedule with the name set based on the assessment's name. + */ + override suspend fun handle( + assessmentSchedule: AssessmentSchedule, + assessment: Assessment, + user: User, + ): AssessmentSchedule { + assessmentSchedule.name = assessment.name + return assessmentSchedule + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/CompletedQuestionnaireHandler.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/CompletedQuestionnaireHandler.kt new file mode 100644 index 000000000..a8eb5634d --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/CompletedQuestionnaireHandler.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.protocol.handler.impl + +import kotlinx.coroutines.Dispatchers +import org.radarbase.appserver.jersey.dto.protocol.Assessment +import org.radarbase.appserver.jersey.dto.questionnaire.AssessmentSchedule +import org.radarbase.appserver.jersey.entity.Task +import org.radarbase.appserver.jersey.entity.User +import org.radarbase.appserver.jersey.event.state.TaskState +import org.radarbase.appserver.jersey.service.protocol.handler.ProtocolHandler +import org.radarbase.appserver.jersey.utils.checkPresence +import org.radarbase.appserver.jersey.utils.mapParallel +import java.sql.Timestamp +import java.util.TimeZone + +open class CompletedQuestionnaireHandler( + private val prevTasks: List, + private val prevTimezone: String, +) : ProtocolHandler { + + override suspend fun handle( + assessmentSchedule: AssessmentSchedule, + assessment: Assessment, + user: User, + ): AssessmentSchedule { + val currentTimezone = checkPresence(user.timezone, "invalid_user_details") { + "User's timezone can't be null in completed questionnaire handler" + } + + val currentTask = assessmentSchedule.tasks ?: emptyList() + + markTasksAsCompleted(currentTask, prevTasks, currentTimezone, prevTimezone) + return assessmentSchedule + } + + suspend fun markTasksAsCompleted( + currentTasks: List, + previousTasks: List, + currentTimezone: String, + prevTimezone: String, + ): List { + currentTasks.mapParallel(Dispatchers.Default) { newTask -> + val matching = if (currentTimezone != prevTimezone) { + val taskTimestamp = newTask.timestamp + requireNotNull(taskTimestamp) { "Task timestamp cannot be null" } + + val prevTimestamp = getPreviousTimezoneEquivalent(taskTimestamp, currentTimezone, prevTimezone) + + previousTasks.parallelStream().filter { areMatchingTasks(newTask, it, prevTimestamp) }.findFirst() + } else { + previousTasks.parallelStream().filter { areMatchingTasks(newTask, it) }.findFirst() + } + + matching.ifPresent { matchingTask -> + if (matchingTask.status == TaskState.COMPLETED) { + newTask.apply { + completed = true + timeCompleted = matchingTask.timeCompleted + status = TaskState.COMPLETED + } + } + } + } + return currentTasks + } + + private fun areMatchingTasks(a: Task, b: Task): Boolean { + return a.timestamp == b.timestamp && a.name == b.name + } + + private fun areMatchingTasks(a: Task, b: Task, bTimestamp: Timestamp): Boolean { + return a.timestamp == bTimestamp && a.name == b.name + } + + private fun getPreviousTimezoneEquivalent( + taskTimestamp: Timestamp, + newTimezone: String, + prevTimezone: String, + ): Timestamp { + val timezoneDiff = TimeZone.getTimeZone(newTimezone).rawOffset - TimeZone.getTimeZone(prevTimezone).rawOffset + return Timestamp(taskTimestamp.time + timezoneDiff) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/DisabledNotificationHandler.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/DisabledNotificationHandler.kt new file mode 100644 index 000000000..8fe19bfbf --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/DisabledNotificationHandler.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.protocol.handler.impl + +import org.radarbase.appserver.jersey.dto.protocol.Assessment +import org.radarbase.appserver.jersey.dto.questionnaire.AssessmentSchedule +import org.radarbase.appserver.jersey.entity.User +import org.radarbase.appserver.jersey.service.protocol.handler.ProtocolHandler + +class DisabledNotificationHandler : ProtocolHandler { + override suspend fun handle( + assessmentSchedule: AssessmentSchedule, + assessment: Assessment, + user: User, + ): AssessmentSchedule { + return assessmentSchedule.also { + it.notifications = emptyList() + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/RandomRepeatQuestionnaireHandler.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/RandomRepeatQuestionnaireHandler.kt new file mode 100644 index 000000000..58ca8f900 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/RandomRepeatQuestionnaireHandler.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.protocol.handler.impl + +import org.radarbase.appserver.jersey.dto.protocol.Assessment +import org.radarbase.appserver.jersey.dto.protocol.TimePeriod +import org.radarbase.appserver.jersey.dto.questionnaire.AssessmentSchedule +import org.radarbase.appserver.jersey.entity.Task +import org.radarbase.appserver.jersey.entity.User +import org.radarbase.appserver.jersey.service.protocol.handler.ProtocolHandler +import org.radarbase.appserver.jersey.service.protocol.time.TimeCalculatorService +import org.radarbase.appserver.jersey.service.questionnaire.schedule.task.TaskGeneratorService +import java.time.Instant +import java.util.TimeZone + +class RandomRepeatQuestionnaireHandler : ProtocolHandler { + private val defaultTaskCompletionWindow = 86_400_000L + private val timeCalculatorService = TimeCalculatorService() + private val taskGeneratorService = TaskGeneratorService() + + override suspend fun handle( + assessmentSchedule: AssessmentSchedule, + assessment: Assessment, + user: User, + ): AssessmentSchedule { + val referenceTimestamps = assessmentSchedule.referenceTimestamps ?: return assessmentSchedule.apply { + tasks = emptyList() + } + + val tasks = generateTasks( + assessment, + referenceTimestamps, + user, + ) + assessmentSchedule.tasks = tasks + return assessmentSchedule + } + + private fun generateTasks( + assessment: Assessment, + referenceTimestamps: List, + user: User, + ): List { + val timezone = TimeZone.getTimeZone(user.timezone) + val repeatQuestionnaire = assessment.protocol?.repeatQuestionnaire + val randomUnitsFromZeroBetween = repeatQuestionnaire?.randomUnitsFromZeroBetween ?: return emptyList() + val completionWindow = calculateCompletionWindow(assessment.protocol?.completionWindow) + + val tasks = mutableListOf() + for (referenceTimestamp in referenceTimestamps) { + val timePeriod = TimePeriod().apply { unit = repeatQuestionnaire.unit } + for (range in randomUnitsFromZeroBetween) { + timePeriod.amount = getRandomAmountInRange(range) + val taskTime = timeCalculatorService.advanceRepeat(referenceTimestamp, timePeriod, timezone) + val task = taskGeneratorService.buildTask(assessment, taskTime, completionWindow).apply { + this.user = user + } + tasks.add(task) + } + } + return tasks + } + + private fun getRandomAmountInRange(range: Array): Int { + val (lowerLimit, upperLimit) = range + return (lowerLimit..upperLimit).random() + } + + private fun calculateCompletionWindow(completionWindow: TimePeriod?): Long { + return completionWindow?.let { + timeCalculatorService.timePeriodToMillis(it) + } ?: defaultTaskCompletionWindow + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/SimpleNotificationHandler.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/SimpleNotificationHandler.kt new file mode 100644 index 000000000..62789d388 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/SimpleNotificationHandler.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.protocol.handler.impl + +import kotlinx.coroutines.Dispatchers +import org.radarbase.appserver.jersey.dto.protocol.Assessment +import org.radarbase.appserver.jersey.dto.questionnaire.AssessmentSchedule +import org.radarbase.appserver.jersey.entity.Notification +import org.radarbase.appserver.jersey.entity.Task +import org.radarbase.appserver.jersey.entity.User +import org.radarbase.appserver.jersey.service.protocol.handler.ProtocolHandler +import org.radarbase.appserver.jersey.service.questionnaire.schedule.notification.NotificationType +import org.radarbase.appserver.jersey.service.questionnaire.schedule.notification.TaskNotificationGeneratorService +import org.radarbase.appserver.jersey.utils.mapParallel +import java.time.Instant +import java.time.temporal.ChronoUnit + +class SimpleNotificationHandler : ProtocolHandler { + private val taskNotificationGeneratorService = TaskNotificationGeneratorService() + + override suspend fun handle( + assessmentSchedule: AssessmentSchedule, + assessment: Assessment, + user: User, + ): AssessmentSchedule { + val notificationProtocol = assessment.protocol?.notification + val estimatedCompletionTime = assessment.estimatedCompletionTime + val assessmentScheduleTasks: List? = assessmentSchedule.tasks + + if (notificationProtocol == null || estimatedCompletionTime == null || assessmentScheduleTasks == null) { + return assessmentSchedule.also { + it.notifications = emptyList() + } + } + + val title = this.taskNotificationGeneratorService.getTitleText( + user.language, + notificationProtocol.title, + NotificationType.NOW, + ) + val body = this.taskNotificationGeneratorService.getBodyText( + user.language, + notificationProtocol.body, + NotificationType.NOW, + estimatedCompletionTime, + ) + + generateNotifications( + assessmentScheduleTasks, + user, + title, + body, + notificationProtocol.email.enabled, + ).also { notifications -> + assessmentSchedule.notifications = notifications + } + + return assessmentSchedule + } + + suspend fun generateNotifications( + tasks: List, + user: User, + title: String, + body: String, + emailEnabled: Boolean, + ): List { + return tasks.mapParallel(Dispatchers.Default) { task: Task -> + task.timestamp?.let { taskTimeStamp -> + this.taskNotificationGeneratorService.createNotification( + task, + taskTimeStamp.toInstant(), + title, + body, + emailEnabled, + ).apply { + this.user = user + } + } + }.filterNotNull().filter { notification: Notification -> + val scheduledTime = notification.scheduledTime + val ttlSeconds = notification.ttlSeconds + + scheduledTime != null && Instant.now().isBefore( + scheduledTime + .plus(ttlSeconds.toLong(), ChronoUnit.SECONDS), + ) + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/SimpleProtocolHandler.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/SimpleProtocolHandler.kt new file mode 100644 index 000000000..ea56cc4c4 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/SimpleProtocolHandler.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.protocol.handler.impl + +import org.radarbase.appserver.jersey.dto.protocol.Assessment +import org.radarbase.appserver.jersey.dto.protocol.ReferenceTimestamp +import org.radarbase.appserver.jersey.dto.protocol.ReferenceTimestampType +import org.radarbase.appserver.jersey.dto.questionnaire.AssessmentSchedule +import org.radarbase.appserver.jersey.entity.User +import org.radarbase.appserver.jersey.service.protocol.handler.ProtocolHandler +import org.radarbase.appserver.jersey.service.protocol.time.TimeCalculatorService +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.util.TimeZone + +/** + * A handler for processing simple protocols. This implementation defines the logic to update + * the [AssessmentSchedule] based on the protocol's reference timestamp. + * + * The `SimpleProtocolHandler` utilizes [TimeCalculatorService] for timestamp calculations + */ +class SimpleProtocolHandler : ProtocolHandler { + private val timeCalculatorService = TimeCalculatorService() + + /** + * Processes the given assessment schedule and updates it with appropriate timestamps + * + * @param assessmentSchedule The assessment schedule to be updated. + * @param assessment The assessment containing protocol details and additional metadata. + * @param user The user whose timezone and enrolment date are used in processing. + * @return The updated [AssessmentSchedule] with a populated reference timestamp and name. + * @throws IllegalArgumentException If the user's enrolment date is null when the reference timestamp + * is not provided. + */ + override suspend fun handle( + assessmentSchedule: AssessmentSchedule, + assessment: Assessment, + user: User, + ): AssessmentSchedule { + val referenceTimestamp: ReferenceTimestamp? = assessment.protocol?.referenceTimestamp + val timezone = TimeZone.getTimeZone(user.timezone) + val timezoneId = timezone.toZoneId() + assessmentSchedule.referenceTimestamp = if (referenceTimestamp != null) { + val timestamp = requireNotNull(referenceTimestamp.timestamp) { + "Reference timestamp is null when handling SimpleProtocolHandler." + } + val timestampFormat = requireNotNull(referenceTimestamp.format) { + "Reference timestamp format is null when handling SimpleProtocolHandler." + } + when (timestampFormat) { + ReferenceTimestampType.DATE -> LocalDate.parse(timestamp).atStartOfDay(timezoneId).toInstant() + ReferenceTimestampType.DATETIME -> LocalDateTime.parse(timestamp).atZone(timezoneId).toInstant() + ReferenceTimestampType.DATETIMEUTC -> Instant.parse(timestamp) + ReferenceTimestampType.NOW -> Instant.now() + ReferenceTimestampType.TODAY -> timeCalculatorService.setDateTimeToMidnight(Instant.now(), timezone) + } + } else { + val userEnrolmentDate = user.enrolmentDate + requireNotNull(userEnrolmentDate) { + "User enrolment date is null when handling SimpleProtocolHandler." + } + timeCalculatorService.setDateTimeToMidnight(userEnrolmentDate, timezone) + } + assessmentSchedule.name = assessment.name + return assessmentSchedule + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/SimpleReminderHandler.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/SimpleReminderHandler.kt new file mode 100644 index 000000000..dd49d5cd2 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/SimpleReminderHandler.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.protocol.handler.impl + +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import org.radarbase.appserver.jersey.dto.protocol.Assessment +import org.radarbase.appserver.jersey.dto.protocol.TimePeriod +import org.radarbase.appserver.jersey.dto.questionnaire.AssessmentSchedule +import org.radarbase.appserver.jersey.entity.Notification +import org.radarbase.appserver.jersey.entity.Task +import org.radarbase.appserver.jersey.entity.User +import org.radarbase.appserver.jersey.service.protocol.handler.ProtocolHandler +import org.radarbase.appserver.jersey.service.protocol.time.TimeCalculatorService +import org.radarbase.appserver.jersey.service.questionnaire.schedule.notification.NotificationType +import org.radarbase.appserver.jersey.service.questionnaire.schedule.notification.TaskNotificationGeneratorService +import org.radarbase.appserver.jersey.utils.flatMapParallel +import java.time.Instant +import java.time.temporal.ChronoUnit +import java.util.TimeZone + +class SimpleReminderHandler : ProtocolHandler { + private val taskNotificationGeneratorService = TaskNotificationGeneratorService() + private val timeCalculatorService = TimeCalculatorService() + + override suspend fun handle( + assessmentSchedule: AssessmentSchedule, + assessment: Assessment, + user: User, + ): AssessmentSchedule { + val timezone = TimeZone.getTimeZone(user.timezone) + val protocol = assessment.protocol?.notification + val assessmentScheduleTasks: List? = assessmentSchedule.tasks + val estimatedCompletionTime = assessment.estimatedCompletionTime + + if (protocol == null || assessmentScheduleTasks == null || estimatedCompletionTime == null) { + return assessmentSchedule.also { + it.reminders = emptyList() + } + } + + val title = this.taskNotificationGeneratorService.getTitleText( + user.language, + protocol.title, + NotificationType.REMINDER, + ) + val body = this.taskNotificationGeneratorService.getBodyText( + user.language, + protocol.body, + NotificationType.REMINDER, + estimatedCompletionTime, + ) + + val notifications = generateReminders( + assessmentScheduleTasks, + assessment, + timezone, + user, + title, + body, + protocol.email.enabled, + ) + assessmentSchedule.reminders = notifications + return assessmentSchedule + } + + suspend fun generateReminders( + tasks: List, + assessment: Assessment, + timezone: TimeZone, + user: User, + title: String, + body: String, + emailEnabled: Boolean, + ): List = coroutineScope { + tasks.flatMapParallel { task: Task -> + val reminders = assessment.protocol?.reminders ?: return@flatMapParallel emptyList() + val repeatReminders = reminders.repeat ?: return@flatMapParallel emptyList() + + (1..repeatReminders).map { repeat: Int -> + async { + val offset = TimePeriod(reminders.unit, reminders.amount!! * repeat) + + val timestamp = timeCalculatorService.advanceRepeat(task.timestamp!!.toInstant(), offset, timezone) + taskNotificationGeneratorService.createNotification( + task, + timestamp, + title, + body, + emailEnabled, + ).also { + it.user = user + } + } + }.awaitAll() + }.filter { notification -> + ( + Instant.now().isBefore( + notification.scheduledTime!! + .plus(notification.ttlSeconds.toLong(), ChronoUnit.SECONDS), + ) + ) + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/SimpleRepeatProtocolHandler.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/SimpleRepeatProtocolHandler.kt new file mode 100644 index 000000000..16c65ae47 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/SimpleRepeatProtocolHandler.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.protocol.handler.impl + +import org.radarbase.appserver.jersey.dto.protocol.Assessment +import org.radarbase.appserver.jersey.dto.protocol.RepeatProtocol +import org.radarbase.appserver.jersey.dto.protocol.TimePeriod +import org.radarbase.appserver.jersey.dto.questionnaire.AssessmentSchedule +import org.radarbase.appserver.jersey.entity.User +import org.radarbase.appserver.jersey.service.protocol.handler.ProtocolHandler +import org.radarbase.appserver.jersey.service.protocol.time.TimeCalculatorService +import org.slf4j.LoggerFactory +import java.time.Instant +import java.util.TimeZone + +class SimpleRepeatProtocolHandler : ProtocolHandler { + private val timeCalculatorService = TimeCalculatorService() + + override suspend fun handle( + assessmentSchedule: AssessmentSchedule, + assessment: Assessment, + user: User, + ): AssessmentSchedule { + val timezone = user.timezone + requireNotNull(timezone) { + "User timezone is null when handling SimpleRepeatProtocolHandler." + } + + val referenceTimestamp = requireNotNull(assessmentSchedule.referenceTimestamp) { + "Reference timestamp is null when handling SimpleRepeatProtocolHandler." + } + + val referenceTimestamps = generateReferenceTimestamps(assessment, referenceTimestamp, timezone) + assessmentSchedule.referenceTimestamps = referenceTimestamps + return assessmentSchedule + } + + private fun generateReferenceTimestamps( + assessment: Assessment, + startTime: Instant, + timezoneId: String, + ): List { + val timezone = TimeZone.getTimeZone(timezoneId) + val repeatProtocol: RepeatProtocol? = assessment.protocol?.repeatProtocol + + val repeatProtocolUnit: String? = repeatProtocol?.unit + val repeatProtocolAmount: Int? = repeatProtocol?.amount + + if (repeatProtocol == null || repeatProtocolUnit == null || repeatProtocolAmount == null) { + logger.warn("Repeat protocol is null for assessment in SimpleRepeatProtocolHandler") + return emptyList() + } + + val simpleRepeatProtocol = TimePeriod(repeatProtocolUnit, repeatProtocolAmount) + var referenceTime: Instant = calculateValidStartTime(startTime, timezone, simpleRepeatProtocol) + val referenceTimestamps: MutableList = mutableListOf() + while (isValidReferenceTimestamp(referenceTime, timezone)) { + referenceTimestamps.add(referenceTime) + referenceTime = timeCalculatorService.advanceRepeat(referenceTime, simpleRepeatProtocol, timezone) + } + return referenceTimestamps + } + + private fun isValidReferenceTimestamp(referenceTime: Instant, timezone: TimeZone): Boolean { + val defaultEndTime = timeCalculatorService.advanceRepeat(Instant.now(), PLUS_ONE_WEEK, timezone) + return referenceTime.isBefore(defaultEndTime) && + referenceTime.atZone(timezone.toZoneId()).year < MAX_YEAR + } + + private fun calculateValidStartTime( + startTime: Instant, + timezone: TimeZone, + simpleRepeatProtocol: TimePeriod, + ): Instant { + var referenceTime = startTime + val defaultStartTime = timeCalculatorService.advanceRepeat(Instant.now(), MINUS_ONE_WEEK, timezone) + while (referenceTime.isBefore(defaultStartTime)) { + referenceTime = timeCalculatorService.advanceRepeat(referenceTime, simpleRepeatProtocol, timezone) + } + return referenceTime + } + + companion object { + private val logger = LoggerFactory.getLogger(SimpleRepeatProtocolHandler::class.java) + + private val PLUS_ONE_WEEK = TimePeriod("week", 1) + private val MINUS_ONE_WEEK = TimePeriod("week", -1) + private const val MAX_YEAR = 2030 + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/SimpleRepeatQuestionnaireHandler.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/SimpleRepeatQuestionnaireHandler.kt new file mode 100644 index 000000000..3cc76719d --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/handler/impl/SimpleRepeatQuestionnaireHandler.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.protocol.handler.impl + +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import org.radarbase.appserver.jersey.dto.protocol.Assessment +import org.radarbase.appserver.jersey.dto.protocol.RepeatQuestionnaire +import org.radarbase.appserver.jersey.dto.protocol.TimePeriod +import org.radarbase.appserver.jersey.dto.questionnaire.AssessmentSchedule +import org.radarbase.appserver.jersey.entity.Task +import org.radarbase.appserver.jersey.entity.User +import org.radarbase.appserver.jersey.service.protocol.handler.ProtocolHandler +import org.radarbase.appserver.jersey.service.protocol.time.TimeCalculatorService +import org.radarbase.appserver.jersey.service.questionnaire.schedule.task.TaskGeneratorService +import org.radarbase.appserver.jersey.utils.flatMapParallel +import java.time.Instant +import java.util.TimeZone + +class SimpleRepeatQuestionnaireHandler : ProtocolHandler { + private val timeCalculatorService = TimeCalculatorService() + private val taskGeneratorService = TaskGeneratorService() + + override suspend fun handle( + assessmentSchedule: AssessmentSchedule, + assessment: Assessment, + user: User, + ): AssessmentSchedule { + val referenceTimestamp = assessmentSchedule.referenceTimestamps + ?: return assessmentSchedule.also { it.tasks = emptyList() } + + generateTasks(assessment, referenceTimestamp, user).let { + assessmentSchedule.apply { + tasks = it + } + } + return assessmentSchedule + } + + private suspend fun generateTasks( + assessment: Assessment, + referenceTimestamps: List, + user: User, + ): List = coroutineScope { + val timezone = TimeZone.getTimeZone(user.timezone) + + val repeatQuestionnaire: RepeatQuestionnaire = assessment.protocol?.repeatQuestionnaire ?: return@coroutineScope emptyList() + val repeatQuestionnaireUnit: String? = repeatQuestionnaire.unit ?: return@coroutineScope emptyList() + val unitsFromZero: List = repeatQuestionnaire.unitsFromZero.orEmpty() + + val completionWindow = this@SimpleRepeatQuestionnaireHandler.calculateCompletionWindow( + assessment.protocol?.completionWindow, + ) + + referenceTimestamps.flatMapParallel { referenceTimestamp: Instant -> + unitsFromZero.map { unitFromZero: Int -> + async { + val period = TimePeriod().apply { + unit = repeatQuestionnaireUnit + amount = unitFromZero + } + val taskTime = timeCalculatorService.advanceRepeat(referenceTimestamp, period, timezone) + taskGeneratorService.buildTask(assessment, taskTime, completionWindow).apply { + this.user = user + } + } + }.awaitAll() + } + } + + private fun calculateCompletionWindow(completionWindow: TimePeriod?): Long { + if (completionWindow == null) return DEFAULT_TASK_COMPLETION_WINDOW + return timeCalculatorService.timePeriodToMillis(completionWindow) + } + + companion object { + private const val DEFAULT_TASK_COMPLETION_WINDOW = 86400000L + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/time/TimeCalculatorService.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/time/TimeCalculatorService.kt new file mode 100644 index 000000000..1d1499a3c --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/protocol/time/TimeCalculatorService.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.protocol.time + +import org.radarbase.appserver.jersey.dto.protocol.TimePeriod +import java.time.Instant +import java.time.ZonedDateTime +import java.time.temporal.ChronoUnit +import java.util.* +import kotlin.time.Duration +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.minutes + +/** + * A service for performing time-related calculations such as advancing a timestamp by a given time period + * or converting time periods to milliseconds. This utility helps in handling various time-based operations, + * considering specific time zones. + */ +class TimeCalculatorService { + + /** + * Calculates an adjusted time based on a reference time, an offset, and a specific timezone. + * + * @param referenceTime the base time from which adjustments will be calculated + * @param offset the time period adjustment to be applied, containing the unit and amount + * @param timezone the timezone used to convert and calculate the adjusted time + * @return the calculated Instant time with the adjustment applied + */ + fun advanceRepeat(referenceTime: Instant, offset: TimePeriod, timezone: TimeZone): Instant { + val time = referenceTime.atZone(timezone.toZoneId()).truncatedTo(ChronoUnit.MILLIS) + val amount = requireNotNull(offset.amount) { "Amount cannot be null in time calculator service" } + return when (offset.unit) { + "min" -> time.plus(amount.toLong(), ChronoUnit.MINUTES).toInstant() + "hour" -> time.plus(amount.toLong(), ChronoUnit.HOURS).toInstant() + "day" -> time.plus(amount.toLong(), ChronoUnit.DAYS).toInstant() + "week" -> time.plus(amount.toLong(), ChronoUnit.WEEKS).toInstant() + "month" -> time.plus(amount.toLong(), ChronoUnit.MONTHS).toInstant() + "year" -> time.plus(amount.toLong(), ChronoUnit.YEARS).toInstant() + else -> ZonedDateTime.now().plus(2, ChronoUnit.YEARS).toInstant() + } + } + + /** + * Adjusts the given timestamp to midnight in the specified timezone. + * + * @param timestamp The original timestamp to be adjusted. + * @param timezone The timezone in which the timestamp should be set to midnight. + * @return The adjusted timestamp set to midnight in the specified timezone. + */ + fun setDateTimeToMidnight(timestamp: Instant, timezone: TimeZone): Instant { + val baseTime = timestamp.atZone(timezone.toZoneId()) + return baseTime.toLocalDate().atStartOfDay(baseTime.zone).toInstant() + } + + /** + * Converts a given time period into its equivalent duration in milliseconds. + * + * @param offset the time period to be converted, consisting of an amount and a unit, + * such as minutes, hours, days, weeks, months, or years. + * @return the time period's equivalent duration in milliseconds as a long value. + */ + fun timePeriodToMillis(offset: TimePeriod): Long { + val amount = requireNotNull(offset.amount) { "Amount cannot be null in time calculator service" } + val duration: Duration = when (offset.unit) { + "min" -> amount.minutes + "hour" -> amount.hours + "day" -> amount.days + "week" -> (amount * WEEK_TO_DAYS).days + "month" -> (amount * MONTH_TO_DAYS).days + "year" -> (amount * YEAR_TO_DAYS).days + else -> 1.days + } + return duration.inWholeMilliseconds + } + + companion object { + /** + * Represents the number of days in a week. + * + * This constant is used for conversions and calculations involving weeks + * and days, where a week is assumed to always contain 7 days. + */ + private const val WEEK_TO_DAYS = 7 + + /** + * Constant representing the number of days in a month. It is used as an approximate value + * where 31 days is considered the standard number of days in a month for calculations. + */ + private const val MONTH_TO_DAYS = 31 + + /** + * Represents the number of days in a standard non-leap year. + * + * This constant is used in date and time calculations where a year is approximated + * as 365 days. It does not account for leap years or variations in calendar systems. + */ + private const val YEAR_TO_DAYS = 365 + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/quartz/MessageJob.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/quartz/MessageJob.kt new file mode 100644 index 000000000..2f641865e --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/quartz/MessageJob.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.quartz + +import jakarta.inject.Inject +import org.quartz.Job +import org.quartz.JobExecutionContext +import org.quartz.JobExecutionException +import org.radarbase.appserver.jersey.exception.FcmMessageTransmitException +import org.radarbase.appserver.jersey.exception.MessageTransmitException +import org.radarbase.appserver.jersey.service.FcmDataMessageService +import org.radarbase.appserver.jersey.service.FcmNotificationService +import org.radarbase.appserver.jersey.service.transmitter.DataMessageTransmitter +import org.radarbase.appserver.jersey.service.transmitter.NotificationTransmitter +import org.radarbase.jersey.service.AsyncCoroutineService +import org.slf4j.LoggerFactory + +/** + * A [Job] that sends notification/message to the device or email when executed. + */ +class MessageJob @Inject constructor( + private val notificationTransmitter: NotificationTransmitter, + private val dataMessageTransmitter: DataMessageTransmitter, + private val notificationService: FcmNotificationService, + private val dataMessageService: FcmDataMessageService, + private val asyncService: AsyncCoroutineService, +) : Job { + /** + * Called by the `[org.quartz.Scheduler]` when a `[org.quartz.Trigger] + `* fires that is associated with the `Job`. + * + * The implementation may wish to set a [result][JobExecutionContext.setResult] + * object on the [JobExecutionContext] before this method exits. The result itself is + * meaningless to Quartz, but may be informative to `[org.quartz.JobListener]s` + * or `[org.quartz.TriggerListener]s` that are watching the job's execution. + * + * @param context context containing jobs details and data added when creating the job. + * @throws JobExecutionException if an error occurred while executing. + */ + @Throws(JobExecutionException::class) + override fun execute(context: JobExecutionContext) { + val jobDataMap = context.jobDetail.jobDataMap + val type = MessageType.valueOf(jobDataMap.getString("messageType")) + val projectId = jobDataMap.getString("projectId") + val subjectId = jobDataMap.getString("subjectId") + val messageId = jobDataMap.getLong("messageId") + val exceptions: MutableList = mutableListOf() + try { + when (type) { + MessageType.NOTIFICATION -> { + asyncService.runBlocking { + val notification = + notificationService.getNotificationByProjectIdAndSubjectIdAndNotificationId( + projectId, + subjectId, + messageId, + ) + + notificationTransmitter.let { transmitter -> + try { + transmitter.send(notification) + } catch (e: MessageTransmitException) { + exceptions.add(e) + } + } + } + } + + MessageType.DATA -> { + asyncService.runBlocking { + val dataMessage = dataMessageService.getDataMessageByProjectIdAndSubjectIdAndDataMessageId( + projectId, + subjectId, + messageId, + ) + + dataMessageTransmitter.let { transmitter -> + try { + transmitter.send(dataMessage) + } catch (e: MessageTransmitException) { + exceptions.add(e) + } + } + } + } + + MessageType.UNKNOWN -> { + logger.warn("Not executing job with type MessageType.UNKNOWN") + } + } + } catch (e: Exception) { + logger.error("Could not transmit a message", e) + throw JobExecutionException("Could not transmit a message", e) + } + + /** + * Exceptions that occurred while transmitting the message via the + * transmitters. At present, only the FcmTransmitter affects the job execution state. + */ + val fcmException: FcmMessageTransmitException? = + exceptions.filterIsInstance().firstOrNull() + if (fcmException != null) { + throw JobExecutionException("Could not transmit a message", fcmException) + } + } + + companion object { + private val logger = LoggerFactory.getLogger(MessageJob::class.java) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/quartz/MessageType.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/quartz/MessageType.kt new file mode 100644 index 000000000..774bc1a27 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/quartz/MessageType.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.quartz + +enum class MessageType { + NOTIFICATION, + DATA, + UNKNOWN, +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/quartz/QuartzNamingStrategy.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/quartz/QuartzNamingStrategy.kt new file mode 100644 index 000000000..043356db8 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/quartz/QuartzNamingStrategy.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.quartz + +interface QuartzNamingStrategy { + fun getTriggerName(userName: String, messageId: String): String + + fun getJobKeyName(userName: String, messageId: String): String + + fun getMessageId(key: String): String? +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/quartz/SchedulerService.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/quartz/SchedulerService.kt new file mode 100644 index 000000000..04e595bba --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/quartz/SchedulerService.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.quartz + +import org.quartz.JobDataMap +import org.quartz.JobDetail +import org.quartz.JobKey +import org.quartz.Trigger +import org.quartz.TriggerKey + +interface SchedulerService { + fun scheduleJob(jobDetail: JobDetail, trigger: Trigger) + + fun scheduleJobs(jobDetailTriggerMap: Map>) + + fun checkJobExists(jobKey: JobKey): Boolean + + fun checkTriggerExists(triggerKey: TriggerKey): Boolean + + fun updateScheduledJob( + jobKey: JobKey, + triggerKey: TriggerKey, + jobDataMap: JobDataMap, + associatedObject: Any?, + ) + + fun deleteScheduledJobs(jobKeys: List) + + fun deleteScheduledJob(jobKey: JobKey) +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/quartz/SchedulerServiceImpl.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/quartz/SchedulerServiceImpl.kt new file mode 100644 index 000000000..02b436c78 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/quartz/SchedulerServiceImpl.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.quartz + +import jakarta.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.quartz.JobDataMap +import org.quartz.JobDetail +import org.quartz.JobKey +import org.quartz.JobListener +import org.quartz.Scheduler +import org.quartz.SchedulerException +import org.quartz.SchedulerListener +import org.quartz.Trigger +import org.quartz.TriggerKey +import org.quartz.impl.triggers.SimpleTriggerImpl +import org.radarbase.appserver.jersey.entity.Scheduled +import org.slf4j.LoggerFactory +import java.util.Date + +class SchedulerServiceImpl @Inject constructor( + private val scheduler: Scheduler, + private val coroutineScope: CoroutineScope, + jobListener: JobListener?, + schedulerListener: SchedulerListener?, +) : SchedulerService { + + init { + if (!scheduler.isStarted) { + scheduler.start() + } + if (jobListener != null && schedulerListener != null) { + try { + scheduler.listenerManager.addJobListener(jobListener) + scheduler.listenerManager.addSchedulerListener(schedulerListener) + } catch (exc: SchedulerException) { + logger.warn("The Listeners could not be added to the scheduler", exc) + } + } else { + logger.error("The Listeners cannot be null and should be added to the scheduler") + } + } + + override fun scheduleJob(jobDetail: JobDetail, trigger: Trigger) { + coroutineScope.launch { + scheduler.scheduleJob(jobDetail, trigger) + } + } + + override fun checkJobExists(jobKey: JobKey): Boolean { + return scheduler.checkExists(jobKey) + } + + override fun checkTriggerExists(triggerKey: TriggerKey): Boolean { + return scheduler.checkExists(triggerKey) + } + + override fun scheduleJobs(jobDetailTriggerMap: Map>) { + coroutineScope.launch { + scheduler.scheduleJobs(jobDetailTriggerMap, true) + } + } + + override fun updateScheduledJob( + jobKey: JobKey, + triggerKey: TriggerKey, + jobDataMap: JobDataMap, + associatedObject: Any?, + ) { + coroutineScope.launch { + require(checkJobExists(jobKey)) { "The Specified Job Key does not exist : $jobKey" } + require(checkTriggerExists(triggerKey)) { "The Specified Trigger Key does not exist :$triggerKey" } + + val jobDetail = scheduler.getJobDetail(jobKey) + jobDetail.jobDataMap.putAll(jobDataMap.wrappedMap) + + val trigger = scheduler.getTrigger(triggerKey) as SimpleTriggerImpl + trigger.jobDataMap.putAll(jobDataMap.wrappedMap) + + if (associatedObject is Scheduled) { + val scheduledTime = + requireNotNull(associatedObject.scheduledTime) { "The scheduled time cannot be null" } + trigger.setStartTime(Date(scheduledTime.toEpochMilli())) + } + + scheduler.addJob(jobDetail, true) + scheduler.rescheduleJob(triggerKey, trigger) + } + } + + override fun deleteScheduledJobs(jobKeys: List) { + // The scheduler::deleteJobs method does not unschedule jobs so using a deleteJob. + jobKeys.forEach(this@SchedulerServiceImpl::deleteScheduledJob) + } + + override fun deleteScheduledJob(jobKey: JobKey) { + coroutineScope.launch { + if (scheduler.checkExists(jobKey)) { + jobKey.let(scheduler::deleteJob) + } + } + } + + companion object { + private val logger = LoggerFactory.getLogger(SchedulerServiceImpl::class.java) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/quartz/SimpleQuartzNamingStrategy.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/quartz/SimpleQuartzNamingStrategy.kt new file mode 100644 index 000000000..bda649e06 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/quartz/SimpleQuartzNamingStrategy.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.quartz + +class SimpleQuartzNamingStrategy : QuartzNamingStrategy { + override fun getTriggerName(userName: String, messageId: String): String { + return "$TRIGGER_PREFIX$userName-$messageId" + } + + override fun getJobKeyName(userName: String, messageId: String): String { + return "$JOB_PREFIX$userName-$messageId" + } + + override fun getMessageId(key: String): String? { + val keys: List = key.split("-") + return keys.lastOrNull() + } + + companion object { + private const val TRIGGER_PREFIX = "message-trigger-" + private const val JOB_PREFIX = "message-jobdetail-" + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/questionnaire/schedule/MessageSchedulerService.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/questionnaire/schedule/MessageSchedulerService.kt new file mode 100644 index 000000000..890e7e8ef --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/questionnaire/schedule/MessageSchedulerService.kt @@ -0,0 +1,234 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.questionnaire.schedule + +import jakarta.inject.Inject +import org.quartz.JobBuilder +import org.quartz.JobDataMap +import org.quartz.JobDetail +import org.quartz.JobKey +import org.quartz.SimpleScheduleBuilder +import org.quartz.Trigger +import org.quartz.TriggerBuilder +import org.quartz.TriggerKey +import org.radarbase.appserver.jersey.entity.DataMessage +import org.radarbase.appserver.jersey.entity.Message +import org.radarbase.appserver.jersey.entity.Notification +import org.radarbase.appserver.jersey.entity.User +import org.radarbase.appserver.jersey.fcm.downstream.FcmSender +import org.radarbase.appserver.jersey.service.quartz.MessageJob +import org.radarbase.appserver.jersey.service.quartz.MessageType +import org.radarbase.appserver.jersey.service.quartz.QuartzNamingStrategy +import org.radarbase.appserver.jersey.service.quartz.SchedulerService +import org.radarbase.appserver.jersey.service.quartz.SimpleQuartzNamingStrategy +import org.slf4j.LoggerFactory +import java.time.Instant +import java.util.Date + +@Suppress("unused") +class MessageSchedulerService @Inject constructor( + val fcmSender: FcmSender, + val schedulerService: SchedulerService, +) { + fun schedule(message: T) { + logger.debug("Scheduling message with id {}", message.id) + val jobDetail = getJobDetailForMessage(message, getMessageType(message)) + if (schedulerService.checkJobExists(jobDetail.key)) { + logger.info("Job with key {} has been scheduled already", { jobDetail.key }) + } else { + logger.debug("Job Detail = {}", jobDetail) + val trigger = getTriggerForMessage(message, jobDetail) + schedulerService.scheduleJob(jobDetail, trigger) + } + } + + fun scheduleMultiple(messages: List) { + val jobDetailSetMap: Map> = buildMap { + for (message in messages) { + val jobDetail = getJobDetailForMessage(message, getMessageType(message)) + + if (schedulerService.checkJobExists(jobDetail.key)) { + logger.info("Job with key {} is already scheduled", { jobDetail.key }) + continue + } + + setOf(getTriggerForMessage(message, jobDetail)).also { triggers -> + this.putIfAbsent(jobDetail, triggers) + } + } + } + logger.info("Scheduling {} messages", jobDetailSetMap.size) + schedulerService.scheduleJobs(jobDetailSetMap) + } + + fun updateScheduled(message: T) { + val (messageId: Long, subjectId: String) = nonNullMessageIdAndSubjectId(message) + val jobKeyString: String = NAMING_STRATEGY.getJobKeyName( + subjectId, + messageId.toString(), + ) + val jobKey = JobKey(jobKeyString) + val triggerKeyString: String = + NAMING_STRATEGY.getTriggerName( + subjectId, + messageId.toString(), + ) + val triggerKey = TriggerKey(triggerKeyString) + val jobDataMap = JobDataMap() + + schedulerService.updateScheduledJob(jobKey, triggerKey, jobDataMap, message) + } + + fun deleteScheduledMultiple(messages: List) { + messages.map { + val (messageId: Long, subjectId: String) = nonNullMessageIdAndSubjectId(it) + JobKey(NAMING_STRATEGY.getJobKeyName(subjectId, messageId.toString())) + } + .let(schedulerService::deleteScheduledJobs) + } + + fun deleteScheduled(message: T) { + JobKey(NAMING_STRATEGY.getJobKeyName(message.user!!.subjectId!!, message.id.toString())) + .let(schedulerService::deleteScheduledJob) + } + + fun getMessageType(message: T): MessageType = when (message) { + is Notification -> MessageType.NOTIFICATION + is DataMessage -> MessageType.DATA + else -> MessageType.UNKNOWN + } + + companion object { + private val logger = LoggerFactory.getLogger(MessageSchedulerService::class.java) + + val NAMING_STRATEGY: QuartzNamingStrategy = SimpleQuartzNamingStrategy() + + /** + * Build a Quartz [Trigger] that will fire exactly once at the message’s scheduled time. + * + * @param message the [Message] whose scheduling fields must be non-null + * @param jobDetail the [JobDetail] to which this trigger will be bound + * @return a one-shot [Trigger] that starts at `message.scheduledTime` + * @throws IllegalArgumentException if any of `message.id`, `message.user?.subjectId` or + * `message.scheduledTime` is null + */ + fun getTriggerForMessage(message: Message, jobDetail: JobDetail): Trigger { + val (messageId: Long, subjectId: String, scheduledTime: Instant) = nonNullTriggerUtils(message) + + return TriggerBuilder.newTrigger() + .withIdentity( + TriggerKey( + NAMING_STRATEGY.getTriggerName( + subjectId, + messageId.toString(), + ), + ), + ) + .forJob(jobDetail) + .startAt(Date(scheduledTime.toEpochMilli())) + .withSchedule( + SimpleScheduleBuilder.simpleSchedule() + .withRepeatCount(0) + .withIntervalInMilliseconds(0) + .withMisfireHandlingInstructionFireNow(), + ) + .build() + } + + /** + * Build a Quartz [JobDetail] that carries the message payload. + * + * @param message the [Message] whose fields must be non-null + * @param messageType the type of the message + * @return a durable [JobDetail] with its [JobDataMap] populated from `message` and `messageType` + * @throws IllegalArgumentException if any of `message.id`, `message.user?.subjectId`, + * `message.user?.project?.projectId` is null + */ + fun getJobDetailForMessage(message: Message, messageType: MessageType): JobDetail { + val (messageId: Long, subjectId: String, projectId: String) = nonNullJobUtils(message) + + val dataMap = JobDataMap( + mapOf( + "subjectId" to subjectId, + "projectId" to projectId, + "messageId" to messageId, + "messageType" to messageType.toString(), + ), + ) + + return JobBuilder.newJob(MessageJob::class.java) + .withIdentity( + JobKey( + NAMING_STRATEGY.getJobKeyName( + subjectId, + messageId.toString(), + ), + ), + ) + .withDescription("Send message at scheduled time...") + .setJobData(dataMap) + .storeDurably(true) + .build() + } + + /** + * Extract and validate the three mandatory scheduling fields from [message]. + * + * @return a [Triple] of `(messageId, subjectId, scheduledTime)` + * @throws IllegalArgumentException if any required field is null + */ + fun nonNullTriggerUtils(message: Message): Triple { + val (messageId: Long, subjectId: String) = nonNullMessageIdAndSubjectId(message) + val scheduledTime: Instant = requireNotNull(message.scheduledTime) { "Scheduled time cannot be null" } + + return Triple(messageId, subjectId, scheduledTime) + } + + /** + * Extract and validate the three mandatory job-creation fields from [message]. + * + * @return a [Triple] of `(messageId, subjectId, projectId)` + * @throws IllegalArgumentException if any required field is null + */ + fun nonNullJobUtils(message: Message): Triple { + val (messageId: Long, subjectId: String) = nonNullMessageIdAndSubjectId(message) + + val user: User = requireNotNull(message.user) { "User for message cannot be null" } + val projectId: String = requireNotNull( + requireNotNull(user.project) { "Project for user in message cannot be null" } + .projectId, + ) { "Project Id for user in message cannot be null" } + + return Triple(messageId, subjectId, projectId) + } + + /** + * Extract and validate the two mandatory common fields from [message]. + * + * @return a [Pair] of `(messageId, subjectId)` + * @throws IllegalArgumentException if `message.id` or `message.user?.subjectId` is null + */ + fun nonNullMessageIdAndSubjectId(message: Message): Pair { + val messageId: Long = requireNotNull(message.id) { "Message Id cannot be null" } + val subjectId: String = requireNotNull( + requireNotNull(message.user) { "User for message cannot be null" }.subjectId, + ) { "Subject Id in message cannot be null" } + + return Pair(messageId, subjectId) + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/questionnaire/schedule/ProtocolHandlerRunner.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/questionnaire/schedule/ProtocolHandlerRunner.kt new file mode 100644 index 000000000..7c8ade45f --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/questionnaire/schedule/ProtocolHandlerRunner.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.questionnaire.schedule + +import org.radarbase.appserver.jersey.dto.protocol.Assessment +import org.radarbase.appserver.jersey.dto.questionnaire.AssessmentSchedule +import org.radarbase.appserver.jersey.entity.User +import org.radarbase.appserver.jersey.service.protocol.handler.ProtocolHandler + +/** + * A class responsible for managing and executing protocol handlers for processing + * and generating an updated assessment schedule. This class allows the dynamic + * addition of protocol handlers which are executed sequentially to apply transformations + * or updates on the assessment schedule. + */ +class ProtocolHandlerRunner { + + /** + * Holds a mutable list of protocol handlers that can be dynamically added and used to process + * various [ProtocolHandler] implementations. + * The protocol handlers are executed sequentially, and each handler modifies the + * [AssessmentSchedule] based on its logic. + */ + private val protocolHandlers: MutableList = mutableListOf() + + /** + * Executes all registered protocol handlers on the provided assessment and user data, sequentially updating + * the `AssessmentSchedule`. Each protocol handler processes the schedule and applies its logic based on the + * assessment and user details. + * + * @param assessment The assessment containing protocol details and metadata to be processed. + * @param user The user whose information, such as timezone and enrolment date, is utilized during processing. + * @return The final updated assessment schedule after being processed by all protocol handlers. + */ + suspend fun runProtocolHandlers(assessment: Assessment, user: User): AssessmentSchedule { + var assessmentSchedule = AssessmentSchedule() + protocolHandlers.forEach { leaf: ProtocolHandler -> + assessmentSchedule = leaf.handle(assessmentSchedule, assessment, user) + } + return assessmentSchedule + } + + /** + * Adds a `ProtocolHandler` to the list of protocol handlers if it is not null. + * + * @param protocolHandler The `ProtocolHandler` instance to be added. If the parameter is null, + * no action is performed. + */ + fun addProtocolHandler(protocolHandler: ProtocolHandler?) { + if (protocolHandler != null) this.protocolHandlers.add(protocolHandler) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/questionnaire/schedule/QuestionnaireScheduleGeneratorService.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/questionnaire/schedule/QuestionnaireScheduleGeneratorService.kt new file mode 100644 index 000000000..8e83377c0 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/questionnaire/schedule/QuestionnaireScheduleGeneratorService.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.questionnaire.schedule + +import org.radarbase.appserver.jersey.dto.protocol.Assessment +import org.radarbase.appserver.jersey.dto.protocol.AssessmentType +import org.radarbase.appserver.jersey.dto.protocol.NotificationProtocol +import org.radarbase.appserver.jersey.dto.protocol.RepeatProtocol +import org.radarbase.appserver.jersey.dto.protocol.RepeatQuestionnaire +import org.radarbase.appserver.jersey.entity.Task +import org.radarbase.appserver.jersey.service.protocol.handler.ProtocolHandler +import org.radarbase.appserver.jersey.service.protocol.handler.factory.CompletedQuestionnaireHandlerFactory +import org.radarbase.appserver.jersey.service.protocol.handler.factory.NotificationHandlerFactory +import org.radarbase.appserver.jersey.service.protocol.handler.factory.ProtocolHandlerFactory +import org.radarbase.appserver.jersey.service.protocol.handler.factory.ProtocolHandlerType +import org.radarbase.appserver.jersey.service.protocol.handler.factory.ReminderHandlerFactory +import org.radarbase.appserver.jersey.service.protocol.handler.factory.RepeatProtocolHandlerFactory +import org.radarbase.appserver.jersey.service.protocol.handler.factory.RepeatProtocolHandlerType +import org.radarbase.appserver.jersey.service.protocol.handler.factory.RepeatQuestionnaireHandlerFactory +import org.radarbase.appserver.jersey.service.protocol.handler.factory.RepeatQuestionnaireHandlerType +import org.slf4j.LoggerFactory +import java.io.IOException + +class QuestionnaireScheduleGeneratorService : ScheduleGeneratorService { + + override fun getProtocolHandler(assessment: Assessment): ProtocolHandler { + return when (assessment.type) { + AssessmentType.CLINICAL -> ProtocolHandlerFactory.getProtocolHandler(ProtocolHandlerType.CLINICAL) + else -> ProtocolHandlerFactory.getProtocolHandler(ProtocolHandlerType.SIMPLE) + } + } + + override fun getRepeatProtocolHandler(assessment: Assessment): ProtocolHandler? { + if (assessment.type == AssessmentType.CLINICAL) return null + + val repeatProtocol: RepeatProtocol? = assessment.protocol?.repeatProtocol + val type = if (repeatProtocol?.dayOfWeek != null) { + RepeatProtocolHandlerType.DAYOFWEEK + } else { + RepeatProtocolHandlerType.SIMPLE + } + return RepeatProtocolHandlerFactory.getRepeatProtocolHandler(type) + } + + override fun getRepeatQuestionnaireHandler(assessment: Assessment): ProtocolHandler? { + if (assessment.type == AssessmentType.CLINICAL) return null + + val repeatQuestionnaire: RepeatQuestionnaire? = assessment.protocol?.repeatQuestionnaire + val type = when { + repeatQuestionnaire?.dayOfWeekMap != null -> RepeatQuestionnaireHandlerType.DAYOFWEEKMAP + repeatQuestionnaire?.randomUnitsFromZeroBetween != null -> RepeatQuestionnaireHandlerType.RANDOM + else -> RepeatQuestionnaireHandlerType.SIMPLE + } + + return RepeatQuestionnaireHandlerFactory.getRepeatQuestionnaireHandler(type) + } + + override fun getNotificationHandler(assessment: Assessment): ProtocolHandler? { + if (assessment.type == AssessmentType.CLINICAL) return null + val protocol: NotificationProtocol = assessment.protocol?.notification ?: return null + + return try { + NotificationHandlerFactory.getNotificationHandler(protocol) + } catch (_: IOException) { + logger.error("Invalid Notification Handler Type") + null + } + } + + override fun getReminderHandler(assessment: Assessment): ProtocolHandler? { + return if (assessment.type == AssessmentType.CLINICAL) { + null + } else { + ReminderHandlerFactory.reminderHandler + } + } + + override fun getCompletedQuestionnaireHandler( + assessment: Assessment, + prevTasks: List, + prevTimezone: String, + ): ProtocolHandler? { + return if (assessment.type == AssessmentType.CLINICAL) { + null + } else { + CompletedQuestionnaireHandlerFactory.getCompletedQuestionnaireHandler(prevTasks, prevTimezone) + } + } + + companion object { + private val logger = LoggerFactory.getLogger(QuestionnaireScheduleGeneratorService::class.java) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/questionnaire/schedule/QuestionnaireScheduleService.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/questionnaire/schedule/QuestionnaireScheduleService.kt new file mode 100644 index 000000000..213807587 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/questionnaire/schedule/QuestionnaireScheduleService.kt @@ -0,0 +1,276 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.questionnaire.schedule + +import jakarta.inject.Inject +import org.radarbase.appserver.jersey.dto.protocol.Assessment +import org.radarbase.appserver.jersey.dto.protocol.AssessmentType +import org.radarbase.appserver.jersey.dto.protocol.Protocol +import org.radarbase.appserver.jersey.dto.questionnaire.AssessmentSchedule +import org.radarbase.appserver.jersey.dto.questionnaire.Schedule +import org.radarbase.appserver.jersey.entity.Notification +import org.radarbase.appserver.jersey.entity.Task +import org.radarbase.appserver.jersey.entity.User +import org.radarbase.appserver.jersey.repository.ProjectRepository +import org.radarbase.appserver.jersey.repository.UserRepository +import org.radarbase.appserver.jersey.search.TaskSpecificationsBuilder +import org.radarbase.appserver.jersey.service.FcmNotificationService +import org.radarbase.appserver.jersey.service.TaskService +import org.radarbase.appserver.jersey.service.github.protocol.ProtocolGenerator +import org.radarbase.appserver.jersey.service.scheduling.SchedulingService +import org.radarbase.appserver.jersey.utils.checkInvalidDetails +import org.radarbase.appserver.jersey.utils.checkPresence +import org.radarbase.jersey.exception.HttpNotFoundException +import org.radarbase.jersey.service.AsyncCoroutineService +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.sql.Timestamp +import java.time.Duration +import java.time.Instant + +@Suppress("unused") +class QuestionnaireScheduleService @Inject constructor( + private val protocolGenerator: ProtocolGenerator, + private val scheduleGeneratorService: ScheduleGeneratorService, + private val userRepository: UserRepository, + private val projectRepository: ProjectRepository, + private val taskService: TaskService, + private val notificationService: FcmNotificationService, + schedulingService: SchedulingService, + asyncService: AsyncCoroutineService, +) { + private val subjectScheduleMap: HashMap = hashMapOf() + + private val cleanScheduleRef: SchedulingService.RepeatReference = schedulingService.repeat( + Duration.ofMillis(3_600_000), + Duration.ofMillis(5_000), + ) { + asyncService.runBlocking { + generateAllSchedules() + } + } + + suspend fun getTasksUsingProjectIdAndSubjectId(subjectId: String, projectId: String): List { + return getTasksForUser(subjectAndProjectExistsElseThrow(subjectId, projectId)) + } + + suspend fun getTasksByTypeUsingProjectIdAndSubjectId( + projectId: String, + subjectId: String, + type: AssessmentType, + search: String, + ): List { + getSearchBuilder(projectId, subjectId, type, search).build().also { spec -> + return this.taskService.getTasksBySpecification(spec) + } + } + + suspend fun getTasksForDateUsingProjectIdAndSubjectId( + subjectId: String, + projectId: String, + startTime: Instant, + endTime: Instant, + ): List { + val user: User = subjectAndProjectExistsElseThrow(subjectId, projectId) + val tasks: MutableList = this.getTasksForUser(user).toMutableList() + + tasks.removeIf { task -> + val taskTime: Timestamp? = task.timestamp + checkNotNull(taskTime) { "Task timestamp cannot is null in questionnaire scheduler service." } + + val completionWindow: Long? = task.completionWindow + checkNotNull(completionWindow) { "Task completion window is null in questionnaire scheduler service." } + + taskTime.toInstant().let { taskTimeInstant -> + taskTimeInstant.plusMillis(completionWindow).isBefore(startTime) || taskTimeInstant.isAfter(endTime) + } + } + + return tasks + } + + suspend fun generateScheduleUsingProjectIdAndSubjectId(subjectId: String, projectId: String): Schedule { + return subjectAndProjectExistsElseThrow(subjectId, projectId).run { + generateScheduleForUser(this) + } + } + + suspend fun generateScheduleForUser(user: User): Schedule { + val subjectId: String? = user.subjectId + checkNotNull(subjectId) { "Subject ID cannot be null in questionnaire scheduler service." } + val protocol: Protocol? = protocolGenerator.getProtocolForSubject(subjectId) + + val newSchedule: Schedule = protocol?.let { + val prevSchedule: Schedule = getScheduleForSubject(subjectId) + val prevTimeZone: String = prevSchedule.timezone ?: checkNotNull(user.timezone) { + "User timezone cannot be null in questionnaire scheduler service." + } + + if ((prevSchedule.version != it.version) || (prevTimeZone != user.timezone)) { + removeScheduleForUser(user) + } + scheduleGeneratorService.generateScheduleForUser(user, it, prevSchedule) + } ?: Schedule() + + return newSchedule.also { + subjectScheduleMap[subjectId] = it + saveTasksAndNotifications(user, newSchedule.assessmentSchedules) + } + } + + suspend fun saveTasksAndNotifications(user: User, assessmentSchedules: List) { + assessmentSchedules.filterNotNull() + .filter(AssessmentSchedule::hasTasks) + .forEach { + val (tasks, notifications, reminders) = nonNullTasksNotificationsAndReminders( + it.tasks, + it.notifications, + it.reminders, + ) + + taskService.addTasks(tasks, user) + notificationService.addNotifications(notifications, user) + notificationService.addNotifications(reminders, user) + } + } + + suspend fun generateScheduleUsingProjectIdAndSubjectIdAndAssessment( + projectId: String, + subjectId: String, + assessment: Assessment, + ): Schedule { + val user: User = subjectAndProjectExistsElseThrow(subjectId, projectId) + val protocol: Protocol? = protocolGenerator.getProtocolForSubject(subjectId) + + checkInvalidDetails( + { protocol == null || !protocol.hasAssessment(assessment.name) }, + { "Assessment not found in protocol. Add assessment to protocol first" }, + ) + + val userTimeZone = user.timezone + checkNotNull(userTimeZone) { "User timezone cannot be null in questionnaire scheduler service." } + + val schedule = getScheduleForSubject(subjectId) + val assessmentSchedule = scheduleGeneratorService.generateSingleAssessmentSchedule( + assessment, + user, + emptyList(), + userTimeZone, + ) + + schedule.addAssessmentSchedule(assessmentSchedule) + + saveTasksAndNotifications(user, listOf(assessmentSchedule)) + + return schedule + } + + suspend fun generateAllSchedules() { + logger.info("Generating all schedules") + userRepository.findAll().also { users: List -> + users.forEach { + generateScheduleForUser(it) + } + } + } + + fun getScheduleForSubject(subjectId: String): Schedule { + val schedule: Schedule? = subjectScheduleMap[subjectId] + return schedule ?: Schedule() + } + + suspend fun getTasksForUser(user: User): List { + return taskService.getTasksByUser(user) + } + + suspend fun subjectAndProjectExistsElseThrow(subjectId: String, projectId: String): User { + return checkPresence(this.projectRepository.findByProjectId(projectId), "project_not_found") { + "Project with projectId $projectId not found. Please create the project first." + }.let { project -> + checkPresence( + this.userRepository.findBySubjectIdAndProjectId( + subjectId, + checkNotNull(project.id) { "Project ID cannot be null." }, + ), + "user_not_found", + ) { + "User with subjectId $subjectId not found. Please create the user first." + } + } + } + + suspend fun removeScheduleForUser(user: User) { + val userId = checkNotNull(user.id) { "User ID cannot be null." } + taskService.deleteTasksByUserId(userId) + } + + suspend fun removeScheduleForUserUsingSubjectIdAndType( + projectId: String, + subjectId: String, + type: AssessmentType, + search: String, + ) { + getSearchBuilder(projectId, subjectId, type, search).build().also { taskSpecification -> + taskService.deleteTasksBySpecification(taskSpecification) + } + } + + suspend fun getSearchBuilder( + projectId: String, + subjectId: String, + type: AssessmentType?, + search: String?, + ): TaskSpecificationsBuilder { + val builder = TaskSpecificationsBuilder() + + subjectAndProjectExistsElseThrow(subjectId, projectId).also { user -> + builder.with("user", ":", user) + } + + if (type != null && type != AssessmentType.ALL) { + builder.with("type", ":", type) + } + if (!search.isNullOrBlank()) { + search.split(COMMA_PATTERN).forEach { searchTerm: String -> + TASK_SEARCH_PATTERN.matchEntire(searchTerm.trim())?.also { matcher -> + val (field, operator, value) = matcher.destructured + builder.with(field, operator, value) + } + } + } + return builder + } + + companion object { + private val logger: Logger = LoggerFactory.getLogger(QuestionnaireScheduleService::class.java) + + private val TASK_SEARCH_PATTERN = Regex("(\\w+)([:<>])(\\w+)") + private val COMMA_PATTERN = Regex(",") + + fun nonNullTasksNotificationsAndReminders( + tasks: List?, + notifications: List?, + reminders: List?, + ): Triple, List, List> { + val nonNullTasks = requireNotNull(tasks) { "Tasks cannot be null" } + val nonNullNotifications = requireNotNull(notifications) { "Notifications cannot be null" } + val nonNullReminders = requireNotNull(reminders) { "Reminders cannot be null" } + + return Triple(nonNullTasks, nonNullNotifications, nonNullReminders) + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/questionnaire/schedule/ScheduleGeneratorService.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/questionnaire/schedule/ScheduleGeneratorService.kt new file mode 100644 index 000000000..ce370562c --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/questionnaire/schedule/ScheduleGeneratorService.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.questionnaire.schedule + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope +import org.radarbase.appserver.jersey.dto.protocol.Assessment +import org.radarbase.appserver.jersey.dto.protocol.Protocol +import org.radarbase.appserver.jersey.dto.questionnaire.AssessmentSchedule +import org.radarbase.appserver.jersey.dto.questionnaire.Schedule +import org.radarbase.appserver.jersey.entity.Task +import org.radarbase.appserver.jersey.entity.User +import org.radarbase.appserver.jersey.service.protocol.handler.ProtocolHandler +import org.radarbase.appserver.jersey.utils.mapParallel +import kotlin.collections.orEmpty + +interface ScheduleGeneratorService { + fun getProtocolHandler(assessment: Assessment): ProtocolHandler? + + fun getRepeatProtocolHandler(assessment: Assessment): ProtocolHandler? + + fun getRepeatQuestionnaireHandler(assessment: Assessment): ProtocolHandler? + + fun getNotificationHandler(assessment: Assessment): ProtocolHandler? + + fun getReminderHandler(assessment: Assessment): ProtocolHandler? + + fun getCompletedQuestionnaireHandler( + assessment: Assessment, + prevTasks: List, + prevTimezone: String, + ): ProtocolHandler? + + suspend fun generateScheduleForUser( + user: User, + protocol: Protocol, + prevSchedule: Schedule, + ): Schedule = coroutineScope { + val assessments = protocol.protocols ?: return@coroutineScope Schedule() + + val prevScheduledTaskByName: Map?> = prevSchedule.assessmentSchedules + .associate { it.name to it.tasks } + + val prevTimezone = prevSchedule.timezone ?: user.timezone!! + + val assessmentSchedules = assessments.mapParallel(Dispatchers.Default) { assessment -> + val prevTasks = prevScheduledTaskByName[assessment.name].orEmpty() + generateSingleAssessmentSchedule(assessment, user, prevTasks, prevTimezone) + } + + Schedule(assessmentSchedules, user, protocol.version) + } + + suspend fun generateSingleAssessmentSchedule( + assessment: Assessment, + user: User, + previousTasks: List, + prevTimezone: String, + ): AssessmentSchedule { + val protocolHandlerRunner = ProtocolHandlerRunner().apply { + addProtocolHandler(getProtocolHandler(assessment)) + addProtocolHandler(getRepeatProtocolHandler(assessment)) + addProtocolHandler(getRepeatQuestionnaireHandler(assessment)) + addProtocolHandler(getNotificationHandler(assessment)) + addProtocolHandler(getReminderHandler(assessment)) + addProtocolHandler( + getCompletedQuestionnaireHandler(assessment, previousTasks, prevTimezone), + ) + } + return protocolHandlerRunner.runProtocolHandlers(assessment, user) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/questionnaire/schedule/notification/NotificationType.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/questionnaire/schedule/notification/NotificationType.kt new file mode 100644 index 000000000..7034ffbbb --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/questionnaire/schedule/notification/NotificationType.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.questionnaire.schedule.notification + +enum class NotificationType { + SOON, + NOW, + REMINDER, + MISSED_SOON, + MISSED, + TEST, + OTHER, +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/questionnaire/schedule/notification/TaskNotificationGeneratorService.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/questionnaire/schedule/notification/TaskNotificationGeneratorService.kt new file mode 100644 index 000000000..e954bfffd --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/questionnaire/schedule/notification/TaskNotificationGeneratorService.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.questionnaire.schedule.notification + +import org.radarbase.appserver.jersey.dto.protocol.LanguageText +import org.radarbase.appserver.jersey.entity.Notification +import org.radarbase.appserver.jersey.entity.Task +import java.time.Instant + +class TaskNotificationGeneratorService { + fun createNotification( + task: Task, + notificationTimestamp: Instant, + title: String?, + body: String?, + emailEnabled: Boolean, + ): Notification { + return Notification.NotificationBuilder().apply { + scheduledTime(notificationTimestamp) + ttlSeconds(calculateTtlSeconds(task, notificationTimestamp)) + type(task.name) + sourceType("Type") + sourceId("id") + appPackage("org.phidatalab.radar-armt") + task(task) + title(title) + body(body) + emailEnabled(emailEnabled) + }.build() + } + + fun getTitleText(language: String?, title: LanguageText?, type: NotificationType): String { + return when { + title != null -> title.getText(language) + else -> when (type) { + NotificationType.REMINDER -> "Missed a questionnaire?" + else -> "Questionnaire time!!" + } + } + } + + fun getBodyText(language: String?, body: LanguageText?, type: NotificationType, time: Int): String { + return when { + body != null -> body.getText(language) + else -> when (type) { + NotificationType.REMINDER -> "It seems you haven't answered all of our questions. Could you please do that now?" + else -> "Won't usually take longer than $time minutes" + } + } + } + + private fun calculateTtlSeconds(task: Task, notificationTimestamp: Instant): Int { + val endTime = task.timestamp!!.getTime() + task.completionWindow!! + val timeUntilEnd = endTime - notificationTimestamp.toEpochMilli() + return timeUntilEnd.toInt() + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/questionnaire/schedule/task/TaskGeneratorService.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/questionnaire/schedule/task/TaskGeneratorService.kt new file mode 100644 index 000000000..0761effec --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/questionnaire/schedule/task/TaskGeneratorService.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.questionnaire.schedule.task + +import org.radarbase.appserver.jersey.dto.protocol.Assessment +import org.radarbase.appserver.jersey.dto.protocol.AssessmentType +import org.radarbase.appserver.jersey.entity.Task +import java.time.Instant + +class TaskGeneratorService { + fun buildTask(assessment: Assessment, timestamp: Instant, completionWindow: Long): Task { + val isClinical = assessment.type == AssessmentType.CLINICAL + return Task.TaskBuilder().apply { + name(assessment.name) + type(assessment.type) + estimatedCompletionTime(assessment.estimatedCompletionTime!!) + completionWindow(completionWindow) + priority(assessment.order) + timestamp(timestamp) + showInCalendar(assessment.showInCalendar) + isDemo(assessment.isDemo) + nQuestions(assessment.nQuestions) + isClinical(isClinical) + }.build() + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/scheduling/SchedulingService.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/scheduling/SchedulingService.kt new file mode 100644 index 000000000..002a9a075 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/scheduling/SchedulingService.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.scheduling + +import org.slf4j.LoggerFactory +import java.io.Closeable +import java.time.Duration +import java.util.concurrent.CancellationException +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit + +class SchedulingService : Closeable { + private val scheduler = Executors.newSingleThreadScheduledExecutor() + + fun execute(method: () -> Unit) = scheduler.execute(method) + + fun repeat(rate: Duration, initialDelay: Duration, method: () -> Unit): RepeatReference { + val ref = scheduler.scheduleAtFixedRate(method, initialDelay.toMillis(), rate.toMillis(), TimeUnit.MILLISECONDS) + return FutureRepeatReference(ref) + } + + interface RepeatReference : Closeable + + private inner class FutureRepeatReference(private val ref: ScheduledFuture<*>) : RepeatReference { + override fun close() { + if (ref.cancel(false)) { + try { + ref.get() + } catch (ex: CancellationException) { + // this is expected + } catch (ex: Exception) { + logger.warn("Failed to get repeating job result", ex) + } + } + } + } + + override fun close() { + scheduler.shutdown() // Disable new tasks from being submitted + + try { + // Wait a while for existing tasks to terminate + if (!scheduler.awaitTermination(TERMINATION_INTERVAL_SECONDS, TimeUnit.SECONDS)) { + scheduler.shutdownNow() // Cancel currently executing tasks + // Wait a while for tasks to respond to being cancelled + if (!scheduler.awaitTermination(TERMINATION_INTERVAL_SECONDS, TimeUnit.SECONDS)) { + logger.error("SchedulingService did not terminate") + } + } + } catch (ie: InterruptedException) { + // (Re-)Cancel if current thread also interrupted + scheduler.shutdownNow() + // Preserve interrupt status + Thread.currentThread().interrupt() + } + } + + companion object { + private val logger = LoggerFactory.getLogger(SchedulingService::class.java) + + private const val TERMINATION_INTERVAL_SECONDS = 60L + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/transmitter/DataMessageTransmitter.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/transmitter/DataMessageTransmitter.kt new file mode 100644 index 000000000..8a9101852 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/transmitter/DataMessageTransmitter.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.transmitter + +import org.radarbase.appserver.jersey.entity.DataMessage +import org.radarbase.appserver.jersey.exception.MessageTransmitException + +interface DataMessageTransmitter { + @Throws(MessageTransmitException::class) + suspend fun send(dataMessage: DataMessage) +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/transmitter/FcmTransmitter.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/transmitter/FcmTransmitter.kt new file mode 100644 index 000000000..73b7b01ee --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/transmitter/FcmTransmitter.kt @@ -0,0 +1,197 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.transmitter + +import com.google.firebase.ErrorCode +import com.google.firebase.messaging.FirebaseMessagingException +import com.google.firebase.messaging.MessagingErrorCode +import jakarta.inject.Inject +import org.radarbase.appserver.jersey.dto.fcm.FcmUserDto +import org.radarbase.appserver.jersey.entity.DataMessage +import org.radarbase.appserver.jersey.entity.Message +import org.radarbase.appserver.jersey.entity.Notification +import org.radarbase.appserver.jersey.exception.FcmMessageTransmitException +import org.radarbase.appserver.jersey.fcm.downstream.FcmSender +import org.radarbase.appserver.jersey.fcm.model.FcmDataMessage +import org.radarbase.appserver.jersey.fcm.model.FcmNotificationMessage +import org.radarbase.appserver.jersey.service.FcmDataMessageService +import org.radarbase.appserver.jersey.service.FcmNotificationService +import org.radarbase.appserver.jersey.service.UserService +import org.radarbase.appserver.jersey.utils.requireNotNullField +import org.slf4j.LoggerFactory +import java.util.Objects + +class FcmTransmitter @Inject constructor( + private val fcmSender: FcmSender, + private val notificationService: FcmNotificationService, + private val dataMessageService: FcmDataMessageService, + private val userService: UserService, +) : DataMessageTransmitter, NotificationTransmitter { + + override suspend fun send(dataMessage: DataMessage) { + try { + fcmSender.send(createMessageFromDataMessage(dataMessage)) + } catch (exc: FirebaseMessagingException) { + handleFcmException(exc, dataMessage) + } catch (exc: Exception) { + throw FcmMessageTransmitException("Could not transmit a data message through Fcm. ${exc.message}") + } + } + + override suspend fun send(notification: Notification) { + try { + fcmSender.send(createMessageFromNotification(notification)) + } catch (exc: FirebaseMessagingException) { + handleFcmException(exc, notification) + } catch (exc: Exception) { + throw FcmMessageTransmitException("Could not transmit a notification through Fcm. ${exc.message}") + } + } + + private suspend fun handleFcmException(exc: FirebaseMessagingException, message: Message?) { + logger.error("Error occurred when sending downstream message.", exc) + if (message != null) { + handleErrorCode(exc.errorCode, message) + handleFCMErrorCode(exc.messagingErrorCode, message) + } + } + + @Suppress("UNUSED_PARAMETER") + fun handleErrorCode(errorCode: ErrorCode, message: Message?) { + // More info on ErrorCode: https://firebase.google.com/docs/reference/fcm/rest/v1/ErrorCode + when (errorCode) { + ErrorCode.INVALID_ARGUMENT, + ErrorCode.INTERNAL, + ErrorCode.ABORTED, + ErrorCode.CONFLICT, + ErrorCode.CANCELLED, + ErrorCode.DATA_LOSS, + ErrorCode.NOT_FOUND, + ErrorCode.OUT_OF_RANGE, + ErrorCode.ALREADY_EXISTS, + ErrorCode.DEADLINE_EXCEEDED, + ErrorCode.PERMISSION_DENIED, + ErrorCode.RESOURCE_EXHAUSTED, + ErrorCode.FAILED_PRECONDITION, + ErrorCode.UNAUTHENTICATED, + ErrorCode.UNKNOWN, + -> {} + ErrorCode.UNAVAILABLE -> { + // Could schedule for retry. + logger.warn("The FCM service is unavailable") + } + } + } + + private suspend fun handleFCMErrorCode(errorCode: MessagingErrorCode, message: Message) { + when (errorCode) { + MessagingErrorCode.INTERNAL, MessagingErrorCode.QUOTA_EXCEEDED, MessagingErrorCode.INVALID_ARGUMENT, MessagingErrorCode.SENDER_ID_MISMATCH, MessagingErrorCode.THIRD_PARTY_AUTH_ERROR -> {} + MessagingErrorCode.UNAVAILABLE -> { + // Could schedule for retry. + logger.warn("The FCM service is unavailable.") + } + + MessagingErrorCode.UNREGISTERED -> { + val userDto = FcmUserDto(requireNotNull(message.user) { "User cannot be null" }) + val subjectId = requireNotNullField(userDto.subjectId, "Subject Id") + val projectId = requireNotNullField(userDto.projectId, "Project Id") + + logger.warn("The Device for user {} was unregistered.", userDto.subjectId) + notificationService.removeNotificationsForUser( + projectId, + subjectId, + ) + dataMessageService.removeDataMessagesForUser( + projectId, + subjectId, + ) + userService.checkFcmTokenExistsAndReplace(userDto) + } + } + } + + companion object { + private val logger = LoggerFactory.getLogger(FcmTransmitter::class.java) + + private const val IS_DELIVERY_RECEIPT_REQUESTED: Boolean = true + private const val DEFAULT_TIME_TO_LIVE: Int = 2419200 // 4 weeks + + private fun createMessageFromNotification(notification: Notification): FcmNotificationMessage { + val to = Objects.requireNonNullElseGet( + notification.fcmTopic, + requireNotNullField(notification.user, "Notification's User")::fcmToken, + ) + return FcmNotificationMessage().apply { + this.to = to + this.condition = notification.fcmCondition + this.priority = notification.priority + this.mutableContent = notification.mutableContent + this.deliveryReceiptRequested = IS_DELIVERY_RECEIPT_REQUESTED + this.data = notification.additionalData + this.messageId = notification.fcmMessageId.toString() + this.timeToLive = Objects.requireNonNullElse(notification.ttlSeconds, DEFAULT_TIME_TO_LIVE) + this.notification = getNotificationMap(notification) + } + } + + private fun createMessageFromDataMessage(dataMessage: DataMessage): FcmDataMessage { + val to = + Objects.requireNonNullElseGet( + dataMessage.fcmTopic, + requireNotNullField(dataMessage.user, "Data Message's User")::fcmToken, + ) + return FcmDataMessage().apply { + this.to = to + this.condition = dataMessage.fcmCondition + this.priority = dataMessage.priority + this.mutableContent = dataMessage.mutableContent + this.deliveryReceiptRequested = IS_DELIVERY_RECEIPT_REQUESTED + this.messageId = dataMessage.fcmMessageId.toString() + this.timeToLive = Objects.requireNonNullElse(dataMessage.ttlSeconds, DEFAULT_TIME_TO_LIVE) + this.data = dataMessage.dataMap + } + } + + private fun getNotificationMap(notification: Notification): Map { + val notificationMap: MutableMap = HashMap() + notificationMap["body"] = notification.body ?: "" + notificationMap["title"] = requireNotNullField(notification.title, "Notification's Title") + notificationMap["sound"] = "default" + + putIfNotNull(notificationMap, "sound", notification.sound) + putIfNotNull(notificationMap, "badge", notification.badge) + putIfNotNull(notificationMap, "click_action", notification.clickAction) + putIfNotNull(notificationMap, "subtitle", notification.subtitle) + putIfNotNull(notificationMap, "body_loc_key", notification.bodyLocKey) + putIfNotNull(notificationMap, "body_loc_args", notification.bodyLocArgs) + putIfNotNull(notificationMap, "title_loc_key", notification.titleLocKey) + putIfNotNull(notificationMap, "title_loc_args", notification.titleLocArgs) + putIfNotNull(notificationMap, "android_channel_id", notification.androidChannelId) + putIfNotNull(notificationMap, "icon", notification.icon) + putIfNotNull(notificationMap, "tag", notification.tag) + putIfNotNull(notificationMap, "color", notification.color) + + return notificationMap + } + + fun putIfNotNull(map: MutableMap, key: String, value: Any?) { + if (value != null) { + map[key] = value + } + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/transmitter/NotificationTransmitter.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/transmitter/NotificationTransmitter.kt new file mode 100644 index 000000000..f4bed6cef --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/service/transmitter/NotificationTransmitter.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.service.transmitter + +import org.radarbase.appserver.jersey.entity.Notification +import org.radarbase.appserver.jersey.exception.MessageTransmitException + +interface NotificationTransmitter { + @Throws(MessageTransmitException::class) + suspend fun send(notification: Notification) +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/Coroutines.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/Coroutines.kt new file mode 100644 index 000000000..0111ca935 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/Coroutines.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.utils + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlin.coroutines.CoroutineContext + +/** + * Launches one coroutine per element in this iterable, applying [transform] in parallel, + * and returns a list of all results once every coroutine completes. + * + * This is useful for CPU‑ or I/O‑bound operations where you want to concurrently process + * each item without blocking the caller thread. + * + * @receiver The [Iterable] of input elements to transform. + * @param context The [CoroutineContext] to launch each coroutine in. Defaults to [Dispatchers.Default]. + * @param transform A suspending function to apply to each element. + * @return A [List] of transformed results, in the same order as the original iterable. + */ +suspend inline fun Iterable.mapParallel( + context: CoroutineContext, + crossinline transform: suspend (T) -> R, +): List = coroutineScope { + map { t -> + async(context) { transform(t) } + }.awaitAll() +} + +/** + * Like flatMap but runs each transform in parallel. + */ +suspend inline fun Iterable.flatMapParallel( + context: CoroutineContext = Dispatchers.Default, + crossinline transform: suspend (T) -> List, +): List = coroutineScope { + map { t -> + async(context) { transform(t) } + }.awaitAll() + .flatten() +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/Extensions.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/Extensions.kt new file mode 100644 index 000000000..d3ae2be84 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/Extensions.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * + * * + * * * Copyright 2018 King's College London + * * * + * * * Licensed under the Apache License, Version 2.0 (the "License"); + * * * you may not use this file except in compliance with the License. + * * * You may obtain a copy of the License at + * * * + * * * http://www.apache.org/licenses/LICENSE-2.0 + * * * + * * * Unless required by applicable law or agreed to in writing, software + * * * distributed under the License is distributed on an "AS IS" BASIS, + * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * * See the License for the specific language governing permissions and + * * * limitations under the License. + * * * + * * + * + */ + +package org.radarbase.appserver.jersey.utils + +import kotlin.reflect.KProperty1 + +/** + * Compares the current object instance with another object to determine equality based on specified fields. + * + * @param other the object to compare with the current instance. + * @param fields the properties of the current object to check for equality. + * @return true if the specified fields of both objects are equal; false otherwise. + */ +internal inline fun T.equalTo(other: Any?, vararg fields: KProperty1): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as T + return fields.all { field -> field.get(this) == field.get(other) } +} + +/** + * Generates a string representation of the calling object by including its class name and the + * values of the specified properties. + * + * @param T the type of the object + * @param fields the properties of the object to include in the string representation + * @return a formatted string representation of the object and its specified properties + */ +internal inline fun T.stringRepresentation(vararg fields: KProperty1): String { + return "${this::class.simpleName}(${fields.joinToString(", ") { field -> + val propertyName = field.name + "$propertyName=${field.get(this)}" + }})" +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/Paths.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/Paths.kt new file mode 100644 index 000000000..533e53155 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/Paths.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.utils + +object Paths { + const val PROJECTS_PATH = "projects" + const val USERS_PATH = "users" + const val PROJECT_ID = "{projectId}" + const val SUBJECT_ID = "{subjectId}" + const val NOTIFICATION_ID = "{notificationId}" + const val MESSAGING_DATA_PATH = "messaging/data" + const val MESSAGING_NOTIFICATION_PATH = "messaging/notifications" + const val NOTIFICATION_STATE_EVENTS_PATH = "state_events" + const val QUESTIONNAIRE_STATE_EVENTS_PATH = "state_events" + const val GITHUB_PATH = "github" + const val GITHUB_CONTENT_PATH = "content" + const val PROTOCOLS_PATH = "protocols" + const val TASK_ID = "{taskId}" + const val TASKS_PATH = "tasks" + const val QUESTIONNAIRE_SCHEDULE = "questionnaire/schedule" + const val ALL_KEYWORD = "all" +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/ReentrantMutex.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/ReentrantMutex.kt new file mode 100644 index 000000000..172b40003 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/ReentrantMutex.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.utils + +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.coroutineContext + +/** + * Acquires a lock on the given [Mutex], allowing **reentrant locking** within the same coroutine. + * + * This function ensures that if a coroutine already holds the given [Mutex], + * it can safely re-enter the lock without deadlocking. This is achieved by + * storing a context element in the coroutine's [CoroutineContext] when the + * lock is first acquired, and checking for that marker in any nested calls. + * + * ### Behavior: + * - On first call in a coroutine, the [Mutex] is locked using [withLock], and + * a marker is added to the coroutine context. + * - On nested calls with the same [Mutex], the marker is detected and the block + * executes immediately without re-locking. + * + * ### Example: + * ``` + * val mutex = Mutex() + * + * suspend fun doSomething() = mutex.withReentrantLock { + * println("First level") + * mutex.withReentrantLock { + * println("Second level") + * } + * } + * ``` + * + * @param block The suspending block of code to execute under the lock. + * @return The result of the [block]. + */ +suspend fun Mutex.withReentrantLock(block: suspend () -> T): T { + val key = ReentrantMutexContextKey(this) + + if (coroutineContext[key] != null) { + return block() + } + + return withContext(ReentrantMutexContextElement(key)) { + withLock { + block() + } + } +} + +data class ReentrantMutexContextKey(val mutex: Mutex) : CoroutineContext.Key + +class ReentrantMutexContextElement( + override val key: ReentrantMutexContextKey, +) : CoroutineContext.Element diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/Utils.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/Utils.kt new file mode 100644 index 000000000..7558efa64 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/Utils.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.utils + +import jakarta.inject.Provider +import org.radarbase.appserver.jersey.dto.ProjectDto +import org.radarbase.appserver.jersey.exception.InvalidProjectDetailsException +import org.radarbase.auth.token.DataRadarToken +import org.radarbase.auth.token.RadarToken +import org.radarbase.jersey.exception.HttpForbiddenException +import org.radarbase.jersey.exception.HttpNotFoundException +import org.radarbase.jersey.service.AsyncCoroutineService +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +/** + * Throws [HttpNotFoundException] with the result of calling [messageProvider] if the value is null. + * Otherwise, returns the not null value. + */ +@OptIn(ExperimentalContracts::class) +inline fun checkPresence(value: T?, code: String, messageProvider: () -> String): T { + contract { + returns() implies (value != null) + } + + if (value == null) { + throw HttpNotFoundException(code, messageProvider()) + } else { + return value + } +} + +fun requireNotNullField(value: T?, fieldName: String): T = checkNotNull(value) { "$fieldName cannot be null" } + +/** + * Validates a condition for the given project details and throws [InvalidProjectDetailsException] + * if the condition is met (Project details are invalid). + * + * @param projectDTO the [ProjectDto] containing details of the project being validated + * @param invalidation a lambda returning `true` if the project details are invalid + * @param messageProvider a lambda providing the error message for the exception + * @throws InvalidProjectDetailsException if the validation fails + */ +@Suppress("UNUSED_PARAMETER") +inline fun checkInvalidProjectDetails( + projectDTO: ProjectDto, + invalidation: () -> Boolean, + messageProvider: () -> String, +) { + if (invalidation()) { + throw InvalidProjectDetailsException( + messageProvider(), + ) + } +} + +/** + * Validates a condition for the given details and throws a specified + * RuntimeException if the condition is met (User details are invalid). + * + * @param invalidation a lambda returning `true` if the user details are invalid + * @param messageProvider a lambda providing the error message for the exception + * @throws E runtime exception if the validation fails + * + */ +inline fun checkInvalidDetails( + invalidation: () -> Boolean, + messageProvider: () -> String, +) { + if (invalidation()) { + throw E::class.java.getDeclaredConstructor( + String::class.java, + ).newInstance(messageProvider()) + } +} + +/** + * Throws an exception of type [E] if the given [shouldInvalidate] is true. + * + * After this function returns normally, Kotlin’s flow analysis knows that + * `shouldInvalidate` is false, so any value checked by it can be treated + * as “valid” (eg: non‑null). + * + * @param shouldInvalidate A Boolean expression: if true, an [E] is thrown. + * @param messageProvider Supplies the exception message when invalidation occurs. + * @throws E if [shouldInvalidate] is true. + */ +@OptIn(ExperimentalContracts::class) +inline fun checkInvalidDetails( + shouldInvalidate: Boolean, + messageProvider: () -> String, +) { + contract { + returns() implies (!shouldInvalidate) + } + if (shouldInvalidate) { + throw E::class.java + .getDeclaredConstructor(String::class.java) + .newInstance(messageProvider()) + } +} + +suspend inline fun tokenForCurrentRequest( + asyncService: AsyncCoroutineService, + tokenProvider: Provider, +): RadarToken = asyncService.runInRequestScope { + try { + DataRadarToken(tokenProvider.get()) + } catch (_: Throwable) { + throw HttpForbiddenException("unauthorized", "User without authentication does not have permission.") + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/annotation/CheckExactlyOneNotNull.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/annotation/CheckExactlyOneNotNull.kt new file mode 100644 index 000000000..e19f30aa6 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/annotation/CheckExactlyOneNotNull.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.utils.annotation + +import jakarta.validation.Constraint +import jakarta.validation.ConstraintValidator +import jakarta.validation.ConstraintValidatorContext +import java.beans.Introspector +import java.beans.PropertyDescriptor +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.annotation.AnnotationTarget.CLASS + +@Target(CLASS) +@Retention(RUNTIME) +@Constraint(validatedBy = [CheckExactlyOneNotNull.Validator::class]) +annotation class CheckExactlyOneNotNull( + val fieldNames: Array, +) { + class Validator : ConstraintValidator { + private lateinit var fieldNames: Array + + override fun initialize(constraint: CheckExactlyOneNotNull) { + fieldNames = constraint.fieldNames + } + + override fun isValid(value: Any?, context: ConstraintValidatorContext): Boolean { + if (value == null) return false + + val beanInfo = Introspector.getBeanInfo(value::class.java, Any::class.java) + val props: Array = beanInfo.propertyDescriptors + + return try { + var nonNullCount = 0 + for (pd in props) { + if (pd.readMethod != null && fieldNames.contains(pd.name)) { + val propValue = pd.readMethod.invoke(value) + if (propValue != null) nonNullCount++ + } + } + nonNullCount == 1 + } catch (_: Exception) { + false + } + } + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/cache/CachedFunction.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/cache/CachedFunction.kt new file mode 100644 index 000000000..a07aad837 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/cache/CachedFunction.kt @@ -0,0 +1,190 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.utils.cache + +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.radarbase.appserver.jersey.utils.cache.deps.CustomThrowingFunction +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.lang.ref.SoftReference +import java.time.Duration +import java.time.Instant + +/** + * A generic caching function wrapper that caches computation results for a specified duration. + * + * Provides a mechanism to cache expensive function computations, retry computations on failure, + * and limit the number of cache entries. If the cached result is expired or unavailable, the computation + * is re-executed using the underlying function. + * + * @param I The type of the input to the function. + * @param O The type of the output from the function. + * @property function The function whose results should be cached. + * @property cacheTime The duration for which cached results remain valid. + * @property retryTime The duration to wait before retrying a computation after a failure. + * @*/ +class CachedFunction( + private val function: CustomThrowingFunction, + val cacheTime: Duration, + val retryTime: Duration, + val maxEntries: Int = 0, +) : CustomThrowingFunction { + private val cacheLock: Mutex = Mutex() + + private val cachedMap: MutableMap = LinkedHashMap(16, 0.75f, false) + + /** + * Computes or retrieves a cached value associated with the given key. + * If the value is not present in the cache, it computes the value and stores it in the cache. + * This method ensures thread-safety and handles exceptions during value computation. + * + * @param key the input key used to fetch or compute the cached value + * @return the cached or newly computed output value associated with the key + * @throws Exception if computation of the value fails + */ + @Throws(Exception::class) + override suspend fun applyWithException(key: I): O { + return cacheLock.withLock { + cachedMap.getOrPut(key) { + LockedResult(key) + }.also { + checkMaxSize() + } + }.getOrCompute() + } + + /** + * Determines whether the eldest entry in the map should be removed based on the current size and + * the maximum number of entries allowed. + */ + private fun checkMaxSize() { + val toRemove = cachedMap.size - maxEntries + if (toRemove > 0) { + val iter = cachedMap.entries.iterator() + repeat(toRemove) { + iter.next() + iter.remove() + } + } + } + + /** + * Represents a thread-safe container for caching and computing results associated with a specific input. + * + * This class ensures that the result is computed only when necessary, and cached results are reused + * when they are still valid (not expired). If an error occurs during computation, the error is cached + * with a retry time, allowing controlled re-computation. + * + * @param I the type of the input parameter used for computation. + * @param O the type of the computed result. + * @property input The input parameter for which the result is being cached or computed. + * @property reference A soft reference to a previously computed result. This may expire and be cleared + * by the garbage collector if memory is needed, or the result may become invalid based + * on its expiration. + */ + private inner class LockedResult( + private val input: I, + private var reference: SoftReference> = SoftReference(null), + ) { + private val computeLock: Mutex = Mutex() + + /** + * Retrieves a cached or precomputed result if available and valid. Otherwise, computes a new result, + * caches it, and returns it. If the computation fails, an exception is thrown and the failure is cached + * for a retry duration. + * + * @return The computed or cached result of type `O`. + * @throws Exception If an error occurs during the computation process. + */ + @Throws(Exception::class) + suspend fun getOrCompute(): O = computeLock.withLock { + val result: Result? = reference.get() + + if (result != null && !result.isExpired()) { + return when (result) { + is Result.Success -> result.value + is Result.Failure -> throw result.exception + } + } + + try { + logger.debug("Computing result for input {}", input) + return function.applyWithException(input).also { computedResult -> + reference = SoftReference( + Result.Success( + computedResult, + Instant.now().plus(cacheTime), + ), + ) + } + } catch (ex: Exception) { + logger.error("Failed to compute result for input {}", input, ex) + reference = SoftReference( + Result.Failure( + Instant.now().plus(retryTime), + ex, + ), + ) + throw ex + } + } + } + + /** + * Represents the result of an operation that can succeed with a value or fail with an exception. + * This sealed class allows modeling of both success and failure outcomes for an operation. + * + * @param T the type of the successful result's value + */ + sealed class Result { + + abstract val expiration: Instant + + /** + * Represents a successful result in an operation. + * + * @param T The type of the value associated with the success result. + * @property value The value associated with the successful result. + * @property expiration The time in the future when this result should no longer be considered valid. + */ + data class Success(val value: T, override val expiration: Instant) : Result() + + /** + * Represents a failure result in an operation. + * + * This class is a specialized type of [Result] that contains information + * about an exception occurring during the execution of an operation. It allows + * tracking both the exception details and the expiration time for the result. + * + * @property expiration the expiration time for the failure result + * @property exception the exception that caused the failure + */ + data class Failure(override val expiration: Instant, val exception: Exception) : Result() + + /** + * Checks if the result has expired based on the current time. + * + * @return true if the current time is after the expiration time, false otherwise + */ + fun isExpired(): Boolean = Instant.now().isAfter(expiration) + } + + companion object { + private val logger: Logger = LoggerFactory.getLogger(CachedFunction::class.java) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/cache/CachedMap.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/cache/CachedMap.kt new file mode 100644 index 000000000..75a86e8ea --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/cache/CachedMap.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.utils.cache + +import org.radarbase.appserver.jersey.utils.cache.deps.AtomicNonNullReference +import java.io.IOException +import java.time.Duration +import java.time.Instant + +/** + * Map that caches the result of a list for a limited time. + * + * This class is thread-safe if the given supplier is thread-safe. + */ +@Suppress("unused") +class CachedMap( + private val supplier: ThrowingSupplier>, + private val invalidateAfter: Duration, + private val retryAfter: Duration, +) { + + /** + * Cache holding the result. Initialized with an empty map and a minimum timestamp. + */ + private val cache = AtomicNonNullReference(Result(emptyMap(), Instant.MIN)) + + /** + * Get the cached map or retrieve a new one if the current one is old. + * + * @return map of data + * @throws IOException if the data could not be retrieved. + */ + @Throws(IOException::class) + suspend fun get(): Map = get(forceRefresh = false) + + /** + * Get the cached map or retrieve a new one if the current one is old. + * + * @param forceRefresh if true, the cache will be refreshed even if it is recent. + * @return map of data + * @throws IOException if the data could not be retrieved. + */ + @Throws(IOException::class) + suspend fun get(forceRefresh: Boolean): Map { + val currentResult: Result = cache.get() + if (!forceRefresh && !currentResult.isStale(invalidateAfter)) { + return currentResult.map + } + + val newResult: Map = supplier.get() + cache.set(Result(newResult)) + return newResult + } + + /** + * Get the cached map. Does not refresh the map even if the data is old. + * + * @return map of data + */ + fun getCachedMap(): Map = cache.get().map + + /** + * Get a key from the map. If the key is missing, it will check whether + * the cache may be updated. If so, it will fetch the cache again and look the key up. + * + * @param key key of the value to find. + * @return element or null if it is not found + * @throws IOException if the cache cannot be refreshed. + */ + @Throws(IOException::class) + suspend fun getByKey(key: S): T? { + val currentResult: Result = cache.get() + val value: T? = currentResult.map[key] + return if (value == null && currentResult.isStale(retryAfter)) { + get(true)[key] + } else { + value + } + } + + /** + * Supplier that may throw an IOException, Otherwise similar to [java.util.function.Supplier]. + */ + fun interface ThrowingSupplier { + @Throws(IOException::class) + suspend fun get(): T + } + + /** + * Result container holding the cached map and its fetch time. + */ + private data class Result( + val map: Map, + private val fetchTime: Instant = Instant.now(), + ) { + fun isStale(freshDuration: Duration): Boolean = + Duration.between(fetchTime, Instant.now()) > freshDuration + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/cache/deps/AtomicNonNullReference.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/cache/deps/AtomicNonNullReference.kt new file mode 100644 index 000000000..92c36983e --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/cache/deps/AtomicNonNullReference.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.utils.cache.deps + +import java.util.concurrent.atomic.AtomicReference + +/** + * A thread-safe wrapper for an atomic reference that ensures the value is non-nullable. + * This class provides atomic operations for accessing and updating the value. + * + * @param T the type of the value, constrained to be non-nullable. + * @param initialValue the initial value to be set in the atomic reference. + */ +@Suppress("unused") +class AtomicNonNullReference(initialValue: T) { + + private val reference = AtomicReference(initialValue) + + /** + * Retrieves the current non-null value stored in the atomic reference. + * + * @return the current value of type T + */ + fun get(): T = reference.get() + + /** + * Sets the given value to the reference, ensuring it is not null. + * + * @param newValue the non-null value to set to the reference + * @throws IllegalArgumentException if the provided value is null + */ + fun set(newValue: T) { + requireNotNull(newValue) { "Cannot set null value to NonNullableAtomicReference" } + reference.set(newValue) + } + + /** + * Atomically sets the value to the given update value if the current value equals the expected value. + * + * @param expect the expected current value + * @param update the new value to set if the current value matches the expected value + * @return `true` if the value was successfully updated, `false` otherwise + */ + fun compareAndSet(expect: T, update: T): Boolean { + requireNotNull(update) { "Cannot set null value to NonNullableAtomicReference" } + return reference.compareAndSet(expect, update) + } +} diff --git a/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/cache/deps/CustomThrowingFunction.kt b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/cache/deps/CustomThrowingFunction.kt new file mode 100644 index 000000000..4e0cc1a61 --- /dev/null +++ b/appserver-jersey/src/main/kotlin/org/radarbase/appserver/jersey/utils/cache/deps/CustomThrowingFunction.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2025 King's College London + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.appserver.jersey.utils.cache.deps + +/** + * Functional interface that represents a function which can throw checked exceptions. + * + * This can be used to define lambda expressions or method references that throw exceptions + * and handle them explicitly where required. + * + * @param T the input type to the function + * @param R the result type of the function + */ +fun interface CustomThrowingFunction { + /** + * Applies the given transformation logic on the input of type T and returns a result of type R. + * + * @param key The input parameter of type T on which the operation is to be applied. + * @return The result of the operation as an instance of type R. + * @throws Exception If an exception occurs during the application of the transformation. + */ + @Throws(Exception::class) + suspend fun applyWithException(key: T): R +} diff --git a/appserver-jersey/src/main/resources/appserver.yml b/appserver-jersey/src/main/resources/appserver.yml new file mode 100644 index 000000000..eb219c412 --- /dev/null +++ b/appserver-jersey/src/main/resources/appserver.yml @@ -0,0 +1,74 @@ +resourceConfig: org.radarbase.appserver.jersey.enhancer.factory.AppserverResourceEnhancerFactory + +# Server configuration. +server: + # Base URI of the application. + baseUri: http://0.0.0.0:8080/ + # Timeout for requests to the application. + requestTimeout: 30 + isJmxEnabled: false + +# Authentication configuration. +auth: + managementPortalUrl: http://localhost:8081/managementportal + resourceName: res_appconfig + +db: + # For Development environment, use H2 database. + # jdbcDriver: org.h2.Driver + # jdbcUrl: jdbc:h2:mem:dev + # hibernateDialect: org.hibernate.dialect.H2Dialect + # liquibase: + # enabled: false + # additionalProperties: + # jakarta.persistence.schema-generation.database.action: drop-and-create + # hibernate.show_sql: true + # hibernate.format_sql: true + jdbcDriver: org.postgresql.Driver + jdbcUrl: jdbc:postgresql://localhost:5432/appserver + hibernateDialect: org.hibernate.dialect.PostgreSQLDialect + username: radar + password: radar + +# GitHub configuration. +github: + # GitHub Cache configuration. + cache: + # Duration in seconds for which the cache is valid. + cacheDurationSec: 3600 + # Duration in seconds after which the cache can be refreshed. + retryDurationSec: 60 + # Maximum number of entries in the cache. + maxCacheSize: 10000 + # GitHub HTTP Client configuration. + client: + maxContentLength: 1000000 + timeoutSec: 10 + githubToken: + +# Quartz Scheduler Configuration. +quartz: + # Specifies the dispatcher for asynchronous operations. + # Options: 'io' (for I/O-bound tasks), 'default' (for CPU-bound tasks), 'unconfined' (uses calling thread). + # Defaults to 'unconfined' if not provided. + coroutineDispatcher: io + + # Specifies the type of coroutine job. + # Options: 'supervisor-job' (supervises child jobs), 'coroutine-job' (no supervision). + # Defaults to 'supervisor-job' if not provided. + coroutineJob: supervisor-job + +protocol: + githubProtocolRepo: RADAR-base/RADAR-aRMT-protocols + protocolFileName: protocol.json + githubBranch: master + +email: + enabled: false + +fcm: + fcmsender: org.radarbase.appserver.jersey.fcm.downstream.AdminSdkFcmSender + credentials: + +eventBus: + numThreads: 3 diff --git a/appserver-jersey/src/main/resources/log4j2.xml b/appserver-jersey/src/main/resources/log4j2.xml new file mode 100644 index 000000000..df5288217 --- /dev/null +++ b/appserver-jersey/src/main/resources/log4j2.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Dockerfile b/appserver-legacy/Dockerfile similarity index 100% rename from Dockerfile rename to appserver-legacy/Dockerfile diff --git a/appserver-legacy/build.gradle.kts b/appserver-legacy/build.gradle.kts new file mode 100644 index 000000000..6d04996ae --- /dev/null +++ b/appserver-legacy/build.gradle.kts @@ -0,0 +1,214 @@ +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.springframework.boot.gradle.tasks.bundling.BootJar + +plugins { + eclipse + scala + kotlin("kapt") + kotlin("plugin.allopen") + kotlin("plugin.noarg") + id("io.gatling.gradle") version Versions.gatlingVersion + id("org.springframework.boot") version Versions.springBootVersion + id("io.spring.dependency-management") version Versions.springDependencyManagementVersion + id("org.radarbase.radar-dependency-management") + id("org.radarbase.radar-kotlin") + id("org.jetbrains.kotlin.plugin.spring") + id("org.jetbrains.kotlin.plugin.jpa") +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +kotlin { + jvmToolchain(17) +} + +kapt { + keepJavacAnnotationProcessors = true +} + +springBoot { + mainClass.set("org.radarbase.appserver.AppserverApplicationKt") +} + +tasks.bootJar { + mainClass.set("org.radarbase.appserver.AppserverApplicationKt") + duplicatesStrategy = DuplicatesStrategy.INCLUDE +} + +tasks.jar { + duplicatesStrategy = DuplicatesStrategy.INCLUDE +} + +sourceSets { + create("integrationTest") { + compileClasspath += sourceSets.main.get().output + sourceSets.test.get().compileClasspath + runtimeClasspath += sourceSets.main.get().output + sourceSets.test.get().runtimeClasspath + } +} +// +val integrationTestImplementation: Configuration by configurations.getting { + extendsFrom(configurations.testImplementation.get()) +} + +val integrationTestRuntimeOnly: Configuration by configurations.getting + +configurations["integrationTestRuntimeOnly"].extendsFrom(configurations.runtimeOnly.get()) + +radarDependencies { + rejectMajorVersionUpdates.set(true) +} + +radarKotlin { + javaVersion.set(Versions.java) + kotlinVersion.set(Versions.kotlinVersion) +// kotlinApiVersion.set(Versions.kotlinVersion) + junitVersion.set(Versions.junit5Version) +} + +// TODO: Need to use a custom gradle plugin defined for this in buildSrc +// integrationTestConfig { +// sourceSetName = "IntegrationTest" +// duplicatesStrategy = DuplicatesStrategy.EXCLUDE +// hookIntoCheck = true +// } + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-quartz") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("org.springframework.boot:spring-boot-starter-mail") + + implementation("org.springframework.security:spring-security-config:${Versions.springSecurityVersion}") + implementation("org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:${Versions.springOauth2AutoconfigureVersion}") + implementation("org.springframework.security.oauth:spring-security-oauth2:${Versions.springOauth2Version}") + + runtimeOnly("org.hibernate.validator:hibernate-validator:${Versions.hibernateValidatorVersion}") + + implementation("io.minio:minio:${Versions.minioVersion}") { + exclude(group = "org.jetbrains.kotlin") + } + + // Open API spec + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:${Versions.springDocVersion}") + + // runtimeOnly("org.springframework.boot:spring-boot-devtools") + runtimeOnly("org.hsqldb:hsqldb") + runtimeOnly("org.liquibase:liquibase-core:4.20.0") + runtimeOnly("org.postgresql:postgresql:42.5.5") + + annotationProcessor("org.projectlombok:lombok:${Versions.lombokVersion}") + implementation("org.projectlombok:lombok:${Versions.lombokVersion}") + + kapt("org.springframework:spring-context-indexer:${Versions.springVersion}") + annotationProcessor("org.springframework:spring-context-indexer:${Versions.springVersion}") + + // FCM Admin SDK + implementation("com.google.firebase:firebase-admin:9.3.0") { + constraints { + implementation("com.google.protobuf:protobuf-java:3.25.5") { + because("Provided version of protobuf has security vulnerabilities") + } + implementation("com.google.protobuf:protobuf-java-util:3.25.5") { + because("Provided version of protobuf has security vulnerabilities") + } + } + } + + // AOP + runtimeOnly("org.springframework:spring-aop:${Versions.springVersion}") + implementation("org.radarbase:radar-spring-auth:${Versions.radarSpringAuthVersion}") + + // Kotlin + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Versions.kotlinVersion}") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:${Versions.jacksonKotlinVersion}") + implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.25") + kapt("org.springframework.boot:spring-boot-configuration-processor") + implementation("io.ktor:ktor-client-core:${Versions.ktorVersion}") + implementation("io.ktor:ktor-client-cio:${Versions.ktorVersion}") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutinesVersion}") + testImplementation("org.mockito.kotlin:mockito-kotlin:${Versions.mockitoKotlinVersion}") + + testImplementation("io.gatling.highcharts:gatling-charts-highcharts:3.9.2") + + implementation("org.liquibase.ext:liquibase-hibernate6:4.20.0") + + testImplementation("org.springframework.boot:spring-boot-starter-test") { + exclude(group = "org.junit", module = "junit") + } + + testImplementation("org.junit.jupiter:junit-jupiter:${Versions.junit5Version}") + testImplementation("org.junit.jupiter:junit-jupiter-api:${Versions.junit5Version}") + testImplementation("org.junit.jupiter:junit-jupiter-engine:${Versions.junit5Version}") + testImplementation("org.junit.platform:junit-platform-commons:1.8.2") + testImplementation("org.junit.platform:junit-platform-launcher:1.8.2") + testImplementation("org.junit.platform:junit-platform-engine:1.8.2") + + gatlingImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") +} + +ktlint { + ignoreFailures.set(true) +} + +noArg { + annotation("org.radarbase.appserver.util.GenerateZeroArgs") +} + +allOpen { + annotation("org.radarbase.appserver.util.OpenClass") + annotation("jakarta.persistence.MappedSuperclass") + annotation("jakarta.persistence.Entity") + annotation("jakarta.persistence.Embeddable") +} + +val integrationTest = task("integrationTest") { + description = "Runs integration tests." + group = "verification" + + testClassesDirs = sourceSets["integrationTest"].output.classesDirs + classpath = sourceSets["integrationTest"].runtimeClasspath + shouldRunAfter("test") + + useJUnitPlatform { + excludeEngines("junit-vintage") + } + + testLogging { + events("passed") + } +} + +tasks.check { dependsOn(integrationTest) } + +tasks.named("processIntegrationTestResources") { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + +tasks.test { + testLogging { + events("failed") + exceptionFormat = TestExceptionFormat.FULL + + error.exceptionFormat = TestExceptionFormat.FULL + } +} + +tasks.javadoc { + (options as CoreJavadocOptions).addBooleanOption("html5", true) + setDestinationDir(layout.projectDirectory.dir("src/main/resources/static/java-docs").asFile) +} + +val bootJarProvider = tasks.named("bootJar") + +tasks.register("unpack") { + dependsOn(bootJarProvider) + from(bootJarProvider.map { zipTree(it.outputs.files.singleFile) }) + into(layout.buildDirectory.dir("dependency")) +} + diff --git a/config/checkstyle/checkstyle.xml b/appserver-legacy/config/checkstyle/checkstyle.xml similarity index 100% rename from config/checkstyle/checkstyle.xml rename to appserver-legacy/config/checkstyle/checkstyle.xml diff --git a/config/codestyles/intellij-java-google-style.xml b/appserver-legacy/config/codestyles/intellij-java-google-style.xml similarity index 100% rename from config/codestyles/intellij-java-google-style.xml rename to appserver-legacy/config/codestyles/intellij-java-google-style.xml diff --git a/images/gatling-results.png b/appserver-legacy/images/gatling-results.png similarity index 100% rename from images/gatling-results.png rename to appserver-legacy/images/gatling-results.png diff --git a/images/java-docs.png b/appserver-legacy/images/java-docs.png similarity index 100% rename from images/java-docs.png rename to appserver-legacy/images/java-docs.png diff --git a/images/swagger-ui.png b/appserver-legacy/images/swagger-ui.png similarity index 100% rename from images/swagger-ui.png rename to appserver-legacy/images/swagger-ui.png diff --git a/radar-is.yml b/appserver-legacy/radar-is.yml similarity index 100% rename from radar-is.yml rename to appserver-legacy/radar-is.yml diff --git a/scripts/.gitignore b/appserver-legacy/scripts/.gitignore similarity index 100% rename from scripts/.gitignore rename to appserver-legacy/scripts/.gitignore diff --git a/scripts/README.md b/appserver-legacy/scripts/README.md similarity index 100% rename from scripts/README.md rename to appserver-legacy/scripts/README.md diff --git a/scripts/clients.py b/appserver-legacy/scripts/clients.py similarity index 100% rename from scripts/clients.py rename to appserver-legacy/scripts/clients.py diff --git a/scripts/hsqldb.jar b/appserver-legacy/scripts/hsqldb.jar similarity index 100% rename from scripts/hsqldb.jar rename to appserver-legacy/scripts/hsqldb.jar diff --git a/scripts/migrate-xmpp-to-appserver.py b/appserver-legacy/scripts/migrate-xmpp-to-appserver.py similarity index 100% rename from scripts/migrate-xmpp-to-appserver.py rename to appserver-legacy/scripts/migrate-xmpp-to-appserver.py diff --git a/scripts/project_mapping.json b/appserver-legacy/scripts/project_mapping.json similarity index 100% rename from scripts/project_mapping.json rename to appserver-legacy/scripts/project_mapping.json diff --git a/scripts/requirements.txt b/appserver-legacy/scripts/requirements.txt similarity index 100% rename from scripts/requirements.txt rename to appserver-legacy/scripts/requirements.txt diff --git a/src/gatling/simulations/org/radarbase/appserver/ApiGatlingSimulationTest.scala b/appserver-legacy/src/gatling/simulations/org/radarbase/appserver/ApiGatlingSimulationTest.scala similarity index 100% rename from src/gatling/simulations/org/radarbase/appserver/ApiGatlingSimulationTest.scala rename to appserver-legacy/src/gatling/simulations/org/radarbase/appserver/ApiGatlingSimulationTest.scala diff --git a/src/gatling/simulations/org/radarbase/appserver/BatchingApiGatlingSimulationTest.scala b/appserver-legacy/src/gatling/simulations/org/radarbase/appserver/BatchingApiGatlingSimulationTest.scala similarity index 100% rename from src/gatling/simulations/org/radarbase/appserver/BatchingApiGatlingSimulationTest.scala rename to appserver-legacy/src/gatling/simulations/org/radarbase/appserver/BatchingApiGatlingSimulationTest.scala diff --git a/src/integrationTest/java/org/radarbase/appserver/auth/NotificationEndpointAuthTest.kt b/appserver-legacy/src/integrationTest/kotlin/org/radarbase/appserver/auth/NotificationEndpointAuthTest.kt similarity index 72% rename from src/integrationTest/java/org/radarbase/appserver/auth/NotificationEndpointAuthTest.kt rename to appserver-legacy/src/integrationTest/kotlin/org/radarbase/appserver/auth/NotificationEndpointAuthTest.kt index 0c62e1b18..a37520379 100644 --- a/src/integrationTest/java/org/radarbase/appserver/auth/NotificationEndpointAuthTest.kt +++ b/appserver-legacy/src/integrationTest/kotlin/org/radarbase/appserver/auth/NotificationEndpointAuthTest.kt @@ -71,30 +71,32 @@ class NotificationEndpointAuthTest { "http://localhost:" + port + ProjectEndpointAuthTest.PROJECT_PATH, HttpMethod.POST, projectEntity, - ProjectDto::class.java + ProjectDto::class.java, ) val userDto: FcmUserDto? = - FcmUserDto().apply { - projectId = "radar" - this.language = "en" - enrolmentDate = Instant.now() - fcmToken = "xxx" - subjectId = "sub-1" - timezone = "Europe/London" - } + FcmUserDto().apply { + projectId = "radar" + this.language = "en" + enrolmentDate = Instant.now() + fcmToken = "xxx" + subjectId = "sub-1" + timezone = "Europe/London" + } val userDtoHttpEntity = HttpEntity(userDto, AUTH_HEADER) restTemplate.exchange( createURLWithPort( port, - (ProjectEndpointAuthTest.PROJECT_PATH - + UserEndpointAuthTest.DEFAULT_PROJECT - + UserEndpointAuthTest.USER_PATH) + ( + ProjectEndpointAuthTest.PROJECT_PATH + + UserEndpointAuthTest.DEFAULT_PROJECT + + UserEndpointAuthTest.USER_PATH + ), ), HttpMethod.POST, userDtoHttpEntity, - FcmUserDto::class.java + FcmUserDto::class.java, ) } @@ -106,15 +108,17 @@ class NotificationEndpointAuthTest { restTemplate.exchange( createURLWithPort( port, - (ProjectEndpointAuthTest.PROJECT_PATH - + UserEndpointAuthTest.DEFAULT_PROJECT - + UserEndpointAuthTest.USER_PATH - + DEFAULT_USER - + "/messaging/notifications") + ( + ProjectEndpointAuthTest.PROJECT_PATH + + UserEndpointAuthTest.DEFAULT_PROJECT + + UserEndpointAuthTest.USER_PATH + + DEFAULT_USER + + "/messaging/notifications" + ), ), HttpMethod.GET, notificationDtoHttpEntity, - FcmNotifications::class.java + FcmNotifications::class.java, ) Assertions.assertEquals(HttpStatus.UNAUTHORIZED, notificationDtoResponseEntity.statusCode) @@ -128,13 +132,15 @@ class NotificationEndpointAuthTest { restTemplate.exchange( createURLWithPort( port, - (ProjectEndpointAuthTest.PROJECT_PATH - + UserEndpointAuthTest.DEFAULT_PROJECT - + "/messaging/notifications") + ( + ProjectEndpointAuthTest.PROJECT_PATH + + UserEndpointAuthTest.DEFAULT_PROJECT + + "/messaging/notifications" + ), ), HttpMethod.GET, notificationDtoHttpEntity, - FcmNotifications::class.java + FcmNotifications::class.java, ) Assertions.assertEquals(HttpStatus.UNAUTHORIZED, notificationDtoResponseEntity.statusCode) @@ -151,15 +157,17 @@ class NotificationEndpointAuthTest { restTemplate.exchange( createURLWithPort( port, - (ProjectEndpointAuthTest.PROJECT_PATH - + UserEndpointAuthTest.DEFAULT_PROJECT - + UserEndpointAuthTest.USER_PATH - + DEFAULT_USER - + NOTIFICATION_PATH) + ( + ProjectEndpointAuthTest.PROJECT_PATH + + UserEndpointAuthTest.DEFAULT_PROJECT + + UserEndpointAuthTest.USER_PATH + + DEFAULT_USER + + NOTIFICATION_PATH + ), ), HttpMethod.POST, notificationDtoHttpEntity, - FcmNotificationDto::class.java + FcmNotificationDto::class.java, ) } catch (e: ResourceAccessException) { Assertions.assertEquals(notificationDtoResponseEntity, null) @@ -176,15 +184,17 @@ class NotificationEndpointAuthTest { restTemplate.exchange( createURLWithPort( port, - (ProjectEndpointAuthTest.PROJECT_PATH - + UserEndpointAuthTest.DEFAULT_PROJECT - + UserEndpointAuthTest.USER_PATH - + DEFAULT_USER - + NOTIFICATION_PATH) + ( + ProjectEndpointAuthTest.PROJECT_PATH + + UserEndpointAuthTest.DEFAULT_PROJECT + + UserEndpointAuthTest.USER_PATH + + DEFAULT_USER + + NOTIFICATION_PATH + ), ), HttpMethod.POST, notificationDtoHttpEntity, - FcmNotificationDto::class.java + FcmNotificationDto::class.java, ) Assertions.assertEquals(HttpStatus.CREATED, notificationDtoResponseEntity.statusCode) @@ -199,25 +209,27 @@ class NotificationEndpointAuthTest { HttpEntity( FcmNotifications() .withNotifications( - listOf(fcmNotificationDto) + listOf(fcmNotificationDto), ), - AUTH_HEADER + AUTH_HEADER, ) val notificationDtoResponseEntity: ResponseEntity = restTemplate.exchange( createURLWithPort( port, - (ProjectEndpointAuthTest.PROJECT_PATH - + UserEndpointAuthTest.DEFAULT_PROJECT - + UserEndpointAuthTest.USER_PATH - + DEFAULT_USER - + NOTIFICATION_PATH - + "/batch") + ( + ProjectEndpointAuthTest.PROJECT_PATH + + UserEndpointAuthTest.DEFAULT_PROJECT + + UserEndpointAuthTest.USER_PATH + + DEFAULT_USER + + NOTIFICATION_PATH + + "/batch" + ), ), HttpMethod.POST, notificationDtoHttpEntity, - FcmNotifications::class.java + FcmNotifications::class.java, ) Assertions.assertEquals(HttpStatus.OK, notificationDtoResponseEntity.statusCode) @@ -231,15 +243,17 @@ class NotificationEndpointAuthTest { restTemplate.exchange( createURLWithPort( port, - (ProjectEndpointAuthTest.PROJECT_PATH - + UserEndpointAuthTest.DEFAULT_PROJECT - + UserEndpointAuthTest.USER_PATH - + DEFAULT_USER - + NOTIFICATION_PATH) + ( + ProjectEndpointAuthTest.PROJECT_PATH + + UserEndpointAuthTest.DEFAULT_PROJECT + + UserEndpointAuthTest.USER_PATH + + DEFAULT_USER + + NOTIFICATION_PATH + ), ), HttpMethod.GET, notificationDtoHttpEntity, - FcmNotifications::class.java + FcmNotifications::class.java, ) Assertions.assertEquals(HttpStatus.OK, notificationDtoResponseEntity.statusCode) @@ -253,13 +267,15 @@ class NotificationEndpointAuthTest { restTemplate.exchange( createURLWithPort( port, - (ProjectEndpointAuthTest.PROJECT_PATH - + UserEndpointAuthTest.DEFAULT_PROJECT - + NOTIFICATION_PATH) + ( + ProjectEndpointAuthTest.PROJECT_PATH + + UserEndpointAuthTest.DEFAULT_PROJECT + + NOTIFICATION_PATH + ), ), HttpMethod.GET, notificationDtoHttpEntity, - FcmNotifications::class.java + FcmNotifications::class.java, ) Assertions.assertEquals(HttpStatus.OK, notificationDtoResponseEntity.statusCode) @@ -273,15 +289,17 @@ class NotificationEndpointAuthTest { restTemplate.exchange( createURLWithPort( port, - (ProjectEndpointAuthTest.PROJECT_PATH - + UserEndpointAuthTest.DEFAULT_PROJECT - + UserEndpointAuthTest.USER_PATH - + "/sub-2" - + NOTIFICATION_PATH) + ( + ProjectEndpointAuthTest.PROJECT_PATH + + UserEndpointAuthTest.DEFAULT_PROJECT + + UserEndpointAuthTest.USER_PATH + + "/sub-2" + + NOTIFICATION_PATH + ), ), HttpMethod.GET, notificationDtoHttpEntity, - FcmNotifications::class.java + FcmNotifications::class.java, ) Assertions.assertEquals(HttpStatus.FORBIDDEN, notificationDtoResponseEntity.statusCode) @@ -294,11 +312,12 @@ class NotificationEndpointAuthTest { val notificationDtoResponseEntity: ResponseEntity = restTemplate.exchange( createURLWithPort( - port, ProjectEndpointAuthTest.PROJECT_PATH + "/test" + NOTIFICATION_PATH + port, + ProjectEndpointAuthTest.PROJECT_PATH + "/test" + NOTIFICATION_PATH, ), HttpMethod.GET, notificationDtoHttpEntity, - FcmNotifications::class.java + FcmNotifications::class.java, ) Assertions.assertEquals(HttpStatus.UNAUTHORIZED, notificationDtoResponseEntity.statusCode) @@ -324,6 +343,5 @@ class NotificationEndpointAuthTest { fun clearDatabase(@Autowired jdbcTemplate: JdbcTemplate) { JdbcTestUtils.deleteFromTables(jdbcTemplate, "notifications", "users", "projects") } - } -} \ No newline at end of file +} diff --git a/src/integrationTest/java/org/radarbase/appserver/auth/ProjectEndpointAuthTest.kt b/appserver-legacy/src/integrationTest/kotlin/org/radarbase/appserver/auth/ProjectEndpointAuthTest.kt similarity index 77% rename from src/integrationTest/java/org/radarbase/appserver/auth/ProjectEndpointAuthTest.kt rename to appserver-legacy/src/integrationTest/kotlin/org/radarbase/appserver/auth/ProjectEndpointAuthTest.kt index 4ed003643..b9083d939 100644 --- a/src/integrationTest/java/org/radarbase/appserver/auth/ProjectEndpointAuthTest.kt +++ b/appserver-legacy/src/integrationTest/kotlin/org/radarbase/appserver/auth/ProjectEndpointAuthTest.kt @@ -46,7 +46,7 @@ class ProjectEndpointAuthTest { @Test fun unauthorisedCreateProject() { - val projectDto = ProjectDto( projectId = "radar") + val projectDto = ProjectDto(projectId = "radar") val projectEntity = HttpEntity(projectDto, HEADERS) var responseEntity: ResponseEntity? = null @@ -56,7 +56,7 @@ class ProjectEndpointAuthTest { createURLWithPort(port, PROJECT_PATH), HttpMethod.POST, projectEntity, - ProjectDto::class.java + ProjectDto::class.java, ) assertEquals(HttpStatus.UNAUTHORIZED, responseEntity.statusCode) @@ -71,7 +71,10 @@ class ProjectEndpointAuthTest { val responseEntity: ResponseEntity = restTemplate.exchange( - createURLWithPort(port, PROJECT_PATH), HttpMethod.GET, projectEntity, String::class.java + createURLWithPort(port, PROJECT_PATH), + HttpMethod.GET, + projectEntity, + String::class.java, ) assertEquals(HttpStatus.UNAUTHORIZED, responseEntity.statusCode) } @@ -85,7 +88,7 @@ class ProjectEndpointAuthTest { createURLWithPort(port, "/projects/radar"), HttpMethod.GET, projectEntity, - ProjectDto::class.java + ProjectDto::class.java, ) assertEquals(HttpStatus.UNAUTHORIZED, responseEntity.statusCode) } @@ -96,7 +99,10 @@ class ProjectEndpointAuthTest { val responseEntity: ResponseEntity = restTemplate.exchange( - createURLWithPort(port, PROJECT_PATH), HttpMethod.GET, projectEntity, String::class.java + createURLWithPort(port, PROJECT_PATH), + HttpMethod.GET, + projectEntity, + String::class.java, ) // Only Admins can view the list of all projects @@ -114,7 +120,7 @@ class ProjectEndpointAuthTest { createURLWithPort(port, PROJECT_PATH), HttpMethod.POST, projectEntity, - ProjectDto::class.java + ProjectDto::class.java, ) if (responseEntity.statusCode == HttpStatus.EXPECTATION_FAILED) { @@ -128,42 +134,42 @@ class ProjectEndpointAuthTest { @Order(2) @Test fun getSingleProjectWithAuth() { - val projectEntity = HttpEntity(AUTH_HEADER) - - val responseEntity: ResponseEntity = - restTemplate.exchange( - createURLWithPort(port, "/projects/radar"), - HttpMethod.GET, - projectEntity, - ProjectDto::class.java - ) + val projectEntity = HttpEntity(AUTH_HEADER) - assertEquals( - HttpStatus.OK, - responseEntity.statusCode + val responseEntity: ResponseEntity = + restTemplate.exchange( + createURLWithPort(port, "/projects/radar"), + HttpMethod.GET, + projectEntity, + ProjectDto::class.java, ) - } + + assertEquals( + HttpStatus.OK, + responseEntity.statusCode, + ) + } @Order(3) @Test fun getForbiddenProjectWithAuth() { - val projectEntity = - HttpEntity(null, AUTH_HEADER) + val projectEntity = + HttpEntity(null, AUTH_HEADER) - val responseEntity: ResponseEntity = - restTemplate.exchange( - createURLWithPort(port, "/projects/test"), - HttpMethod.GET, - projectEntity, - ProjectDto::class.java - ) + val responseEntity: ResponseEntity = + restTemplate.exchange( + createURLWithPort(port, "/projects/test"), + HttpMethod.GET, + projectEntity, + ProjectDto::class.java, + ) - // Access denied as the user has only access to the project that it is part of. + // Access denied as the user has only access to the project that it is part of. assertEquals( HttpStatus.FORBIDDEN, - responseEntity.statusCode + responseEntity.statusCode, ) - } + } companion object { const val PROJECT_PATH: String = "/projects" @@ -182,4 +188,4 @@ class ProjectEndpointAuthTest { return "http://localhost:$port$uri" } } -} \ No newline at end of file +} diff --git a/src/integrationTest/java/org/radarbase/appserver/auth/UserEndpointAuthTest.kt b/appserver-legacy/src/integrationTest/kotlin/org/radarbase/appserver/auth/UserEndpointAuthTest.kt similarity index 84% rename from src/integrationTest/java/org/radarbase/appserver/auth/UserEndpointAuthTest.kt rename to appserver-legacy/src/integrationTest/kotlin/org/radarbase/appserver/auth/UserEndpointAuthTest.kt index d13cc6962..a5d2e089f 100644 --- a/src/integrationTest/java/org/radarbase/appserver/auth/UserEndpointAuthTest.kt +++ b/appserver-legacy/src/integrationTest/kotlin/org/radarbase/appserver/auth/UserEndpointAuthTest.kt @@ -15,10 +15,6 @@ package org.radarbase.appserver.auth -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.SerializationFeature -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import org.junit.jupiter.api.* import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.MethodOrderer.OrderAnnotation @@ -30,16 +26,13 @@ import org.radarbase.appserver.dto.fcm.FcmUsers import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.web.client.TestRestTemplate import org.springframework.boot.test.web.server.LocalServerPort -import org.springframework.boot.web.client.RestTemplateBuilder import org.springframework.http.HttpEntity import org.springframework.http.HttpHeaders import org.springframework.http.HttpMethod import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter import org.springframework.test.context.junit.jupiter.SpringExtension import org.springframework.web.client.ResourceAccessException -import org.springframework.web.client.RestTemplate @ExtendWith(SpringExtension::class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @@ -67,7 +60,7 @@ class UserEndpointAuthTest { createURLWithPort(port, ProjectEndpointAuthTest.PROJECT_PATH), HttpMethod.POST, projectEntity, - ProjectDto::class.java + ProjectDto::class.java, ) } @@ -77,10 +70,10 @@ class UserEndpointAuthTest { val responseEntity = restTemplate.exchange( createURLWithPort(port, ProjectEndpointAuthTest.PROJECT_PATH) + - DEFAULT_PROJECT + USER_PATH + "/sub-1", + DEFAULT_PROJECT + USER_PATH + "/sub-1", HttpMethod.GET, userDtoHttpEntity, - FcmUserDto::class.java + FcmUserDto::class.java, ) assertEquals(HttpStatus.UNAUTHORIZED, responseEntity.statusCode) @@ -93,11 +86,11 @@ class UserEndpointAuthTest { try { responseEntity = restTemplate.exchange( createURLWithPort( - port, ProjectEndpointAuthTest.PROJECT_PATH + DEFAULT_PROJECT + USER_PATH + port, ProjectEndpointAuthTest.PROJECT_PATH + DEFAULT_PROJECT + USER_PATH, ), HttpMethod.POST, userDtoHttpEntity, - FcmUserDto::class.java + FcmUserDto::class.java, ) } catch (e: ResourceAccessException) { assertEquals(responseEntity, null) @@ -111,11 +104,12 @@ class UserEndpointAuthTest { val responseEntity = restTemplate.exchange( createURLWithPort( - port, ProjectEndpointAuthTest.PROJECT_PATH + DEFAULT_PROJECT + USER_PATH + port, + ProjectEndpointAuthTest.PROJECT_PATH + DEFAULT_PROJECT + USER_PATH, ), HttpMethod.POST, userDtoHttpEntity, - FcmUserDto::class.java + FcmUserDto::class.java, ) if (responseEntity.statusCode == HttpStatus.EXPECTATION_FAILED) { @@ -133,11 +127,11 @@ class UserEndpointAuthTest { val responseEntity = restTemplate.exchange( createURLWithPort( port, - ProjectEndpointAuthTest.PROJECT_PATH + DEFAULT_PROJECT + USER_PATH + "/sub-1" + ProjectEndpointAuthTest.PROJECT_PATH + DEFAULT_PROJECT + USER_PATH + "/sub-1", ), HttpMethod.GET, userDtoHttpEntity, - FcmUserDto::class.java + FcmUserDto::class.java, ) assertEquals(HttpStatus.OK, responseEntity.statusCode) @@ -150,11 +144,12 @@ class UserEndpointAuthTest { val responseEntity = restTemplate.exchange( createURLWithPort( - port, ProjectEndpointAuthTest.PROJECT_PATH + DEFAULT_PROJECT + USER_PATH + port, + ProjectEndpointAuthTest.PROJECT_PATH + DEFAULT_PROJECT + USER_PATH, ), HttpMethod.GET, userDtoHttpEntity, - FcmUsers::class.java + FcmUsers::class.java, ) assertEquals(HttpStatus.OK, responseEntity.statusCode) @@ -169,7 +164,7 @@ class UserEndpointAuthTest { createURLWithPort(port, ProjectEndpointAuthTest.PROJECT_PATH + "/test" + USER_PATH), HttpMethod.GET, userDtoHttpEntity, - String::class.java + String::class.java, ) assertEquals(HttpStatus.FORBIDDEN, responseEntity.statusCode) @@ -181,7 +176,10 @@ class UserEndpointAuthTest { val userDtoHttpEntity = HttpEntity(null, AUTH_HEADER) val responseEntity = restTemplate.exchange( - createURLWithPort(port, USER_PATH), HttpMethod.GET, userDtoHttpEntity, FcmUsers::class.java + createURLWithPort(port, USER_PATH), + HttpMethod.GET, + userDtoHttpEntity, + FcmUsers::class.java, ) assertEquals(HttpStatus.OK, responseEntity.statusCode) diff --git a/src/integrationTest/java/org/radarbase/appserver/auth/common/MPOAuthHelper.kt b/appserver-legacy/src/integrationTest/kotlin/org/radarbase/appserver/auth/common/MPOAuthHelper.kt similarity index 99% rename from src/integrationTest/java/org/radarbase/appserver/auth/common/MPOAuthHelper.kt rename to appserver-legacy/src/integrationTest/kotlin/org/radarbase/appserver/auth/common/MPOAuthHelper.kt index 278428b15..893997670 100644 --- a/src/integrationTest/java/org/radarbase/appserver/auth/common/MPOAuthHelper.kt +++ b/appserver-legacy/src/integrationTest/kotlin/org/radarbase/appserver/auth/common/MPOAuthHelper.kt @@ -102,7 +102,7 @@ class MPOAuthHelper : OAuthHelper { private fun getProperty(response: ResponseEntity, property: String): String { if (response.statusCode.isError) { - throw IllegalStateException("The request was not successful: ${response.toString()}") + throw IllegalStateException("The request was not successful: $response") } return try { val root: JsonNode = mapper.readTree(response.body) @@ -115,4 +115,3 @@ class MPOAuthHelper : OAuthHelper { override fun getAccessToken(): String = ACCESS_TOKEN } - diff --git a/src/integrationTest/java/org/radarbase/appserver/auth/common/OAuthHelper.kt b/appserver-legacy/src/integrationTest/kotlin/org/radarbase/appserver/auth/common/OAuthHelper.kt similarity index 95% rename from src/integrationTest/java/org/radarbase/appserver/auth/common/OAuthHelper.kt rename to appserver-legacy/src/integrationTest/kotlin/org/radarbase/appserver/auth/common/OAuthHelper.kt index 00f629d6e..984a9e317 100644 --- a/src/integrationTest/java/org/radarbase/appserver/auth/common/OAuthHelper.kt +++ b/appserver-legacy/src/integrationTest/kotlin/org/radarbase/appserver/auth/common/OAuthHelper.kt @@ -21,5 +21,5 @@ package org.radarbase.appserver.auth.common interface OAuthHelper { - fun getAccessToken(): String + fun getAccessToken(): String } diff --git a/src/integrationTest/resources/application.properties b/appserver-legacy/src/integrationTest/resources/application.properties similarity index 96% rename from src/integrationTest/resources/application.properties rename to appserver-legacy/src/integrationTest/resources/application.properties index 23ca2c012..90b14a787 100644 --- a/src/integrationTest/resources/application.properties +++ b/appserver-legacy/src/integrationTest/resources/application.properties @@ -22,9 +22,9 @@ server.port=8080 # Datasource spring.jpa.hibernate.ddl-auto=none -spring.datasource.username=postgres -spring.datasource.password=radar -spring.datasource.url=jdbc:postgresql://localhost:5432/radar +spring.datasource.username=appserveruser +spring.datasource.password=appserverpwd +spring.datasource.url=jdbc:postgresql://localhost:5432/appserverdb # jdbc:hsqldb:hsql://localhost:9001/appserver for running hsql separately in dev or testing spring.jpa.properties.hibernate.jdbc.time_zone=UTC spring.jpa.properties.hibernate.generate_statistics=false diff --git a/src/integrationTest/resources/docker/appserver.yml b/appserver-legacy/src/integrationTest/resources/docker/appserver.yml similarity index 100% rename from src/integrationTest/resources/docker/appserver.yml rename to appserver-legacy/src/integrationTest/resources/docker/appserver.yml diff --git a/src/integrationTest/resources/docker/appserver_dockerhub/docker-compose.yml b/appserver-legacy/src/integrationTest/resources/docker/appserver_dockerhub/docker-compose.yml similarity index 100% rename from src/integrationTest/resources/docker/appserver_dockerhub/docker-compose.yml rename to appserver-legacy/src/integrationTest/resources/docker/appserver_dockerhub/docker-compose.yml diff --git a/src/integrationTest/resources/docker/appserver_downstream/docker-compose.override.yml b/appserver-legacy/src/integrationTest/resources/docker/appserver_downstream/docker-compose.override.yml similarity index 100% rename from src/integrationTest/resources/docker/appserver_downstream/docker-compose.override.yml rename to appserver-legacy/src/integrationTest/resources/docker/appserver_downstream/docker-compose.override.yml diff --git a/src/integrationTest/resources/docker/appserver_downstream/docker-compose.yml b/appserver-legacy/src/integrationTest/resources/docker/appserver_downstream/docker-compose.yml similarity index 100% rename from src/integrationTest/resources/docker/appserver_downstream/docker-compose.yml rename to appserver-legacy/src/integrationTest/resources/docker/appserver_downstream/docker-compose.yml diff --git a/src/integrationTest/resources/docker/appserver_local/docker-compose.yml b/appserver-legacy/src/integrationTest/resources/docker/appserver_local/docker-compose.yml similarity index 100% rename from src/integrationTest/resources/docker/appserver_local/docker-compose.yml rename to appserver-legacy/src/integrationTest/resources/docker/appserver_local/docker-compose.yml diff --git a/appserver-legacy/src/integrationTest/resources/docker/etc/config/keystore.p12 b/appserver-legacy/src/integrationTest/resources/docker/etc/config/keystore.p12 new file mode 100644 index 000000000..f1cdeb82a Binary files /dev/null and b/appserver-legacy/src/integrationTest/resources/docker/etc/config/keystore.p12 differ diff --git a/appserver-legacy/src/integrationTest/resources/docker/etc/config/oauth_client_details.csv b/appserver-legacy/src/integrationTest/resources/docker/etc/config/oauth_client_details.csv new file mode 100644 index 000000000..79e5149e3 --- /dev/null +++ b/appserver-legacy/src/integrationTest/resources/docker/etc/config/oauth_client_details.csv @@ -0,0 +1,8 @@ +client_id;resource_ids;client_secret;scope;authorized_grant_types;redirect_uri;authorities;access_token_validity;refresh_token_validity;additional_information;autoapprove +pRMT;res_ManagementPortal,res_gateway,res_AppServer;;MEASUREMENT.CREATE,SUBJECT.UPDATE,SUBJECT.READ,PROJECT.READ,SOURCETYPE.READ,SOURCE.READ,SOURCETYPE.READ,SOURCEDATA.READ,USER.READ,ROLE.READ;refresh_token,authorization_code;;;43200;7948800;{"dynamic_registration": true}; +aRMT;res_ManagementPortal,res_gateway,res_AppServer;;MEASUREMENT.CREATE,SUBJECT.UPDATE,SUBJECT.READ,PROJECT.READ,SOURCETYPE.READ,SOURCE.READ,SOURCETYPE.READ,SOURCEDATA.READ,USER.READ,ROLE.READ;refresh_token,authorization_code;;;43200;7948800;{"dynamic_registration": true}; +THINC-IT;res_ManagementPortal,res_gateway;secret;MEASUREMENT.CREATE,SUBJECT.UPDATE,SUBJECT.READ,PROJECT.READ,SOURCETYPE.READ,SOURCE.READ,SOURCETYPE.READ,SOURCEDATA.READ,USER.READ,ROLE.READ;refresh_token,authorization_code;;;43200;7948800;{"dynamic_registration": true}; +radar_restapi;res_ManagementPortal;secret;SUBJECT.READ,PROJECT.READ,SOURCE.READ,SOURCETYPE.READ;password,client_credentials;;;43200;259200;{}; +radar_redcap_integrator;res_ManagementPortal;secret;PROJECT.READ,SUBJECT.CREATE,SUBJECT.READ,SUBJECT.UPDATE;client_credentials;;;43200;259200;{}; +radar_dashboard;res_ManagementPortal,res_RestApi;secret;SUBJECT.READ,PROJECT.READ,SOURCE.READ,SOURCETYPE.READ,MEASUREMENT.READ;client_credentials;;;43200;259200;{}; +radar_appserver_client;res_ManagementPortal,res_AppServer;;MEASUREMENT.CREATE,SUBJECT.UPDATE,SUBJECT.READ,PROJECT.READ,SOURCETYPE.READ,SOURCE.READ,SOURCETYPE.READ,SOURCEDATA.READ,USER.READ,ROLE.READ;client_credentials;;;43200;259200;{}; \ No newline at end of file diff --git a/src/integrationTest/resources/docker/managementportal-postgresql.yml b/appserver-legacy/src/integrationTest/resources/docker/managementportal-postgresql.yml similarity index 100% rename from src/integrationTest/resources/docker/managementportal-postgresql.yml rename to appserver-legacy/src/integrationTest/resources/docker/managementportal-postgresql.yml diff --git a/src/integrationTest/resources/docker/managementportal.yml b/appserver-legacy/src/integrationTest/resources/docker/managementportal.yml similarity index 100% rename from src/integrationTest/resources/docker/managementportal.yml rename to appserver-legacy/src/integrationTest/resources/docker/managementportal.yml diff --git a/src/integrationTest/resources/docker/non_appserver/docker-compose.yml b/appserver-legacy/src/integrationTest/resources/docker/non_appserver/docker-compose.yml similarity index 74% rename from src/integrationTest/resources/docker/non_appserver/docker-compose.yml rename to appserver-legacy/src/integrationTest/resources/docker/non_appserver/docker-compose.yml index 175bc701e..e52f5ecc7 100644 --- a/src/integrationTest/resources/docker/non_appserver/docker-compose.yml +++ b/appserver-legacy/src/integrationTest/resources/docker/non_appserver/docker-compose.yml @@ -14,23 +14,23 @@ networks: internal: true services: - postgres: - extends: - file: ../postgres.yml - service: postgres - networks: - - db - - default - ports: - - "5432:5432" - - spring-boot-admin: - extends: - file: ../spring-boot-admin.yml - service: spring-boot-admin - networks: - - admin - - default +# postgres: +# extends: +# file: ../postgres.yml +# service: postgres +# networks: +# - db +# - default +# ports: +# - "5432:5432" +# +# spring-boot-admin: +# extends: +# file: ../spring-boot-admin.yml +# service: spring-boot-admin +# networks: +# - admin +# - default #---------------------------------------------------------------------------# # Management Portal # diff --git a/src/integrationTest/resources/docker/postgres.yml b/appserver-legacy/src/integrationTest/resources/docker/postgres.yml similarity index 100% rename from src/integrationTest/resources/docker/postgres.yml rename to appserver-legacy/src/integrationTest/resources/docker/postgres.yml diff --git a/src/integrationTest/resources/docker/spring-boot-admin.yml b/appserver-legacy/src/integrationTest/resources/docker/spring-boot-admin.yml similarity index 100% rename from src/integrationTest/resources/docker/spring-boot-admin.yml rename to appserver-legacy/src/integrationTest/resources/docker/spring-boot-admin.yml diff --git a/src/integrationTest/resources/google-credentials.enc.gpg b/appserver-legacy/src/integrationTest/resources/google-credentials.enc.gpg similarity index 100% rename from src/integrationTest/resources/google-credentials.enc.gpg rename to appserver-legacy/src/integrationTest/resources/google-credentials.enc.gpg diff --git a/src/integrationTest/resources/radar-is.yml b/appserver-legacy/src/integrationTest/resources/radar-is.yml similarity index 100% rename from src/integrationTest/resources/radar-is.yml rename to appserver-legacy/src/integrationTest/resources/radar-is.yml diff --git a/src/main/java/org/radarbase/appserver/AppserverApplication.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/AppserverApplication.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/AppserverApplication.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/AppserverApplication.kt index 60d2bf1c1..da663fd53 100644 --- a/src/main/java/org/radarbase/appserver/AppserverApplication.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/AppserverApplication.kt @@ -28,4 +28,4 @@ class AppserverApplication fun main(args: Array) { SpringApplication.run(AppserverApplication::class.java, *args) -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/config/ApplicationConfig.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/config/ApplicationConfig.kt similarity index 98% rename from src/main/java/org/radarbase/appserver/config/ApplicationConfig.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/config/ApplicationConfig.kt index a01e8049c..4badc8e48 100644 --- a/src/main/java/org/radarbase/appserver/config/ApplicationConfig.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/config/ApplicationConfig.kt @@ -34,4 +34,4 @@ import org.springframework.transaction.annotation.EnableTransactionManagement @EnableTransactionManagement @EnableAsync @EnableScheduling -class ApplicationConfig +class ApplicationConfig diff --git a/src/main/java/org/radarbase/appserver/config/AuthConfig.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/config/AuthConfig.kt similarity index 91% rename from src/main/java/org/radarbase/appserver/config/AuthConfig.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/config/AuthConfig.kt index a775037ff..8ecd924c4 100644 --- a/src/main/java/org/radarbase/appserver/config/AuthConfig.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/config/AuthConfig.kt @@ -49,9 +49,9 @@ class AuthConfig { @Bean fun getAuthProperties(): ManagementPortalAuthProperties { - val validatorConfig = TokenVerifierPublicKeyConfig.readFromFileOrClasspath() - return ManagementPortalAuthProperties(baseUrl!!, resourceName!!, validatorConfig.publicKeyEndpoints) - } + val validatorConfig = TokenVerifierPublicKeyConfig.readFromFileOrClasspath() + return ManagementPortalAuthProperties(baseUrl!!, resourceName!!, validatorConfig.publicKeyEndpoints) + } /** * First tries to load config from radar-is.yml config file. If any issues, then uses the default @@ -62,7 +62,7 @@ class AuthConfig { */ @Bean fun getAuthValidator( - @Autowired managementPortalAuthProperties: ManagementPortalAuthProperties + @Autowired managementPortalAuthProperties: ManagementPortalAuthProperties, ): AuthValidator { return ManagementPortalAuthValidator(managementPortalAuthProperties) } @@ -75,7 +75,7 @@ class AuthConfig { @Bean fun getAuthAspect( @Autowired authValidator: AuthValidator, - @Autowired authorization: Authorization + @Autowired authorization: Authorization, ): AuthAspect { return AuthAspect(authValidator, authorization) } @@ -96,4 +96,4 @@ class AuthConfig { const val UPDATE: String = "UPDATE" } } -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/config/MailAutoconfigureExcludeConfig.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/config/MailAutoconfigureExcludeConfig.kt similarity index 89% rename from src/main/java/org/radarbase/appserver/config/MailAutoconfigureExcludeConfig.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/config/MailAutoconfigureExcludeConfig.kt index 3b89e26f6..fd5cc9491 100644 --- a/src/main/java/org/radarbase/appserver/config/MailAutoconfigureExcludeConfig.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/config/MailAutoconfigureExcludeConfig.kt @@ -30,7 +30,8 @@ import org.springframework.context.annotation.Configuration @Configuration @ConditionalOnProperty(value = ["radar.notification.email.enabled"], havingValue = "false", matchIfMissing = true) @EnableAutoConfiguration( - exclude = [MailSenderAutoConfiguration::class, MailSenderValidatorAutoConfiguration::class - ] + exclude = [ + MailSenderAutoConfiguration::class, MailSenderValidatorAutoConfiguration::class, + ], ) -class MailAutoconfigureExcludeConfig +class MailAutoconfigureExcludeConfig diff --git a/src/main/java/org/radarbase/appserver/config/MultiHttpSecurityConfig.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/config/MultiHttpSecurityConfig.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/config/MultiHttpSecurityConfig.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/config/MultiHttpSecurityConfig.kt index dfb186a87..beab04a72 100644 --- a/src/main/java/org/radarbase/appserver/config/MultiHttpSecurityConfig.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/config/MultiHttpSecurityConfig.kt @@ -59,7 +59,7 @@ class MultiHttpSecurityConfig { .password(adminPassword) .roles("ADMIN") .authorities("ROLE_SYS_ADMIN") - .build() + .build(), ) return manager } diff --git a/src/main/java/org/radarbase/appserver/config/S3StorageProperties.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/config/S3StorageProperties.kt similarity index 93% rename from src/main/java/org/radarbase/appserver/config/S3StorageProperties.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/config/S3StorageProperties.kt index 52a51daa7..ca4ce318f 100644 --- a/src/main/java/org/radarbase/appserver/config/S3StorageProperties.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/config/S3StorageProperties.kt @@ -26,10 +26,10 @@ data class S3StorageProperties( var accessKey: String? = null, var secretKey: String? = null, var bucketName: String? = null, - var path: Path = Path() + var path: Path = Path(), ) { data class Path( var prefix: String? = null, - var collectPerDay: Boolean = false + var collectPerDay: Boolean = false, ) } diff --git a/src/main/java/org/radarbase/appserver/config/SchedulerConfig.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/config/SchedulerConfig.kt similarity index 100% rename from src/main/java/org/radarbase/appserver/config/SchedulerConfig.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/config/SchedulerConfig.kt diff --git a/src/main/java/org/radarbase/appserver/config/TokenVerifierPublicKeyConfig.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/config/TokenVerifierPublicKeyConfig.kt similarity index 100% rename from src/main/java/org/radarbase/appserver/config/TokenVerifierPublicKeyConfig.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/config/TokenVerifierPublicKeyConfig.kt diff --git a/src/main/java/org/radarbase/appserver/controller/FcmDataMessageController.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/FcmDataMessageController.kt similarity index 60% rename from src/main/java/org/radarbase/appserver/controller/FcmDataMessageController.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/FcmDataMessageController.kt index c0e165248..a7f8aa02f 100644 --- a/src/main/java/org/radarbase/appserver/controller/FcmDataMessageController.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/FcmDataMessageController.kt @@ -27,7 +27,15 @@ import org.radarbase.appserver.dto.fcm.FcmDataMessageDto import org.radarbase.appserver.dto.fcm.FcmDataMessages import org.radarbase.appserver.service.FcmDataMessageService import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.* +import org.springframework.web.bind.annotation.CrossOrigin +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController import radar.spring.auth.common.Authorized import radar.spring.auth.common.PermissionOn import java.net.URI @@ -64,142 +72,163 @@ class FcmDataMessageController(private val dataMessageService: FcmDataMessageSer @RequestParam(value = "ttlSeconds", required = false) @Valid ttlSeconds: Int, @RequestParam(value = "startTime", required = false) @Valid startTime: LocalDateTime, @RequestParam(value = "endTime", required = false) @Valid endTime: LocalDateTime, - @RequestParam(value = "limit", required = false) @Valid limit: Int + @RequestParam(value = "limit", required = false) @Valid limit: Int, ): ResponseEntity { return ResponseEntity.ok( this.dataMessageService.getFilteredDataMessages( - type, delivered, ttlSeconds, startTime, endTime, limit - ) + type, + delivered, + ttlSeconds, + startTime, + endTime, + limit, + ), ) } @Authorized(permission = AuthPermissions.READ, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.SUBJECT) @GetMapping( - value = [("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT - + "/" - + PathsUtil.MESSAGING_DATA_PATH)] + value = [ + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + + "/" + + PathsUtil.MESSAGING_DATA_PATH + ), + ], ) fun getDataMessagesUsingProjectIdAndSubjectId( - @PathVariable @Valid projectId: String, @PathVariable @Valid subjectId: String + @PathVariable @Valid projectId: String, + @PathVariable @Valid subjectId: String, ): ResponseEntity { return ResponseEntity.ok( - this.dataMessageService.getDataMessagesByProjectIdAndSubjectId(projectId, subjectId) + this.dataMessageService.getDataMessagesByProjectIdAndSubjectId(projectId, subjectId), ) } @Authorized(permission = AuthPermissions.READ, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.PROJECT) @GetMapping( - ("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.MESSAGING_DATA_PATH) + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.MESSAGING_DATA_PATH + ), ) fun getDataMessagesUsingProjectId( - @PathVariable @Valid projectId: String + @PathVariable @Valid projectId: String, ): ResponseEntity { return ResponseEntity.ok(this.dataMessageService.getDataMessagesByProjectId(projectId)) } @Authorized(permission = AuthPermissions.UPDATE, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.SUBJECT) @PostMapping( - ("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT - + "/" - + PathsUtil.MESSAGING_DATA_PATH) + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + + "/" + + PathsUtil.MESSAGING_DATA_PATH + ), ) @Throws(URISyntaxException::class) fun addSingleDataMessage( @PathVariable projectId: String, @PathVariable subjectId: String, - @RequestBody @Valid dataMessage: FcmDataMessageDto + @RequestBody @Valid dataMessage: FcmDataMessageDto, ): ResponseEntity { val dataMessageDto = this.dataMessageService.addDataMessage(dataMessage, subjectId, projectId) return ResponseEntity.created( - URI("/" + PathsUtil.MESSAGING_DATA_PATH + "/" + dataMessageDto.id) + URI("/" + PathsUtil.MESSAGING_DATA_PATH + "/" + dataMessageDto.id), ) .body(dataMessageDto) } @Authorized(permission = AuthPermissions.UPDATE, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.SUBJECT) @PostMapping( - ("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT - + "/" - + PathsUtil.MESSAGING_DATA_PATH - + "/batch") + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + + "/" + + PathsUtil.MESSAGING_DATA_PATH + + "/batch" + ), ) fun addBatchDataMessages( @PathVariable projectId: String, @PathVariable subjectId: String, - @RequestBody @Valid dataMessages: FcmDataMessages + @RequestBody @Valid dataMessages: FcmDataMessages, ): ResponseEntity { return ResponseEntity.ok( - this.dataMessageService.addDataMessages(dataMessages, subjectId, projectId) + this.dataMessageService.addDataMessages(dataMessages, subjectId, projectId), ) } @Authorized(permission = AuthPermissions.UPDATE, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.SUBJECT) @PutMapping( - ("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT - + "/" - + PathsUtil.MESSAGING_DATA_PATH) + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + + "/" + + PathsUtil.MESSAGING_DATA_PATH + ), ) fun updateDataMessage( @PathVariable projectId: String, @PathVariable subjectId: String, - @RequestBody @Valid dataMessage: FcmDataMessageDto + @RequestBody @Valid dataMessage: FcmDataMessageDto, ): ResponseEntity { return ResponseEntity.ok( - this.dataMessageService.updateDataMessage(dataMessage, subjectId, projectId) + this.dataMessageService.updateDataMessage(dataMessage, subjectId, projectId), ) } @Authorized(permission = AuthPermissions.UPDATE, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.SUBJECT) @DeleteMapping( - ("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT - + "/" - + PathsUtil.MESSAGING_DATA_PATH - + "/" - + PathsUtil.ALL_KEYWORD) + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + + "/" + + PathsUtil.MESSAGING_DATA_PATH + + "/" + + PathsUtil.ALL_KEYWORD + ), ) fun deleteDataMessagesForUser( - @PathVariable projectId: String, @PathVariable subjectId: String + @PathVariable projectId: String, + @PathVariable subjectId: String, ): ResponseEntity { this.dataMessageService.removeDataMessagesForUser(projectId, subjectId) return ResponseEntity.ok().build() @@ -207,23 +236,29 @@ class FcmDataMessageController(private val dataMessageService: FcmDataMessageSer @Authorized(permission = AuthPermissions.UPDATE, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.SUBJECT) @DeleteMapping( - ("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT - + "/" - + PathsUtil.MESSAGING_DATA_PATH - + "/{id}") + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + + "/" + + PathsUtil.MESSAGING_DATA_PATH + + "/{id}" + ), ) fun deleteDataMessageUsingProjectIdAndSubjectIdAndDataMessageId( - @PathVariable projectId: String, @PathVariable subjectId: String, @PathVariable id: Long + @PathVariable projectId: String, + @PathVariable subjectId: String, + @PathVariable id: Long, ): ResponseEntity { this.dataMessageService.deleteDataMessageByProjectIdAndSubjectIdAndDataMessageId( - projectId, subjectId, id + projectId, + subjectId, + id, ) return ResponseEntity.ok().build() } diff --git a/src/main/java/org/radarbase/appserver/controller/FcmNotificationController.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/FcmNotificationController.kt similarity index 56% rename from src/main/java/org/radarbase/appserver/controller/FcmNotificationController.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/FcmNotificationController.kt index 4e756f1a4..061c79a6b 100644 --- a/src/main/java/org/radarbase/appserver/controller/FcmNotificationController.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/FcmNotificationController.kt @@ -27,7 +27,15 @@ import org.radarbase.appserver.dto.fcm.FcmNotificationDto import org.radarbase.appserver.dto.fcm.FcmNotifications import org.radarbase.appserver.service.FcmNotificationService import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.* +import org.springframework.web.bind.annotation.CrossOrigin +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController import radar.spring.auth.common.Authorized import radar.spring.auth.common.PermissionOn import java.net.URI @@ -45,7 +53,7 @@ class FcmNotificationController(private val notificationService: FcmNotification @GetMapping("/" + PathsUtil.MESSAGING_NOTIFICATION_PATH) @Authorized(permission = AuthPermissions.READ, entity = AuthEntities.PROJECT) - fun getAllNotifications(): ResponseEntity { + fun getAllNotifications(): ResponseEntity { return ResponseEntity.ok(this.notificationService.getAllNotifications()) } @@ -64,195 +72,220 @@ class FcmNotificationController(private val notificationService: FcmNotification @RequestParam(value = "ttlSeconds", required = false) @Valid ttlSeconds: Int, @RequestParam(value = "startTime", required = false) @Valid startTime: LocalDateTime?, @RequestParam(value = "endTime", required = false) @Valid endTime: LocalDateTime?, - @RequestParam(value = "limit", required = false) @Valid limit: Int + @RequestParam(value = "limit", required = false) @Valid limit: Int, ): ResponseEntity { return ResponseEntity.ok( this.notificationService.getFilteredNotifications( - type, delivered, ttlSeconds, startTime, endTime, limit - ) + type, + delivered, + ttlSeconds, + startTime, + endTime, + limit, + ), ) } @Authorized(permission = AuthPermissions.READ, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.SUBJECT) @GetMapping( - value = [("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT - + "/" - + PathsUtil.MESSAGING_NOTIFICATION_PATH)] + value = [ + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + + "/" + + PathsUtil.MESSAGING_NOTIFICATION_PATH + ), + ], ) fun getNotificationsUsingProjectIdAndSubjectId( - @PathVariable @Valid projectId: String, @PathVariable @Valid subjectId: String + @PathVariable @Valid projectId: String, + @PathVariable @Valid subjectId: String, ): ResponseEntity { return ResponseEntity.ok( - this.notificationService.getNotificationsByProjectIdAndSubjectId(projectId, subjectId) + this.notificationService.getNotificationsByProjectIdAndSubjectId(projectId, subjectId), ) } @Authorized(permission = AuthPermissions.READ, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.PROJECT) @GetMapping( - ("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.MESSAGING_NOTIFICATION_PATH) + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.MESSAGING_NOTIFICATION_PATH + ), ) fun getNotificationsUsingProjectId( - @PathVariable @Valid projectId: String + @PathVariable @Valid projectId: String, ): ResponseEntity { return ResponseEntity.ok(this.notificationService.getNotificationsByProjectId(projectId)) } @Authorized(permission = AuthPermissions.UPDATE, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.SUBJECT) @PostMapping( - ("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT - + "/" - + PathsUtil.MESSAGING_NOTIFICATION_PATH) + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + + "/" + + PathsUtil.MESSAGING_NOTIFICATION_PATH + ), ) @Throws(URISyntaxException::class) fun addSingleNotification( @PathVariable projectId: String?, @PathVariable subjectId: String?, @RequestParam(required = false, defaultValue = "true") schedule: Boolean, - @RequestBody notification: @Valid FcmNotificationDto + @RequestBody notification: @Valid FcmNotificationDto, ): ResponseEntity { val notificationDto = this.notificationService.addNotification(notification, subjectId, projectId, schedule) return ResponseEntity.created( - URI("/" + PathsUtil.MESSAGING_NOTIFICATION_PATH + "/" + notificationDto.id) + URI("/" + PathsUtil.MESSAGING_NOTIFICATION_PATH + "/" + notificationDto.id), ) .body(notificationDto) } @Authorized(permission = AuthPermissions.UPDATE, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.SUBJECT) @PostMapping( - ("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT - + "/" - + PathsUtil.MESSAGING_NOTIFICATION_PATH - + "/schedule") + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + + "/" + + PathsUtil.MESSAGING_NOTIFICATION_PATH + + "/schedule" + ), ) @Throws(URISyntaxException::class) fun scheduleUserNotifications( @PathVariable projectId: String, - @PathVariable subjectId: String + @PathVariable subjectId: String, ): ResponseEntity { return ResponseEntity.ok( - this.notificationService.scheduleAllUserNotifications(subjectId, projectId) + this.notificationService.scheduleAllUserNotifications(subjectId, projectId), ) } @Authorized(permission = AuthPermissions.UPDATE, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.SUBJECT) @PostMapping( - ("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT - + "/" - + PathsUtil.MESSAGING_NOTIFICATION_PATH - + "/" - + PathsUtil.NOTIFICATION_ID_CONSTANT - + "/schedule") + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + + "/" + + PathsUtil.MESSAGING_NOTIFICATION_PATH + + "/" + + PathsUtil.NOTIFICATION_ID_CONSTANT + + "/schedule" + ), ) @Throws(URISyntaxException::class) fun scheduleUserNotification( @PathVariable projectId: String, @PathVariable subjectId: String, - @PathVariable notificationId: Long + @PathVariable notificationId: Long, ): ResponseEntity { return ResponseEntity.ok( - this.notificationService.scheduleNotification(subjectId, projectId, notificationId) + this.notificationService.scheduleNotification(subjectId, projectId, notificationId), ) } @Authorized(permission = AuthPermissions.UPDATE, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.SUBJECT) @PostMapping( - ("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT - + "/" - + PathsUtil.MESSAGING_NOTIFICATION_PATH - + "/batch") + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + + "/" + + PathsUtil.MESSAGING_NOTIFICATION_PATH + + "/batch" + ), ) fun addBatchNotifications( @PathVariable projectId: String, @PathVariable subjectId: String, @RequestParam(required = false, defaultValue = "true") schedule: Boolean, - @RequestBody @Valid notifications: FcmNotifications + @RequestBody @Valid notifications: FcmNotifications, ): ResponseEntity { return ResponseEntity.ok( - this.notificationService.addNotifications(notifications, subjectId, projectId, schedule) + this.notificationService.addNotifications(notifications, subjectId, projectId, schedule), ) } @Authorized(permission = AuthPermissions.UPDATE, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.SUBJECT) @PutMapping( - ("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT - + "/" - + PathsUtil.MESSAGING_NOTIFICATION_PATH) + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + + "/" + + PathsUtil.MESSAGING_NOTIFICATION_PATH + ), ) fun updateNotification( @PathVariable projectId: String, @PathVariable subjectId: String, - @RequestBody @Valid notification: FcmNotificationDto + @RequestBody @Valid notification: FcmNotificationDto, ): ResponseEntity { return ResponseEntity.ok( - this.notificationService.updateNotification(notification, subjectId, projectId) + this.notificationService.updateNotification(notification, subjectId, projectId), ) } @Authorized(permission = AuthPermissions.UPDATE, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.SUBJECT) @DeleteMapping( - ("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT - + "/" - + PathsUtil.MESSAGING_NOTIFICATION_PATH - + "/" - + PathsUtil.ALL_KEYWORD) + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + + "/" + + PathsUtil.MESSAGING_NOTIFICATION_PATH + + "/" + + PathsUtil.ALL_KEYWORD + ), ) fun deleteNotificationsForUser( - @PathVariable projectId: String, @PathVariable subjectId: String + @PathVariable projectId: String, + @PathVariable subjectId: String, ): ResponseEntity<*> { this.notificationService.removeNotificationsForUser(projectId, subjectId) return ResponseEntity.ok().build() @@ -260,49 +293,61 @@ class FcmNotificationController(private val notificationService: FcmNotification @Authorized(permission = AuthPermissions.UPDATE, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.SUBJECT) @DeleteMapping( - ("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT - + "/" - + PathsUtil.MESSAGING_NOTIFICATION_PATH - + "/" - + PathsUtil.NOTIFICATION_ID_CONSTANT) + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + + "/" + + PathsUtil.MESSAGING_NOTIFICATION_PATH + + "/" + + PathsUtil.NOTIFICATION_ID_CONSTANT + ), ) fun deleteNotificationUsingProjectIdAndSubjectIdAndNotificationId( - @PathVariable projectId: String, @PathVariable subjectId: String, @PathVariable notificationId: Long + @PathVariable projectId: String, + @PathVariable subjectId: String, + @PathVariable notificationId: Long, ): ResponseEntity { this.notificationService.deleteNotificationByProjectIdAndSubjectIdAndNotificationId( - projectId, subjectId, notificationId + projectId, + subjectId, + notificationId, ) return ResponseEntity.ok().build() } @Authorized(permission = AuthPermissions.UPDATE, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.SUBJECT) @DeleteMapping( - ("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT - + "/" - + PathsUtil.MESSAGING_NOTIFICATION_PATH - + "/" - + PathsUtil.TASK_PATH - + "/{id}") + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + + "/" + + PathsUtil.MESSAGING_NOTIFICATION_PATH + + "/" + + PathsUtil.TASK_PATH + + "/{id}" + ), ) fun deleteNotificationUsingProjectIdAndSubjectIdAndTaskId( - @PathVariable projectId: String, @PathVariable subjectId: String, @PathVariable id: Long + @PathVariable projectId: String, + @PathVariable subjectId: String, + @PathVariable id: Long, ): ResponseEntity { this.notificationService.removeNotificationsForUserUsingTaskId( - projectId, subjectId, id + projectId, + subjectId, + id, ) return ResponseEntity.ok().build() } diff --git a/src/main/java/org/radarbase/appserver/controller/GithubEndpoint.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/GithubEndpoint.kt similarity index 91% rename from src/main/java/org/radarbase/appserver/controller/GithubEndpoint.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/GithubEndpoint.kt index 4a559171a..9f192eb88 100644 --- a/src/main/java/org/radarbase/appserver/controller/GithubEndpoint.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/GithubEndpoint.kt @@ -37,13 +37,15 @@ class GithubEndpoint(private val githubService: GithubService) { @Authorized(permission = AuthConfig.AuthPermissions.READ, entity = AuthConfig.AuthEntities.SUBJECT) @GetMapping( - ("/" + - PathsUtil.GITHUB_PATH - + "/" + - PathsUtil.GITHUB_CONTENT_PATH) + ( + "/" + + PathsUtil.GITHUB_PATH + + "/" + + PathsUtil.GITHUB_CONTENT_PATH + ), ) fun getGithubContent( - @RequestParam url: String + @RequestParam url: String, ): ResponseEntity { return try { ResponseEntity.ok().body(this.githubService.getGithubContent(url)) diff --git a/src/main/java/org/radarbase/appserver/controller/NotificationStateEventController.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/NotificationStateEventController.kt similarity index 55% rename from src/main/java/org/radarbase/appserver/controller/NotificationStateEventController.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/NotificationStateEventController.kt index 7976785fe..0ac342615 100644 --- a/src/main/java/org/radarbase/appserver/controller/NotificationStateEventController.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/NotificationStateEventController.kt @@ -25,7 +25,12 @@ import org.radarbase.appserver.config.AuthConfig.AuthPermissions import org.radarbase.appserver.dto.NotificationStateEventDto import org.radarbase.appserver.service.NotificationStateEventService import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.* +import org.springframework.web.bind.annotation.CrossOrigin +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController import radar.spring.auth.common.Authorized import radar.spring.auth.common.PermissionOn import javax.naming.SizeLimitExceededException @@ -35,76 +40,93 @@ import javax.naming.SizeLimitExceededException class NotificationStateEventController(private val notificationStateEventService: NotificationStateEventService) { @Authorized(permission = AuthPermissions.READ, entity = AuthEntities.PROJECT) @GetMapping( - value = [("/" - + PathsUtil.MESSAGING_NOTIFICATION_PATH - + "/" - + PathsUtil.NOTIFICATION_ID_CONSTANT - + "/" - + PathsUtil.NOTIFICATION_STATE_EVENTS_PATH)] + value = [ + ( + "/" + + PathsUtil.MESSAGING_NOTIFICATION_PATH + + "/" + + PathsUtil.NOTIFICATION_ID_CONSTANT + + "/" + + PathsUtil.NOTIFICATION_STATE_EVENTS_PATH + ), + ], ) fun getNotificationStateEventsByNotificationId( - @PathVariable notificationId: Long + @PathVariable notificationId: Long, ): ResponseEntity> { return ResponseEntity.ok( - notificationStateEventService.getNotificationStateEventsByNotificationId(notificationId) + notificationStateEventService.getNotificationStateEventsByNotificationId(notificationId), ) } @Authorized(permission = AuthPermissions.READ, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.SUBJECT) @GetMapping( - value = [("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT - + "/" - + PathsUtil.MESSAGING_NOTIFICATION_PATH - + "/" - + PathsUtil.NOTIFICATION_ID_CONSTANT - + "/" - + PathsUtil.NOTIFICATION_STATE_EVENTS_PATH)] + value = [ + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + + "/" + + PathsUtil.MESSAGING_NOTIFICATION_PATH + + "/" + + PathsUtil.NOTIFICATION_ID_CONSTANT + + "/" + + PathsUtil.NOTIFICATION_STATE_EVENTS_PATH + ), + ], ) fun getNotificationStateEvents( @PathVariable projectId: String?, @PathVariable subjectId: String?, - @PathVariable notificationId: Long + @PathVariable notificationId: Long, ): ResponseEntity> { return ResponseEntity.ok( notificationStateEventService.getNotificationStateEvents( - projectId, subjectId, notificationId - ) + projectId, + subjectId, + notificationId, + ), ) } @Authorized(permission = AuthPermissions.UPDATE, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.SUBJECT) @PostMapping( - value = [("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT - + "/" - + PathsUtil.MESSAGING_NOTIFICATION_PATH - + "/" - + PathsUtil.NOTIFICATION_ID_CONSTANT - + "/" - + PathsUtil.NOTIFICATION_STATE_EVENTS_PATH)] + value = [ + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + + "/" + + PathsUtil.MESSAGING_NOTIFICATION_PATH + + "/" + + PathsUtil.NOTIFICATION_ID_CONSTANT + + "/" + + PathsUtil.NOTIFICATION_STATE_EVENTS_PATH + ), + ], ) @Throws(SizeLimitExceededException::class) fun postNotificationStateEvent( @PathVariable projectId: String?, @PathVariable subjectId: String?, @PathVariable notificationId: Long, - @RequestBody notificationStateEventDto: NotificationStateEventDto + @RequestBody notificationStateEventDto: NotificationStateEventDto, ): ResponseEntity> { notificationStateEventService.publishNotificationStateEventExternal( - projectId, subjectId, notificationId, notificationStateEventDto + projectId, + subjectId, + notificationId, + notificationStateEventDto, ) return getNotificationStateEvents(projectId, subjectId, notificationId) } diff --git a/src/main/java/org/radarbase/appserver/controller/PathsUtil.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/PathsUtil.kt similarity index 100% rename from src/main/java/org/radarbase/appserver/controller/PathsUtil.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/PathsUtil.kt diff --git a/src/main/java/org/radarbase/appserver/controller/ProtocolEndpoint.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/ProtocolEndpoint.kt similarity index 69% rename from src/main/java/org/radarbase/appserver/controller/ProtocolEndpoint.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/ProtocolEndpoint.kt index 930e90987..0ae23f8ab 100644 --- a/src/main/java/org/radarbase/appserver/controller/ProtocolEndpoint.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/ProtocolEndpoint.kt @@ -37,45 +37,55 @@ import java.io.IOException @Suppress("unused") @CrossOrigin @RestController -class ProtocolEndpoint (private val protocolGenerator: ProtocolGenerator) { +class ProtocolEndpoint(private val protocolGenerator: ProtocolGenerator) { + @Suppress("annotation-wrapping") @Authorized(permission = AuthPermissions.READ, entity = AuthEntities.PROJECT) @GetMapping("/" + PathsUtil.PROTOCOL_PATH) - fun getProtocols(): @Size(max = 100) Map { + fun getProtocols(): + @Size(max = 100) + Map { return this.protocolGenerator.retrieveAllProtocols() } @GetMapping( - value = [("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT - + "/" - + PathsUtil.PROTOCOL_PATH)] + value = [ + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + + "/" + + PathsUtil.PROTOCOL_PATH + ), + ], ) @Authorized(permission = AuthPermissions.READ, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.PROJECT) fun getProtocolUsingProjectIdAndSubjectId( - @PathVariable @Valid projectId: String, @PathVariable @Valid subjectId: String + @PathVariable @Valid projectId: String, + @PathVariable @Valid subjectId: String, ): Protocol { return this.protocolGenerator.getProtocolForSubject(subjectId) ?: Protocol() } @GetMapping( - ("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.PROTOCOL_PATH) + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.PROTOCOL_PATH + ), ) @Authorized(permission = AuthPermissions.READ, entity = AuthEntities.PROJECT) @Throws(IOException::class) fun getProtocolUsingProjectId( - @PathVariable projectId: @Valid String + @PathVariable projectId: @Valid String, ): Protocol { return this.protocolGenerator.getProtocol(projectId) } diff --git a/src/main/java/org/radarbase/appserver/controller/QuestionnaireScheduleEndpoint.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/QuestionnaireScheduleEndpoint.kt similarity index 68% rename from src/main/java/org/radarbase/appserver/controller/QuestionnaireScheduleEndpoint.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/QuestionnaireScheduleEndpoint.kt index 375f94ceb..b0d6a697d 100644 --- a/src/main/java/org/radarbase/appserver/controller/QuestionnaireScheduleEndpoint.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/QuestionnaireScheduleEndpoint.kt @@ -30,7 +30,15 @@ import org.radarbase.appserver.service.QuestionnaireScheduleService import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.* +import org.springframework.web.bind.annotation.CrossOrigin +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController import radar.spring.auth.common.Authorized import java.net.URI import java.net.URISyntaxException @@ -41,74 +49,79 @@ import java.util.* @RestController class QuestionnaireScheduleEndpoint @Autowired constructor(@field:Transient @field:Autowired private val scheduleService: QuestionnaireScheduleService) { @PostMapping( - ("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT - + "/" - + PathsUtil.QUESTIONNAIRE_SCHEDULE_PATH) + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + + "/" + + PathsUtil.QUESTIONNAIRE_SCHEDULE_PATH + ), ) @Authorized(permission = AuthPermissions.UPDATE, entity = AuthEntities.SUBJECT) @Throws(URISyntaxException::class) fun generateScheduleUsingProjectIdAndSubjectId( @PathVariable @Valid projectId: String, - @PathVariable @Valid subjectId: String + @PathVariable @Valid subjectId: String, ): ResponseEntity { this.scheduleService.generateScheduleUsingProjectIdAndSubjectId(projectId, subjectId) return ResponseEntity.created( - URI("/" + PathsUtil.QUESTIONNAIRE_SCHEDULE_PATH + "/") + URI("/" + PathsUtil.QUESTIONNAIRE_SCHEDULE_PATH + "/"), ).build() } @PutMapping( - ("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT - + "/" - + PathsUtil.QUESTIONNAIRE_SCHEDULE_PATH) + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + + "/" + + PathsUtil.QUESTIONNAIRE_SCHEDULE_PATH + ), ) @Authorized(permission = AuthPermissions.UPDATE, entity = AuthEntities.SUBJECT) @Throws(URISyntaxException::class) fun generateScheduleUsingProtocol( @PathVariable projectId: String, @PathVariable subjectId: String, - @RequestBody assessment: @Valid Assessment + @RequestBody assessment: @Valid Assessment, ): ResponseEntity { return try { this.scheduleService.generateScheduleUsingProjectIdAndSubjectIdAndAssessment( projectId, subjectId, - assessment + assessment, ) ResponseEntity.created( - URI("/" + PathsUtil.QUESTIONNAIRE_SCHEDULE_PATH + "/") + URI("/" + PathsUtil.QUESTIONNAIRE_SCHEDULE_PATH + "/"), ).build() } catch (e: URISyntaxException) { ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.message) } } - @GetMapping( - ("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT - + "/" - + PathsUtil.QUESTIONNAIRE_SCHEDULE_PATH) + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + + "/" + + PathsUtil.QUESTIONNAIRE_SCHEDULE_PATH + ), ) @Authorized(permission = AuthPermissions.READ, entity = AuthEntities.SUBJECT) fun getScheduleUsingProjectIdAndSubjectId( @@ -117,7 +130,7 @@ class QuestionnaireScheduleEndpoint @Autowired constructor(@field:Transient @fie @RequestParam(required = false, defaultValue = "all") type: String, @RequestParam(required = false, defaultValue = "") search: String, @RequestParam(required = false) startTime: Instant? = null, - @RequestParam(required = false) endTime: Instant? = null + @RequestParam(required = false) endTime: Instant? = null, ): List { val assessmentType = AssessmentType.valueOf(type.uppercase(Locale.getDefault())) // TODO: Use search instead of startTime and endTime @@ -126,7 +139,7 @@ class QuestionnaireScheduleEndpoint @Autowired constructor(@field:Transient @fie projectId, subjectId, startTime, - endTime + endTime, ) } @@ -135,30 +148,32 @@ class QuestionnaireScheduleEndpoint @Autowired constructor(@field:Transient @fie projectId, subjectId, assessmentType, - search + search, ) } return this.scheduleService.getTasksUsingProjectIdAndSubjectId(projectId, subjectId) } @DeleteMapping( - ("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT - + "/" - + PathsUtil.QUESTIONNAIRE_SCHEDULE_PATH) + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + + "/" + + PathsUtil.QUESTIONNAIRE_SCHEDULE_PATH + ), ) @Authorized(permission = AuthPermissions.UPDATE, entity = AuthEntities.SUBJECT) fun deleteScheduleForUser( @PathVariable projectId: String, @PathVariable subjectId: String, @RequestParam(required = false, defaultValue = "all") type: String, - @RequestParam(required = false, defaultValue = "") search: String + @RequestParam(required = false, defaultValue = "") search: String, ): ResponseEntity { val assessmentType = AssessmentType.valueOf(type.uppercase(Locale.getDefault())) this.scheduleService.removeScheduleForUserUsingSubjectIdAndType(projectId, subjectId, assessmentType, search) diff --git a/src/main/java/org/radarbase/appserver/controller/RadarProjectController.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/RadarProjectController.kt similarity index 87% rename from src/main/java/org/radarbase/appserver/controller/RadarProjectController.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/RadarProjectController.kt index 27458aea3..2ea998a62 100644 --- a/src/main/java/org/radarbase/appserver/controller/RadarProjectController.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/RadarProjectController.kt @@ -22,8 +22,6 @@ package org.radarbase.appserver.controller import jakarta.servlet.http.HttpServletRequest import jakarta.validation.Valid -import jakarta.websocket.server.PathParam -import lombok.extern.slf4j.Slf4j import org.radarbase.appserver.config.AuthConfig.AuthEntities import org.radarbase.appserver.config.AuthConfig.AuthPermissions import org.radarbase.appserver.dto.ProjectDto @@ -32,17 +30,21 @@ import org.radarbase.appserver.service.ProjectService import org.radarbase.auth.token.RadarToken import org.springframework.http.MediaType import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.* +import org.springframework.web.bind.annotation.CrossOrigin +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController import radar.spring.auth.common.AuthAspect import radar.spring.auth.common.Authorization import radar.spring.auth.common.Authorized import radar.spring.auth.common.PermissionOn import radar.spring.auth.exception.AuthorizationFailedException -import java.io.IOException import java.net.URI -import java.net.URISyntaxException import java.util.* -import java.util.stream.Collectors /** * Resource Endpoint for getting and adding projects. Each user [ ] needs to be associated to a project. A project may represent @@ -56,7 +58,7 @@ import java.util.stream.Collectors @RestController class RadarProjectController( private val projectService: ProjectService, - private val authorization: Authorization? + private val authorization: Authorization?, ) { /** * Method for updating a project. @@ -67,11 +69,11 @@ class RadarProjectController( @Authorized(permission = AuthPermissions.READ, entity = AuthEntities.SUBJECT) @PostMapping( value = ["/${PathsUtil.PROJECT_PATH}"], - consumes = [MediaType.APPLICATION_JSON_VALUE] + consumes = [MediaType.APPLICATION_JSON_VALUE], ) fun addProject( request: HttpServletRequest, - @Valid @RequestBody projectDto: ProjectDto + @Valid @RequestBody projectDto: ProjectDto, ): ResponseEntity { authorization?.let { val token = request.getAttribute(AuthAspect.TOKEN_KEY) as RadarToken @@ -82,7 +84,7 @@ class RadarProjectController( PermissionOn.PROJECT, projectDto.projectId, null, - null + null, ) ) { val projectDtoNew = projectService.addProject(projectDto) @@ -107,15 +109,15 @@ class RadarProjectController( @Authorized( permission = AuthPermissions.UPDATE, entity = AuthEntities.SUBJECT, - permissionOn = PermissionOn.PROJECT + permissionOn = PermissionOn.PROJECT, ) @PutMapping( value = ["/" + PathsUtil.PROJECT_PATH + "/" + PathsUtil.PROJECT_ID_CONSTANT], - consumes = [MediaType.APPLICATION_JSON_VALUE] + consumes = [MediaType.APPLICATION_JSON_VALUE], ) fun updateProject( @PathVariable("projectId") projectId: String, - @Valid @RequestBody projectDto: ProjectDto + @Valid @RequestBody projectDto: ProjectDto, ): ResponseEntity { val updatedProject = projectService.updateProject(projectDto) return ResponseEntity.ok(updatedProject) @@ -134,20 +136,19 @@ class RadarProjectController( PermissionOn.PROJECT, project.projectId, null, - null + null, ) } ResponseEntity.ok(ProjectDtos().withProjects(filteredProjects)) } ?: ResponseEntity.ok(allProjects) } - // TODO think about plain authorized @Authorized(permission = AuthPermissions.READ, entity = AuthEntities.PROJECT) @GetMapping("/" + PathsUtil.PROJECT_PATH + "/project") fun getProjectsUsingId( request: HttpServletRequest, - @RequestParam("id") id: Long + @RequestParam("id") id: Long, ): ResponseEntity { val projectDto = projectService.getProjectById(id) return authorization?.let { @@ -159,7 +160,7 @@ class RadarProjectController( PermissionOn.PROJECT, projectDto.projectId, null, - null + null, ) ) { ResponseEntity.ok(projectDto) @@ -169,11 +170,9 @@ class RadarProjectController( } ?: ResponseEntity.ok(projectDto) } - @Authorized(permission = AuthPermissions.READ, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.PROJECT) @GetMapping("/" + PathsUtil.PROJECT_PATH + "/" + PathsUtil.PROJECT_ID_CONSTANT) fun getProjectsUsingProjectId(@PathVariable projectId: String): ResponseEntity { return ResponseEntity.ok(projectService.getProjectByProjectId(projectId)) } - -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/controller/RadarUserController.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/RadarUserController.kt similarity index 77% rename from src/main/java/org/radarbase/appserver/controller/RadarUserController.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/RadarUserController.kt index 5f662bd7d..1192ecdbf 100644 --- a/src/main/java/org/radarbase/appserver/controller/RadarUserController.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/RadarUserController.kt @@ -30,7 +30,15 @@ import org.radarbase.appserver.exception.InvalidUserDetailsException import org.radarbase.appserver.service.UserService import org.radarbase.auth.token.RadarToken import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.* +import org.springframework.web.bind.annotation.CrossOrigin +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController import radar.spring.auth.common.AuthAspect import radar.spring.auth.common.Authorization import radar.spring.auth.common.Authorized @@ -51,24 +59,26 @@ import java.net.URISyntaxException @RestController class RadarUserController( private val userService: UserService, - private val authorization: Authorization? + private val authorization: Authorization?, ) { @Authorized(permission = AuthPermissions.UPDATE, entity = AuthEntities.SUBJECT) @PostMapping( - ("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH) + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + ), ) @Throws(URISyntaxException::class) fun addUserToProject( request: HttpServletRequest, @Valid @RequestBody userDto: FcmUserDto, @PathVariable projectId: String, - @RequestParam(required = false, defaultValue = "false") forceFcmToken: Boolean + @RequestParam(required = false, defaultValue = "false") forceFcmToken: Boolean, ): ResponseEntity { userDto.projectId = projectId authorization?.let { @@ -80,7 +90,7 @@ class RadarUserController( PermissionOn.SUBJECT, projectId, userDto.subjectId, - null + null, ) ) { if (forceFcmToken) userService.checkFcmTokenExistsAndReplace(userDto) @@ -98,20 +108,22 @@ class RadarUserController( @Authorized(permission = AuthPermissions.UPDATE, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.SUBJECT) @PutMapping( - ("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT) + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + ), ) fun updateUserInProject( @Valid @RequestBody userDto: FcmUserDto, @PathVariable subjectId: String, @PathVariable projectId: String, - @RequestParam(required = false, defaultValue = "false") forceFcmToken: Boolean + @RequestParam(required = false, defaultValue = "false") forceFcmToken: Boolean, ): ResponseEntity { userDto.subjectId = subjectId userDto.projectId = projectId @@ -134,7 +146,7 @@ class RadarUserController( PermissionOn.SUBJECT, user.projectId, user.subjectId, - null + null, ) } ResponseEntity.ok(FcmUsers(filteredUsers)) @@ -145,7 +157,7 @@ class RadarUserController( @GetMapping("/" + PathsUtil.USER_PATH + "/user") fun getRadarUserUsingId( request: HttpServletRequest, - @RequestParam("id") id: Long? + @RequestParam("id") id: Long?, ): ResponseEntity { id ?: throw InvalidUserDetailsException("The given id must not be null!") val userDto = userService.getUserById(id) @@ -156,7 +168,7 @@ class RadarUserController( @GetMapping("/" + PathsUtil.USER_PATH + "/" + PathsUtil.SUBJECT_ID_CONSTANT) fun getRadarUserUsingSubjectId( request: HttpServletRequest, - @PathVariable subjectId: String + @PathVariable subjectId: String, ): ResponseEntity { val userDto = userService.getUserBySubjectId(subjectId) return getFcmUserDtoResponseEntity(request, userDto) @@ -172,7 +184,7 @@ class RadarUserController( PermissionOn.SUBJECT, userDto.projectId, userDto.subjectId, - null + null, ) ) { ResponseEntity.ok(userDto) @@ -184,12 +196,14 @@ class RadarUserController( @Authorized(permission = AuthPermissions.READ, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.PROJECT) @GetMapping( - ("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH) + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + ), ) fun getUsersUsingProjectId(@PathVariable projectId: String): ResponseEntity { return ResponseEntity.ok(userService.getUsersByProjectId(projectId)) @@ -197,38 +211,42 @@ class RadarUserController( @Authorized(permission = AuthPermissions.READ, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.SUBJECT) @GetMapping( - ("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT) + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + ), ) fun getUsersUsingProjectIdAndSubjectId( @PathVariable projectId: String, - @PathVariable subjectId: String + @PathVariable subjectId: String, ): ResponseEntity { return ResponseEntity.ok(userService.getUserByProjectIdAndSubjectId(projectId, subjectId)) } @Authorized(permission = AuthPermissions.UPDATE, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.SUBJECT) @DeleteMapping( - ("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT) + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + ), ) fun deleteUserUsingProjectIdAndSubjectId( @PathVariable projectId: String, - @PathVariable subjectId: String + @PathVariable subjectId: String, ): ResponseEntity { userService.deleteUserByProjectIdAndSubjectId(projectId, subjectId) return ResponseEntity.ok().build() } -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/controller/TaskStateEventController.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/TaskStateEventController.kt similarity index 53% rename from src/main/java/org/radarbase/appserver/controller/TaskStateEventController.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/TaskStateEventController.kt index 98fa854a5..f0843c40d 100644 --- a/src/main/java/org/radarbase/appserver/controller/TaskStateEventController.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/TaskStateEventController.kt @@ -25,7 +25,12 @@ import org.radarbase.appserver.config.AuthConfig.AuthPermissions import org.radarbase.appserver.dto.TaskStateEventDto import org.radarbase.appserver.service.TaskStateEventService import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.* +import org.springframework.web.bind.annotation.CrossOrigin +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController import radar.spring.auth.common.Authorized import radar.spring.auth.common.PermissionOn import javax.naming.SizeLimitExceededException @@ -33,84 +38,99 @@ import javax.naming.SizeLimitExceededException @CrossOrigin @RestController class TaskStateEventController( - private val taskStateEventService: TaskStateEventService + private val taskStateEventService: TaskStateEventService, ) { @Authorized(permission = AuthPermissions.UPDATE, entity = AuthEntities.SUBJECT) @GetMapping( - value = [("/" - + PathsUtil.QUESTIONNAIRE_SCHEDULE_PATH - + "/" - + PathsUtil.TASK_ID_CONSTANT - + "/" - + PathsUtil.QUESTIONNAIRE_STATE_EVENTS_PATH)] + value = [ + ( + "/" + + PathsUtil.QUESTIONNAIRE_SCHEDULE_PATH + + "/" + + PathsUtil.TASK_ID_CONSTANT + + "/" + + PathsUtil.QUESTIONNAIRE_STATE_EVENTS_PATH + ), + ], ) fun getTaskStateEventsByTaskId( - @PathVariable taskId: Long + @PathVariable taskId: Long, ): ResponseEntity> { return ResponseEntity.ok( taskStateEventService.getTaskStateEventsByTaskId( - taskId - ) + taskId, + ), ) } @Authorized(permission = AuthPermissions.UPDATE, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.SUBJECT) @GetMapping( - value = [("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT - + "/" - + PathsUtil.QUESTIONNAIRE_SCHEDULE_PATH - + "/" - + PathsUtil.TASK_ID_CONSTANT - + "/" - + PathsUtil.QUESTIONNAIRE_STATE_EVENTS_PATH)] + value = [ + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + + "/" + + PathsUtil.QUESTIONNAIRE_SCHEDULE_PATH + + "/" + + PathsUtil.TASK_ID_CONSTANT + + "/" + + PathsUtil.QUESTIONNAIRE_STATE_EVENTS_PATH + ), + ], ) fun getTaskStateEvents( @PathVariable projectId: String?, @PathVariable subjectId: String?, - @PathVariable taskId: Long + @PathVariable taskId: Long, ): ResponseEntity> { return ResponseEntity.ok( taskStateEventService.getTaskStateEvents( projectId, subjectId, - taskId - ) + taskId, + ), ) } @Authorized(permission = AuthPermissions.UPDATE, entity = AuthEntities.SUBJECT, permissionOn = PermissionOn.SUBJECT) @PostMapping( - value = [("/" - + PathsUtil.PROJECT_PATH - + "/" - + PathsUtil.PROJECT_ID_CONSTANT - + "/" - + PathsUtil.USER_PATH - + "/" - + PathsUtil.SUBJECT_ID_CONSTANT - + "/" - + PathsUtil.QUESTIONNAIRE_SCHEDULE_PATH - + "/" - + PathsUtil.TASK_ID_CONSTANT - + "/" - + PathsUtil.QUESTIONNAIRE_STATE_EVENTS_PATH)] + value = [ + ( + "/" + + PathsUtil.PROJECT_PATH + + "/" + + PathsUtil.PROJECT_ID_CONSTANT + + "/" + + PathsUtil.USER_PATH + + "/" + + PathsUtil.SUBJECT_ID_CONSTANT + + "/" + + PathsUtil.QUESTIONNAIRE_SCHEDULE_PATH + + "/" + + PathsUtil.TASK_ID_CONSTANT + + "/" + + PathsUtil.QUESTIONNAIRE_STATE_EVENTS_PATH + ), + ], ) @Throws(SizeLimitExceededException::class) fun postTaskStateEvent( @PathVariable projectId: String, @PathVariable subjectId: String, @PathVariable taskId: Long, - @RequestBody taskStateEventDto: TaskStateEventDto + @RequestBody taskStateEventDto: TaskStateEventDto, ): ResponseEntity> { taskStateEventService.publishNotificationStateEventExternal( - projectId, subjectId, taskId, taskStateEventDto + projectId, + subjectId, + taskId, + taskStateEventDto, ) return getTaskStateEvents(projectId, subjectId, taskId) } diff --git a/src/main/java/org/radarbase/appserver/controller/UploadController.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/UploadController.kt similarity index 82% rename from src/main/java/org/radarbase/appserver/controller/UploadController.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/UploadController.kt index b2cd7cb01..020b370b6 100644 --- a/src/main/java/org/radarbase/appserver/controller/UploadController.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/UploadController.kt @@ -24,7 +24,12 @@ import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.* +import org.springframework.web.bind.annotation.CrossOrigin +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestMethod +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController import org.springframework.web.multipart.MultipartFile import radar.spring.auth.common.Authorized import radar.spring.auth.common.PermissionOn @@ -46,14 +51,16 @@ class UploadController { @Authorized( permission = AuthPermissions.CREATE, entity = AuthEntities.MEASUREMENT, - permissionOn = PermissionOn.SUBJECT + permissionOn = PermissionOn.SUBJECT, ) @PostMapping( - ("/" + PathsUtil.PROJECT_PATH + "/" + PathsUtil.PROJECT_ID_CONSTANT + + ( + "/" + PathsUtil.PROJECT_PATH + "/" + PathsUtil.PROJECT_ID_CONSTANT + "/" + PathsUtil.USER_PATH + "/" + PathsUtil.SUBJECT_ID_CONSTANT + "/" + PathsUtil.FILE_PATH + "/" + PathsUtil.TOPIC_PATH + "/" + PathsUtil.TOPIC_ID_CONSTANT + - "/upload") + "/upload" + ), ) @CrossOrigin(origins = ["*"], allowedHeaders = ["*"], exposedHeaders = ["Location"], methods = [RequestMethod.POST]) @Throws(URISyntaxException::class) @@ -61,7 +68,7 @@ class UploadController { @RequestParam("file") file: MultipartFile, @PathVariable projectId: String, @PathVariable subjectId: String, - @PathVariable topicId: String + @PathVariable topicId: String, ): ResponseEntity { logger.info("Storing file for project: {}, subject: {}, topic: {}", projectId, subjectId, topicId) diff --git a/src/main/java/org/radarbase/appserver/controller/exception/CustomExceptionHandler.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/exception/CustomExceptionHandler.kt similarity index 92% rename from src/main/java/org/radarbase/appserver/controller/exception/CustomExceptionHandler.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/exception/CustomExceptionHandler.kt index 47846602c..3598d4640 100644 --- a/src/main/java/org/radarbase/appserver/controller/exception/CustomExceptionHandler.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/controller/exception/CustomExceptionHandler.kt @@ -47,10 +47,12 @@ class CustomExceptionHandler : ResponseEntityExceptionHandler() { @ExceptionHandler(ConstraintViolationException::class) fun handleConstraintViolationException( - ex: RuntimeException, request: WebRequest + ex: RuntimeException, + request: WebRequest, ): ResponseEntity? { val body = DEFAULT_ERROR_ATTRIBUTES.getErrorAttributes( - request, ErrorAttributeOptions.of(Include.STACK_TRACE) + request, + ErrorAttributeOptions.of(Include.STACK_TRACE), ) body["message"] = "A Constraint was violated while Persisting. ${body["message"]}" body["status"] = HttpStatus.CONFLICT.value() @@ -61,10 +63,11 @@ class CustomExceptionHandler : ResponseEntityExceptionHandler() { ex: MethodArgumentNotValidException, headers: HttpHeaders, status: HttpStatusCode, - request: WebRequest + request: WebRequest, ): ResponseEntity? { val body = DEFAULT_ERROR_ATTRIBUTES.getErrorAttributes( - request, ErrorAttributeOptions.of(Include.STACK_TRACE) + request, + ErrorAttributeOptions.of(Include.STACK_TRACE), ) body["status"] = status return handleExceptionInternal(ex, body, headers, status, request) @@ -76,4 +79,4 @@ class CustomExceptionHandler : ResponseEntityExceptionHandler() { fun handleException(e: NotificationAlreadyExistsException): NotificationAlreadyExistsException { return e } -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/dto/DataMessageStateEventDto.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/DataMessageStateEventDto.kt similarity index 93% rename from src/main/java/org/radarbase/appserver/dto/DataMessageStateEventDto.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/DataMessageStateEventDto.kt index 7ff677c4a..30df60ab2 100644 --- a/src/main/java/org/radarbase/appserver/dto/DataMessageStateEventDto.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/DataMessageStateEventDto.kt @@ -26,10 +26,10 @@ import org.radarbase.appserver.event.state.MessageState import java.time.Instant @JsonIgnoreProperties(ignoreUnknown = true) -data class DataMessageStateEventDto ( +data class DataMessageStateEventDto( var id: Long? = null, var dataMessageId: Long? = null, var state: MessageState? = null, var time: Instant? = null, - var associatedInfo: String? = null -) \ No newline at end of file + var associatedInfo: String? = null, +) diff --git a/src/main/java/org/radarbase/appserver/dto/NotificationStateEventDto.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/NotificationStateEventDto.kt similarity index 93% rename from src/main/java/org/radarbase/appserver/dto/NotificationStateEventDto.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/NotificationStateEventDto.kt index df1d9244d..c1dba63f4 100644 --- a/src/main/java/org/radarbase/appserver/dto/NotificationStateEventDto.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/NotificationStateEventDto.kt @@ -26,10 +26,10 @@ import org.radarbase.appserver.event.state.MessageState import java.time.Instant @JsonIgnoreProperties(ignoreUnknown = true) -data class NotificationStateEventDto ( +data class NotificationStateEventDto( var id: Long? = null, var notificationId: Long? = null, var state: MessageState? = null, var time: Instant? = null, - var associatedInfo: String? = null -) \ No newline at end of file + var associatedInfo: String? = null, +) diff --git a/src/main/java/org/radarbase/appserver/dto/ProjectDto.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/ProjectDto.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/dto/ProjectDto.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/ProjectDto.kt index 7dbf3f4e8..d621ae9ea 100644 --- a/src/main/java/org/radarbase/appserver/dto/ProjectDto.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/ProjectDto.kt @@ -48,4 +48,4 @@ data class ProjectDto( @field:DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) var updatedAt: Instant? = null, -) \ No newline at end of file +) diff --git a/src/main/java/org/radarbase/appserver/dto/ProjectDtos.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/ProjectDtos.kt similarity index 92% rename from src/main/java/org/radarbase/appserver/dto/ProjectDtos.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/ProjectDtos.kt index 85b74e60d..81c1689a2 100644 --- a/src/main/java/org/radarbase/appserver/dto/ProjectDtos.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/ProjectDtos.kt @@ -2,11 +2,11 @@ package org.radarbase.appserver.dto import jakarta.validation.constraints.Size -data class ProjectDtos ( +data class ProjectDtos( @field:Size(max = 500) val projects: MutableList = mutableListOf(), ) { - fun withProjects(projects: List): ProjectDtos = apply{ + fun withProjects(projects: List): ProjectDtos = apply { this.projects.clear() this.projects.addAll(projects) } @@ -14,4 +14,4 @@ data class ProjectDtos ( fun addProject(projectDto: ProjectDto): ProjectDtos = apply { this.projects.add(projectDto) } -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/dto/TaskStateEventDto.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/TaskStateEventDto.kt similarity index 93% rename from src/main/java/org/radarbase/appserver/dto/TaskStateEventDto.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/TaskStateEventDto.kt index 84476eddd..f8c5c60ff 100644 --- a/src/main/java/org/radarbase/appserver/dto/TaskStateEventDto.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/TaskStateEventDto.kt @@ -26,10 +26,10 @@ import org.radarbase.appserver.event.state.TaskState import java.time.Instant @JsonIgnoreProperties(ignoreUnknown = true) -data class TaskStateEventDto ( +data class TaskStateEventDto( var id: Long? = null, var taskId: Long? = null, var state: TaskState? = null, var time: Instant? = null, - var associatedInfo: String? = null -) \ No newline at end of file + var associatedInfo: String? = null, +) diff --git a/src/main/java/org/radarbase/appserver/dto/fcm/FcmDataMessageDto.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/fcm/FcmDataMessageDto.kt similarity index 95% rename from src/main/java/org/radarbase/appserver/dto/fcm/FcmDataMessageDto.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/fcm/FcmDataMessageDto.kt index ee3eea9b6..f60f2a015 100644 --- a/src/main/java/org/radarbase/appserver/dto/fcm/FcmDataMessageDto.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/fcm/FcmDataMessageDto.kt @@ -138,14 +138,18 @@ class FcmDataMessageDto(dataMessageEntity: DataMessage? = null) : Serializable { if (this === other) return true if (other !is FcmDataMessageDto) return false val that = other - return delivered == that.delivered && ttlSeconds == that.ttlSeconds && scheduledTime == that.scheduledTime - && appPackage == that.appPackage - && sourceType == that.sourceType + return delivered == that.delivered && ttlSeconds == that.ttlSeconds && scheduledTime == that.scheduledTime && + appPackage == that.appPackage && + sourceType == that.sourceType } override fun hashCode(): Int { return Objects.hash( - scheduledTime, delivered, ttlSeconds, appPackage, sourceType + scheduledTime, + delivered, + ttlSeconds, + appPackage, + sourceType, ) } @@ -153,7 +157,6 @@ class FcmDataMessageDto(dataMessageEntity: DataMessage? = null) : Serializable { return "FcmDataMessageDto(id=$id, scheduledTime=$scheduledTime, delivered=$delivered, ttlSeconds=$ttlSeconds, sourceId=$sourceId, fcmMessageId=$fcmMessageId, fcmTopic=$fcmTopic, fcmCondition=$fcmCondition, appPackage=$appPackage, sourceType=$sourceType, dataMap=$dataMap, priority=$priority, mutableContent=$mutableContent, createdAt=$createdAt, updatedAt=$updatedAt)" } - companion object { private const val serialVersionUID = 3L } diff --git a/src/main/java/org/radarbase/appserver/dto/fcm/FcmDataMessages.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/fcm/FcmDataMessages.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/dto/fcm/FcmDataMessages.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/fcm/FcmDataMessages.kt index cac469a54..4f5d57985 100644 --- a/src/main/java/org/radarbase/appserver/dto/fcm/FcmDataMessages.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/fcm/FcmDataMessages.kt @@ -60,7 +60,6 @@ class FcmDataMessages { return Objects.hash(_dataMessages) } - companion object { private val logger: Logger = LoggerFactory.getLogger(FcmDataMessages::class.java) } diff --git a/src/main/java/org/radarbase/appserver/dto/fcm/FcmNotificationDto.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/fcm/FcmNotificationDto.kt similarity index 98% rename from src/main/java/org/radarbase/appserver/dto/fcm/FcmNotificationDto.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/fcm/FcmNotificationDto.kt index 532b39dea..6d8c49cce 100644 --- a/src/main/java/org/radarbase/appserver/dto/fcm/FcmNotificationDto.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/fcm/FcmNotificationDto.kt @@ -264,10 +264,16 @@ class FcmNotificationDto(notificationEntity: Notification? = null) : Serializabl FcmNotificationDto::sourceType, ) - override fun hashCode(): Int { return Objects.hash( - scheduledTime, delivered, title, body, ttlSeconds, type, appPackage, sourceType + scheduledTime, + delivered, + title, + body, + ttlSeconds, + type, + appPackage, + sourceType, ) } @@ -275,7 +281,6 @@ class FcmNotificationDto(notificationEntity: Notification? = null) : Serializabl return "FcmNotificationDto(id=$id, scheduledTime=$scheduledTime, delivered=$delivered, title=$title, body=$body, ttlSeconds=$ttlSeconds, sourceId=$sourceId, fcmMessageId=$fcmMessageId, fcmTopic=$fcmTopic, fcmCondition=$fcmCondition, type=$type, appPackage=$appPackage, sourceType=$sourceType, additionalData=$additionalData, priority=$priority, sound=$sound, badge=$badge, subtitle=$subtitle, icon=$icon, color=$color, bodyLocKey=$bodyLocKey, bodyLocArgs=$bodyLocArgs, titleLocKey=$titleLocKey, titleLocArgs=$titleLocArgs, androidChannelId=$androidChannelId, tag=$tag, clickAction=$clickAction, emailEnabled=$emailEnabled, emailTitle=$emailTitle, emailBody=$emailBody, mutableContent=$mutableContent, createdAt=$createdAt, updatedAt=$updatedAt)" } - companion object { @Serial private const val serialVersionUID = 3L diff --git a/src/main/java/org/radarbase/appserver/dto/fcm/FcmNotifications.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/fcm/FcmNotifications.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/dto/fcm/FcmNotifications.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/fcm/FcmNotifications.kt index c88b6600d..60876ac40 100644 --- a/src/main/java/org/radarbase/appserver/dto/fcm/FcmNotifications.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/fcm/FcmNotifications.kt @@ -62,7 +62,6 @@ class FcmNotifications { return Objects.hash(_notifications) } - companion object { private val logger: Logger = LoggerFactory.getLogger(FcmNotifications::class.java) } diff --git a/src/main/java/org/radarbase/appserver/dto/fcm/FcmUserDto.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/fcm/FcmUserDto.kt similarity index 98% rename from src/main/java/org/radarbase/appserver/dto/fcm/FcmUserDto.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/fcm/FcmUserDto.kt index df76825af..50f457906 100644 --- a/src/main/java/org/radarbase/appserver/dto/fcm/FcmUserDto.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/fcm/FcmUserDto.kt @@ -76,5 +76,5 @@ data class FcmUserDto( timezone = user.timezone, fcmToken = user.fcmToken, language = user.language, - attributes = user.attributes - )} \ No newline at end of file + attributes = user.attributes, + ) } diff --git a/src/main/java/org/radarbase/appserver/dto/fcm/FcmUsers.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/fcm/FcmUsers.kt similarity index 96% rename from src/main/java/org/radarbase/appserver/dto/fcm/FcmUsers.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/fcm/FcmUsers.kt index 8b20864ce..db2fc3ae6 100644 --- a/src/main/java/org/radarbase/appserver/dto/fcm/FcmUsers.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/fcm/FcmUsers.kt @@ -25,5 +25,5 @@ import jakarta.validation.constraints.Size data class FcmUsers( @field:Size(max = 1500) - var users: List -) \ No newline at end of file + var users: List, +) diff --git a/src/main/java/org/radarbase/appserver/dto/protocol/Assessment.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/Assessment.kt similarity index 98% rename from src/main/java/org/radarbase/appserver/dto/protocol/Assessment.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/Assessment.kt index c9b9bec94..b328b109e 100644 --- a/src/main/java/org/radarbase/appserver/dto/protocol/Assessment.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/Assessment.kt @@ -45,7 +45,7 @@ data class Assessment( var order: Int = 0, var nQuestions: Int = 0, var showInCalendar: Boolean = true, - var isDemo: Boolean = false + var isDemo: Boolean = false, ) { var type: AssessmentType? get() { diff --git a/src/main/java/org/radarbase/appserver/dto/protocol/AssessmentProtocol.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/AssessmentProtocol.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/dto/protocol/AssessmentProtocol.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/AssessmentProtocol.kt index ee1e91a4a..8e48fb5c1 100644 --- a/src/main/java/org/radarbase/appserver/dto/protocol/AssessmentProtocol.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/AssessmentProtocol.kt @@ -32,7 +32,7 @@ data class AssessmentProtocol( var repeatQuestionnaire: RepeatQuestionnaire? = null, var referenceTimestamp: ReferenceTimestamp? = null, var clinicalProtocol: ClinicalProtocol? = null, - var notification: NotificationProtocol = NotificationProtocol() + var notification: NotificationProtocol = NotificationProtocol(), ) { @JsonDeserialize(using = ReferenceTimestampDeserializer::class) fun setReferenceTimestamp(responseObject: Any?) { diff --git a/src/main/java/org/radarbase/appserver/dto/protocol/AssessmentType.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/AssessmentType.kt similarity index 98% rename from src/main/java/org/radarbase/appserver/dto/protocol/AssessmentType.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/AssessmentType.kt index 7704fa1e5..6a6347038 100644 --- a/src/main/java/org/radarbase/appserver/dto/protocol/AssessmentType.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/AssessmentType.kt @@ -29,5 +29,5 @@ enum class AssessmentType { TRIGGERED, @JsonProperty("all") - ALL -} \ No newline at end of file + ALL, +} diff --git a/src/main/java/org/radarbase/appserver/dto/protocol/ClinicalProtocol.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/ClinicalProtocol.kt similarity index 93% rename from src/main/java/org/radarbase/appserver/dto/protocol/ClinicalProtocol.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/ClinicalProtocol.kt index 97e46dfb1..c3390369c 100644 --- a/src/main/java/org/radarbase/appserver/dto/protocol/ClinicalProtocol.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/ClinicalProtocol.kt @@ -25,5 +25,5 @@ package org.radarbase.appserver.dto.protocol */ data class ClinicalProtocol( var requiresInClinicCompletion: Boolean = false, - var repeatAfterClinicVisit: RepeatQuestionnaire? = null + var repeatAfterClinicVisit: RepeatQuestionnaire? = null, ) diff --git a/src/main/java/org/radarbase/appserver/dto/protocol/DefinitionInfo.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/DefinitionInfo.kt similarity index 92% rename from src/main/java/org/radarbase/appserver/dto/protocol/DefinitionInfo.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/DefinitionInfo.kt index 1caf7e9dc..46f294236 100644 --- a/src/main/java/org/radarbase/appserver/dto/protocol/DefinitionInfo.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/DefinitionInfo.kt @@ -21,8 +21,8 @@ import java.net.URI /** @author yatharthranjan */ -data class DefinitionInfo ( +data class DefinitionInfo( val repository: URI? = null, val name: String? = null, - val avsc: String? = null -) \ No newline at end of file + val avsc: String? = null, +) diff --git a/src/main/java/org/radarbase/appserver/dto/protocol/EmailNotificationProtocol.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/EmailNotificationProtocol.kt similarity index 69% rename from src/main/java/org/radarbase/appserver/dto/protocol/EmailNotificationProtocol.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/EmailNotificationProtocol.kt index b1f0e6650..0d54439cd 100644 --- a/src/main/java/org/radarbase/appserver/dto/protocol/EmailNotificationProtocol.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/EmailNotificationProtocol.kt @@ -2,12 +2,9 @@ package org.radarbase.appserver.dto.protocol import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonProperty -import lombok.AllArgsConstructor -import lombok.Data -import lombok.NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) -data class EmailNotificationProtocol ( +data class EmailNotificationProtocol( @field:JsonProperty("enabled") val enabled: Boolean = false, @@ -15,5 +12,5 @@ data class EmailNotificationProtocol ( var title: LanguageText? = null, @field:JsonProperty("text") - var body: LanguageText? = null - ) + var body: LanguageText? = null, +) diff --git a/src/main/java/org/radarbase/appserver/dto/protocol/GithubContent.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/GithubContent.kt similarity index 94% rename from src/main/java/org/radarbase/appserver/dto/protocol/GithubContent.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/GithubContent.kt index bf3749a16..094758da2 100644 --- a/src/main/java/org/radarbase/appserver/dto/protocol/GithubContent.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/GithubContent.kt @@ -21,7 +21,6 @@ package org.radarbase.appserver.dto.protocol import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import lombok.Data import org.radarbase.appserver.util.Base64Deserializer /** @@ -32,7 +31,7 @@ import org.radarbase.appserver.util.Base64Deserializer * * @author yatharthranjan */ -data class GithubContent ( +data class GithubContent( @field:JsonDeserialize(using = Base64Deserializer::class) var content: String? = null, @@ -44,5 +43,5 @@ data class GithubContent ( var node_id: String? = null, - var encoding: String? = null + var encoding: String? = null, ) diff --git a/src/main/java/org/radarbase/appserver/dto/protocol/LanguageText.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/LanguageText.kt similarity index 97% rename from src/main/java/org/radarbase/appserver/dto/protocol/LanguageText.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/LanguageText.kt index 2691a944c..f6b7fa31c 100644 --- a/src/main/java/org/radarbase/appserver/dto/protocol/LanguageText.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/LanguageText.kt @@ -29,7 +29,7 @@ data class LanguageText( var nl: String? = null, var da: String? = null, var de: String? = null, - var es: String? = null + var es: String? = null, ) { fun getText(languageCode: String?): String { return when (languageCode?.lowercase(Locale.getDefault())) { @@ -42,4 +42,4 @@ data class LanguageText( else -> "" } ?: "" } -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/dto/protocol/NotificationProtocol.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/NotificationProtocol.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/dto/protocol/NotificationProtocol.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/NotificationProtocol.kt index be207526b..eee093f02 100644 --- a/src/main/java/org/radarbase/appserver/dto/protocol/NotificationProtocol.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/NotificationProtocol.kt @@ -32,5 +32,5 @@ data class NotificationProtocol( @field:JsonProperty("text") var body: LanguageText? = null, @field:JsonProperty("email") - var email: EmailNotificationProtocol = EmailNotificationProtocol() + var email: EmailNotificationProtocol = EmailNotificationProtocol(), ) diff --git a/src/main/java/org/radarbase/appserver/dto/protocol/NotificationProtocolMode.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/NotificationProtocolMode.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/dto/protocol/NotificationProtocolMode.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/NotificationProtocolMode.kt index babc68a4a..6d3888f4e 100644 --- a/src/main/java/org/radarbase/appserver/dto/protocol/NotificationProtocolMode.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/NotificationProtocolMode.kt @@ -11,4 +11,4 @@ enum class NotificationProtocolMode { @JsonProperty("combined") COMBINED, -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/dto/protocol/Protocol.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/Protocol.kt similarity index 96% rename from src/main/java/org/radarbase/appserver/dto/protocol/Protocol.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/Protocol.kt index 95f448fea..8a9890db3 100644 --- a/src/main/java/org/radarbase/appserver/dto/protocol/Protocol.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/Protocol.kt @@ -27,7 +27,7 @@ data class Protocol( var schemaVersion: String? = null, var name: String? = null, var healthIssues: List? = null, - var protocols: List? = null + var protocols: List? = null, ) { fun hasAssessment(assessment: String?): Boolean { return protocols?.any { it.name == assessment } == true diff --git a/src/main/java/org/radarbase/appserver/dto/protocol/ProtocolCacheEntry.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/ProtocolCacheEntry.kt similarity index 89% rename from src/main/java/org/radarbase/appserver/dto/protocol/ProtocolCacheEntry.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/ProtocolCacheEntry.kt index ead45d07d..fde9ebe2a 100644 --- a/src/main/java/org/radarbase/appserver/dto/protocol/ProtocolCacheEntry.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/ProtocolCacheEntry.kt @@ -20,7 +20,4 @@ */ package org.radarbase.appserver.dto.protocol -import lombok.Data - -@Data -class ProtocolCacheEntry(val id: String, val protocol: Protocol?) +data class ProtocolCacheEntry(val id: String, val protocol: Protocol?) diff --git a/src/main/java/org/radarbase/appserver/dto/protocol/ReferenceTimestamp.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/ReferenceTimestamp.kt similarity index 85% rename from src/main/java/org/radarbase/appserver/dto/protocol/ReferenceTimestamp.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/ReferenceTimestamp.kt index 2abe62aea..a6ad6cb6d 100644 --- a/src/main/java/org/radarbase/appserver/dto/protocol/ReferenceTimestamp.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/ReferenceTimestamp.kt @@ -21,16 +21,13 @@ package org.radarbase.appserver.dto.protocol import com.fasterxml.jackson.annotation.JsonProperty -import lombok.AllArgsConstructor -import lombok.Data -import lombok.NoArgsConstructor /** * @author yatharthranjan */ -data class ReferenceTimestamp ( +data class ReferenceTimestamp( @field:JsonProperty("timestamp") var timestamp: String? = null, @field:JsonProperty("format") - var format: ReferenceTimestampType? = null + var format: ReferenceTimestampType? = null, ) diff --git a/src/main/java/org/radarbase/appserver/dto/protocol/ReferenceTimestampDeserializer.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/ReferenceTimestampDeserializer.kt similarity index 95% rename from src/main/java/org/radarbase/appserver/dto/protocol/ReferenceTimestampDeserializer.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/ReferenceTimestampDeserializer.kt index 40c32d554..9edb4e032 100644 --- a/src/main/java/org/radarbase/appserver/dto/protocol/ReferenceTimestampDeserializer.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/ReferenceTimestampDeserializer.kt @@ -17,7 +17,6 @@ package org.radarbase.appserver.dto.protocol import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.core.JsonProcessingException import com.fasterxml.jackson.core.JsonToken import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.JsonDeserializer diff --git a/src/main/java/org/radarbase/appserver/dto/protocol/ReferenceTimestampType.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/ReferenceTimestampType.kt similarity index 96% rename from src/main/java/org/radarbase/appserver/dto/protocol/ReferenceTimestampType.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/ReferenceTimestampType.kt index 5e6156780..46f9f69cd 100644 --- a/src/main/java/org/radarbase/appserver/dto/protocol/ReferenceTimestampType.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/ReferenceTimestampType.kt @@ -16,5 +16,5 @@ enum class ReferenceTimestampType { NOW, @JsonProperty("today") - TODAY -} \ No newline at end of file + TODAY, +} diff --git a/src/main/java/org/radarbase/appserver/dto/protocol/ReminderTimePeriod.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/ReminderTimePeriod.kt similarity index 96% rename from src/main/java/org/radarbase/appserver/dto/protocol/ReminderTimePeriod.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/ReminderTimePeriod.kt index fe54d1778..8797601db 100644 --- a/src/main/java/org/radarbase/appserver/dto/protocol/ReminderTimePeriod.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/ReminderTimePeriod.kt @@ -31,10 +31,10 @@ class ReminderTimePeriod( override fun equals(other: Any?): Boolean = equalTo( other, - ReminderTimePeriod::repeat + ReminderTimePeriod::repeat, ) override fun hashCode(): Int { return Objects.hash(repeat) } -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/dto/protocol/RepeatProtocol.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/RepeatProtocol.kt similarity index 98% rename from src/main/java/org/radarbase/appserver/dto/protocol/RepeatProtocol.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/RepeatProtocol.kt index d7e998065..60ec88db6 100644 --- a/src/main/java/org/radarbase/appserver/dto/protocol/RepeatProtocol.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/RepeatProtocol.kt @@ -30,7 +30,7 @@ data class RepeatProtocol( var unit: String? = null, var amount: Int? = null, var randomAmountBetween: Array? = null, - var dayOfWeek: String? = null + var dayOfWeek: String? = null, ) { override fun equals(other: Any?): Boolean { diff --git a/src/main/java/org/radarbase/appserver/dto/protocol/RepeatQuestionnaire.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/RepeatQuestionnaire.kt similarity index 95% rename from src/main/java/org/radarbase/appserver/dto/protocol/RepeatQuestionnaire.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/RepeatQuestionnaire.kt index 1efd52a30..700f2d13b 100644 --- a/src/main/java/org/radarbase/appserver/dto/protocol/RepeatQuestionnaire.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/RepeatQuestionnaire.kt @@ -34,6 +34,5 @@ data class RepeatQuestionnaire( var unit: String? = null, var unitsFromZero: List? = null, var randomUnitsFromZeroBetween: List>? = null, - var dayOfWeekMap: Map? = null + var dayOfWeekMap: Map? = null, ) - diff --git a/src/main/java/org/radarbase/appserver/dto/protocol/ScheduleCacheEntry.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/ScheduleCacheEntry.kt similarity index 97% rename from src/main/java/org/radarbase/appserver/dto/protocol/ScheduleCacheEntry.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/ScheduleCacheEntry.kt index 0d4adb7df..e83e248f7 100644 --- a/src/main/java/org/radarbase/appserver/dto/protocol/ScheduleCacheEntry.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/ScheduleCacheEntry.kt @@ -25,4 +25,4 @@ import org.radarbase.appserver.dto.questionnaire.Schedule /** * @author yatharthranjan */ -data class ScheduleCacheEntry(val id: String, val schedule: Schedule = Schedule()) \ No newline at end of file +data class ScheduleCacheEntry(val id: String, val schedule: Schedule = Schedule()) diff --git a/src/main/java/org/radarbase/appserver/dto/protocol/TimePeriod.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/TimePeriod.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/dto/protocol/TimePeriod.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/TimePeriod.kt index 304f40eed..53f368110 100644 --- a/src/main/java/org/radarbase/appserver/dto/protocol/TimePeriod.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/protocol/TimePeriod.kt @@ -37,4 +37,4 @@ class TimePeriod(var unit: String? = null, var amount: Int? = null) { override fun hashCode(): Int { return Objects.hash(amount, unit) } -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/dto/questionnaire/AssessmentSchedule.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/questionnaire/AssessmentSchedule.kt similarity index 95% rename from src/main/java/org/radarbase/appserver/dto/questionnaire/AssessmentSchedule.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/questionnaire/AssessmentSchedule.kt index c0fc122b5..251f25463 100644 --- a/src/main/java/org/radarbase/appserver/dto/questionnaire/AssessmentSchedule.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/questionnaire/AssessmentSchedule.kt @@ -26,7 +26,7 @@ data class AssessmentSchedule( var referenceTimestamps: List? = null, var tasks: List? = null, var notifications: List? = null, - var reminders: List? = null + var reminders: List? = null, ) { fun hasTasks(): Boolean { return !tasks.isNullOrEmpty() diff --git a/src/main/java/org/radarbase/appserver/dto/questionnaire/Schedule.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/questionnaire/Schedule.kt similarity index 93% rename from src/main/java/org/radarbase/appserver/dto/questionnaire/Schedule.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/questionnaire/Schedule.kt index af2e7c304..96c502d96 100644 --- a/src/main/java/org/radarbase/appserver/dto/questionnaire/Schedule.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/dto/questionnaire/Schedule.kt @@ -28,7 +28,7 @@ data class Schedule( var assessmentSchedules: MutableList = mutableListOf(), var user: User? = null, var version: String = "0.0.0", - var timezone: String? = user?.timezone + var timezone: String? = user?.timezone, ) { constructor(user: User) : this(mutableListOf(), user, "0.0.0", user.timezone) @@ -36,18 +36,18 @@ data class Schedule( assessmentSchedules.toMutableList(), user, "0.0.0", - user.timezone + user.timezone, ) constructor( assessmentSchedules: List, user: User, - version: String? + version: String?, ) : this( assessmentSchedules.toMutableList(), user, version ?: "0.0.0", - user.timezone + user.timezone, ) fun addAssessmentSchedule(assessmentSchedule: AssessmentSchedule): Schedule = apply { diff --git a/src/main/java/org/radarbase/appserver/entity/AuditModel.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/AuditModel.kt similarity index 89% rename from src/main/java/org/radarbase/appserver/entity/AuditModel.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/AuditModel.kt index e3e5960ee..897affec5 100644 --- a/src/main/java/org/radarbase/appserver/entity/AuditModel.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/AuditModel.kt @@ -1,7 +1,11 @@ package org.radarbase.appserver.entity import com.fasterxml.jackson.annotation.JsonIgnoreProperties -import jakarta.persistence.* +import jakarta.persistence.Column +import jakarta.persistence.EntityListeners +import jakarta.persistence.MappedSuperclass +import jakarta.persistence.Temporal +import jakarta.persistence.TemporalType import org.radarbase.appserver.util.equalTo import org.springframework.data.annotation.CreatedDate import org.springframework.data.annotation.LastModifiedDate @@ -49,10 +53,10 @@ abstract class AuditModel { override fun equals(other: Any?): Boolean = equalTo( other, AuditModel::createdAt, - AuditModel::updatedAt + AuditModel::updatedAt, ) override fun hashCode(): Int { return Objects.hash(createdAt, updatedAt) } -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/entity/DataMessage.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/DataMessage.kt similarity index 88% rename from src/main/java/org/radarbase/appserver/entity/DataMessage.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/DataMessage.kt index 46401fd6c..907f4de27 100644 --- a/src/main/java/org/radarbase/appserver/entity/DataMessage.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/DataMessage.kt @@ -20,7 +20,16 @@ */ package org.radarbase.appserver.entity -import jakarta.persistence.* +import jakarta.persistence.CollectionTable +import jakarta.persistence.Column +import jakarta.persistence.ElementCollection +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.Inheritance +import jakarta.persistence.InheritanceType +import jakarta.persistence.MapKeyColumn +import jakarta.persistence.Table +import jakarta.persistence.UniqueConstraint import org.springframework.lang.Nullable import java.io.Serial import java.time.Instant @@ -37,10 +46,14 @@ import java.util.* */ @Entity @Table( - name = "data_messages", uniqueConstraints = [UniqueConstraint( - columnNames = ["user_id", "source_id", "scheduled_time", "ttl_seconds", "delivered", "dry_run" - ] - )] + name = "data_messages", + uniqueConstraints = [ + UniqueConstraint( + columnNames = [ + "user_id", "source_id", "scheduled_time", "ttl_seconds", "delivered", "dry_run", + ], + ), + ], ) @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) class DataMessage : Message() { @@ -102,64 +115,63 @@ class DataMessage : Message() { fun id(id: Long?): DataMessageBuilder = apply { this.id = id - } + } fun user(user: User?): DataMessageBuilder = apply { this.user = user - } + } fun sourceId(sourceId: String?): DataMessageBuilder = apply { this.sourceId = sourceId - } + } fun scheduledTime(scheduledTime: Instant?): DataMessageBuilder = apply { this.scheduledTime = scheduledTime - } + } fun ttlSeconds(ttlSeconds: Int): DataMessageBuilder = apply { this.ttlSeconds = ttlSeconds - } + } fun fcmMessageId(fcmMessageId: String?): DataMessageBuilder = apply { this.fcmMessageId = fcmMessageId - } + } fun fcmTopic(fcmTopic: String?): DataMessageBuilder = apply { this.fcmTopic = fcmTopic - } + } fun fcmCondition(fcmCondition: String?): DataMessageBuilder = apply { this.fcmCondition = fcmCondition - } + } fun delivered(delivered: Boolean): DataMessageBuilder = apply { this.delivered = delivered - } + } fun appPackage(appPackage: String?): DataMessageBuilder = apply { this.appPackage = appPackage - } + } fun sourceType(sourceType: String?): DataMessageBuilder = apply { this.sourceType = sourceType - } + } fun dryRun(dryRun: Boolean): DataMessageBuilder = apply { this.dryRun = dryRun - } + } fun priority(priority: String?): DataMessageBuilder = apply { this.priority = priority - } + } fun mutableContent(mutableContent: Boolean): DataMessageBuilder = apply { this.mutableContent = mutableContent - } - + } fun dataMap(dataMap: MutableMap?): DataMessageBuilder = apply { this.dataMap = dataMap - } + } fun build(): DataMessage { val dataMessage = DataMessage() @@ -184,8 +196,6 @@ class DataMessage : Message() { } } - - override fun toString(): String { return "DataMessage(dataMap=$dataMap)" } diff --git a/src/main/java/org/radarbase/appserver/entity/DataMessageStateEvent.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/DataMessageStateEvent.kt similarity index 80% rename from src/main/java/org/radarbase/appserver/entity/DataMessageStateEvent.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/DataMessageStateEvent.kt index 5b5fc29c7..a5536535b 100644 --- a/src/main/java/org/radarbase/appserver/entity/DataMessageStateEvent.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/DataMessageStateEvent.kt @@ -21,7 +21,14 @@ package org.radarbase.appserver.entity import com.fasterxml.jackson.annotation.JsonIgnore -import jakarta.persistence.* +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table import jakarta.validation.constraints.NotNull import org.hibernate.annotations.OnDelete import org.hibernate.annotations.OnDeleteAction @@ -31,7 +38,7 @@ import java.time.Instant @Entity @Table(name = "data_message_state_events") -class DataMessageStateEvent :MessageStateEvent { +class DataMessageStateEvent : MessageStateEvent { @Id @GeneratedValue(strategy = GenerationType.AUTO) @@ -44,13 +51,12 @@ class DataMessageStateEvent :MessageStateEvent { @field:JsonIgnore var dataMessage: DataMessage? = null - constructor( dataMessage: DataMessage?, state: MessageState, time: Instant, - associatedInfo: String? - ): super(state, time, associatedInfo) { + associatedInfo: String?, + ) : super(state, time, associatedInfo) { this.dataMessage = dataMessage } diff --git a/src/main/java/org/radarbase/appserver/entity/Message.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/Message.kt similarity index 90% rename from src/main/java/org/radarbase/appserver/entity/Message.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/Message.kt index c6347cb88..98eeeb183 100644 --- a/src/main/java/org/radarbase/appserver/entity/Message.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/Message.kt @@ -22,7 +22,15 @@ package org.radarbase.appserver.entity import com.fasterxml.jackson.annotation.JsonIgnore -import jakarta.persistence.* +import jakarta.persistence.CascadeType +import jakarta.persistence.Column +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.MappedSuperclass import jakarta.validation.constraints.NotNull import org.hibernate.annotations.OnDelete import org.hibernate.annotations.OnDeleteAction @@ -95,7 +103,7 @@ class Message( var priority: String? = null, @Column(name = "mutable_content") - var mutableContent: Boolean = false + var mutableContent: Boolean = false, ) : AuditModel(), Serializable, Scheduled { override fun equals(other: Any?): Boolean = equalTo( @@ -119,7 +127,7 @@ class Message( delivered, dryRun, appPackage, - sourceType + sourceType, ) } @@ -127,9 +135,8 @@ class Message( return "Message(id=$id, user=$user, task=$task, sourceId=$sourceId, scheduledTime=$scheduledTime, ttlSeconds=$ttlSeconds, fcmMessageId=$fcmMessageId, fcmTopic=$fcmTopic, fcmCondition=$fcmCondition, delivered=$delivered, validated=$validated, appPackage=$appPackage, sourceType=$sourceType, dryRun=$dryRun, priority=$priority, mutableContent=$mutableContent)" } - companion object { @Serial private const val serialVersionUID = -367424816328519L } -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/entity/MessageStateEvent.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/MessageStateEvent.kt similarity index 88% rename from src/main/java/org/radarbase/appserver/entity/MessageStateEvent.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/MessageStateEvent.kt index 8f1a27f24..61df439de 100644 --- a/src/main/java/org/radarbase/appserver/entity/MessageStateEvent.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/MessageStateEvent.kt @@ -20,7 +20,10 @@ */ package org.radarbase.appserver.entity -import jakarta.persistence.* +import jakarta.persistence.Column +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.MappedSuperclass import jakarta.validation.constraints.NotNull import org.radarbase.appserver.event.state.MessageState import java.io.Serial @@ -42,7 +45,7 @@ class MessageStateEvent( @Column(name = "associated_info", length = 1250) var associatedInfo: String? = null, - ) : Serializable { +) : Serializable { companion object { @Serial diff --git a/src/main/java/org/radarbase/appserver/entity/Notification.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/Notification.kt similarity index 96% rename from src/main/java/org/radarbase/appserver/entity/Notification.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/Notification.kt index 609de4366..cbb755660 100644 --- a/src/main/java/org/radarbase/appserver/entity/Notification.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/Notification.kt @@ -36,19 +36,21 @@ import java.util.* @Table( name = "notifications", - uniqueConstraints = [UniqueConstraint( - columnNames = [ - "user_id", - "source_id", - "scheduled_time", - "title", - "body", - "type", - "ttl_seconds", - "delivered", - "dry_run" - ] - )] + uniqueConstraints = [ + UniqueConstraint( + columnNames = [ + "user_id", + "source_id", + "scheduled_time", + "title", + "body", + "type", + "ttl_seconds", + "delivered", + "dry_run", + ], + ), + ], ) @Entity class Notification : Message() { @@ -220,7 +222,6 @@ class Notification : Message() { @Transient private var emailBody: String? = notification?.emailBody - fun id(id: Long?): NotificationBuilder = apply { this.id = id return this @@ -291,7 +292,6 @@ class Notification : Message() { return this } - fun title(title: String?): NotificationBuilder = apply { this.title = title return this @@ -443,10 +443,10 @@ class Notification : Message() { return false } val that = other - return super.equals(other) - && title == that.title - && body == that.body - && type == that.type + return super.equals(other) && + title == that.title && + body == that.body && + type == that.type } override fun hashCode(): Int { @@ -454,7 +454,7 @@ class Notification : Message() { super.hashCode(), title, body, - type + type, ) } @@ -462,9 +462,8 @@ class Notification : Message() { return "Notification(title=$title, body=$body, type=$type, sound=$sound, badge=$badge, subtitle=$subtitle, icon=$icon, color=$color, bodyLocKey=$bodyLocKey, bodyLocArgs=$bodyLocArgs, titleLocKey=$titleLocKey, titleLocArgs=$titleLocArgs, androidChannelId=$androidChannelId, tag=$tag, clickAction=$clickAction, emailEnabled=$emailEnabled, emailTitle=$emailTitle, emailBody=$emailBody, additionalData=$additionalData, Message=${super.toString()})" } - companion object { @Serial private const val serialVersionUID = 6L } -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/entity/NotificationStateEvent.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/NotificationStateEvent.kt similarity index 86% rename from src/main/java/org/radarbase/appserver/entity/NotificationStateEvent.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/NotificationStateEvent.kt index 9b27b4b1c..c716f8134 100644 --- a/src/main/java/org/radarbase/appserver/entity/NotificationStateEvent.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/NotificationStateEvent.kt @@ -21,7 +21,14 @@ package org.radarbase.appserver.entity import com.fasterxml.jackson.annotation.JsonIgnore -import jakarta.persistence.* +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table import jakarta.validation.constraints.NotNull import org.hibernate.annotations.OnDelete import org.hibernate.annotations.OnDeleteAction diff --git a/src/main/java/org/radarbase/appserver/entity/Project.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/Project.kt similarity index 89% rename from src/main/java/org/radarbase/appserver/entity/Project.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/Project.kt index d08869cba..b5bf5235c 100644 --- a/src/main/java/org/radarbase/appserver/entity/Project.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/Project.kt @@ -21,7 +21,12 @@ package org.radarbase.appserver.entity -import jakarta.persistence.* +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.Table import jakarta.validation.constraints.NotNull import org.radarbase.appserver.util.equalTo import org.radarbase.appserver.util.stringRepresentation @@ -67,6 +72,6 @@ class Project( Project::id, Project::projectId, Project::createdAt, - Project::updatedAt + Project::updatedAt, ) -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/entity/Scheduled.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/Scheduled.kt similarity index 100% rename from src/main/java/org/radarbase/appserver/entity/Scheduled.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/Scheduled.kt diff --git a/src/main/java/org/radarbase/appserver/entity/Task.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/Task.kt similarity index 94% rename from src/main/java/org/radarbase/appserver/entity/Task.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/Task.kt index ebb034374..39fc7aaf5 100644 --- a/src/main/java/org/radarbase/appserver/entity/Task.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/Task.kt @@ -22,7 +22,17 @@ package org.radarbase.appserver.entity import com.fasterxml.jackson.annotation.JsonFormat import com.fasterxml.jackson.annotation.JsonIgnore -import jakarta.persistence.* +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table import jakarta.validation.constraints.NotNull import org.hibernate.annotations.OnDelete import org.hibernate.annotations.OnDeleteAction diff --git a/src/main/java/org/radarbase/appserver/entity/TaskStateEvent.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/TaskStateEvent.kt similarity index 82% rename from src/main/java/org/radarbase/appserver/entity/TaskStateEvent.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/TaskStateEvent.kt index 522137765..567ec8ecd 100644 --- a/src/main/java/org/radarbase/appserver/entity/TaskStateEvent.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/TaskStateEvent.kt @@ -22,7 +22,17 @@ package org.radarbase.appserver.entity import com.fasterxml.jackson.annotation.JsonIgnore -import jakarta.persistence.* +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table import jakarta.validation.constraints.NotNull import org.hibernate.annotations.OnDelete import org.hibernate.annotations.OnDeleteAction @@ -63,6 +73,4 @@ class TaskStateEvent { this.associatedInfo = associatedInfo this.task = task } - - } diff --git a/src/main/java/org/radarbase/appserver/entity/User.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/User.kt similarity index 98% rename from src/main/java/org/radarbase/appserver/entity/User.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/User.kt index 7ea129f4f..6d91d52ed 100644 --- a/src/main/java/org/radarbase/appserver/entity/User.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/User.kt @@ -57,8 +57,8 @@ import java.util.Objects name = "users", uniqueConstraints = [ UniqueConstraint(columnNames = ["subject_id", "project_id"]), - UniqueConstraint(columnNames = ["fcm_token"]) - ] + UniqueConstraint(columnNames = ["fcm_token"]), + ], ) @Entity class User( @@ -138,4 +138,4 @@ class User( @Serial private const val serialVersionUID = -87395866328519L } -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/entity/UserMetrics.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/UserMetrics.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/entity/UserMetrics.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/UserMetrics.kt index d366d78ee..020a88392 100644 --- a/src/main/java/org/radarbase/appserver/entity/UserMetrics.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/entity/UserMetrics.kt @@ -77,4 +77,4 @@ class UserMetrics( @Serial private const val serialVersionUID = 9182735866328519L } -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/event/listener/QuartzMessageJobListener.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/listener/QuartzMessageJobListener.kt similarity index 87% rename from src/main/java/org/radarbase/appserver/event/listener/QuartzMessageJobListener.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/listener/QuartzMessageJobListener.kt index 40e52b59a..c47217aee 100644 --- a/src/main/java/org/radarbase/appserver/event/listener/QuartzMessageJobListener.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/listener/QuartzMessageJobListener.kt @@ -40,7 +40,7 @@ import java.time.Instant class QuartzMessageJobListener( @Transient private val messageStateEventPublisher: ApplicationEventPublisher, @Transient private val notificationRepository: NotificationRepository, - @Transient private val dataMessageRepository: DataMessageRepository + @Transient private val dataMessageRepository: DataMessageRepository, ) : JobListener { /** * Get the name of the `JobListener`. @@ -100,7 +100,11 @@ class QuartzMessageJobListener( additionalInfo.put("error_description", jobException.toString()) val notificationStateEventError = NotificationStateEventDto( - this, notification, MessageState.ERRORED, additionalInfo, Instant.now() + this, + notification, + MessageState.ERRORED, + additionalInfo, + Instant.now(), ) messageStateEventPublisher.publishEvent(notificationStateEventError) @@ -110,7 +114,11 @@ class QuartzMessageJobListener( val notificationStateEvent = NotificationStateEventDto( - this, notification, MessageState.EXECUTED, null, Instant.now() + this, + notification, + MessageState.EXECUTED, + null, + Instant.now(), ) messageStateEventPublisher.publishEvent(notificationStateEvent) } @@ -127,8 +135,12 @@ class QuartzMessageJobListener( additionalInfo.put("error", jobException.message!!) additionalInfo.put("error_description", jobException.toString()) val dataMessageStateEventError = DataMessageStateEventDto( - this, dataMessage, MessageState.ERRORED, additionalInfo, Instant.now() - ) + this, + dataMessage, + MessageState.ERRORED, + additionalInfo, + Instant.now(), + ) messageStateEventPublisher.publishEvent(dataMessageStateEventError) log.warn("The job could not be executed.", jobException) @@ -137,7 +149,11 @@ class QuartzMessageJobListener( val dataMessageStateEvent = DataMessageStateEventDto( - this, dataMessage, MessageState.EXECUTED, null, Instant.now() + this, + dataMessage, + MessageState.EXECUTED, + null, + Instant.now(), ) messageStateEventPublisher.publishEvent(dataMessageStateEvent) } diff --git a/src/main/java/org/radarbase/appserver/event/listener/QuartzMessageSchedulerListener.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/listener/QuartzMessageSchedulerListener.kt similarity index 91% rename from src/main/java/org/radarbase/appserver/event/listener/QuartzMessageSchedulerListener.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/listener/QuartzMessageSchedulerListener.kt index e6cf30ca6..7f9055de6 100644 --- a/src/main/java/org/radarbase/appserver/event/listener/QuartzMessageSchedulerListener.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/listener/QuartzMessageSchedulerListener.kt @@ -20,7 +20,13 @@ */ package org.radarbase.appserver.event.listener -import org.quartz.* +import org.quartz.JobDetail +import org.quartz.JobKey +import org.quartz.Scheduler +import org.quartz.SchedulerException +import org.quartz.SchedulerListener +import org.quartz.Trigger +import org.quartz.TriggerKey import org.radarbase.appserver.event.state.MessageState import org.radarbase.appserver.event.state.dto.DataMessageStateEventDto import org.radarbase.appserver.event.state.dto.NotificationStateEventDto @@ -41,7 +47,7 @@ class QuartzMessageSchedulerListener( @Transient private val messageStateEventPublisher: ApplicationEventPublisher, @Transient private val notificationRepository: NotificationRepository, @Transient private val dataMessageRepository: DataMessageRepository, - @Transient private val scheduler: Scheduler + @Transient private val scheduler: Scheduler, ) : SchedulerListener { /** * Called by the `[Scheduler]` when a `[JobDetail]` is @@ -54,7 +60,7 @@ class QuartzMessageSchedulerListener( } catch (exc: SchedulerException) { log.warn( "Encountered error while getting job information from Trigger: ", - exc + exc, ) return } @@ -72,7 +78,11 @@ class QuartzMessageSchedulerListener( } val notificationStateEvent = NotificationStateEventDto( - this, notification, MessageState.SCHEDULED, null, Instant.now() + this, + notification, + MessageState.SCHEDULED, + null, + Instant.now(), ) messageStateEventPublisher.publishEvent(notificationStateEvent) } @@ -84,8 +94,12 @@ class QuartzMessageSchedulerListener( return } val dataMessageStateEvent = DataMessageStateEventDto( - this, dataMessage, MessageState.SCHEDULED, null, Instant.now() - ) + this, + dataMessage, + MessageState.SCHEDULED, + null, + Instant.now(), + ) messageStateEventPublisher.publishEvent(dataMessageStateEvent) } @@ -115,7 +129,11 @@ class QuartzMessageSchedulerListener( } val notificationStateEvent = NotificationStateEventDto( - this, notification, MessageState.CANCELLED, null, Instant.now() + this, + notification, + MessageState.CANCELLED, + null, + Instant.now(), ) messageStateEventPublisher.publishEvent(notificationStateEvent) } diff --git a/src/main/java/org/radarbase/appserver/event/state/MessageState.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/state/MessageState.kt similarity index 100% rename from src/main/java/org/radarbase/appserver/event/state/MessageState.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/state/MessageState.kt diff --git a/src/main/java/org/radarbase/appserver/event/state/MessageStateEventListener.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/state/MessageStateEventListener.kt similarity index 94% rename from src/main/java/org/radarbase/appserver/event/state/MessageStateEventListener.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/state/MessageStateEventListener.kt index bd7cebf1f..a6ee118b2 100644 --- a/src/main/java/org/radarbase/appserver/event/state/MessageStateEventListener.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/state/MessageStateEventListener.kt @@ -40,7 +40,7 @@ import org.springframework.transaction.event.TransactionalEventListener class MessageStateEventListener( @Transient private val objectMapper: ObjectMapper, @Transient private val notificationStateEventService: NotificationStateEventService, - @Transient private val dataMessageStateEventService: DataMessageStateEventService + @Transient private val dataMessageStateEventService: DataMessageStateEventService, ) { /** * Handle an application event. @@ -54,7 +54,10 @@ class MessageStateEventListener( val info = convertMapToString(event.additionalInfo) logger.debug("ID: {}, STATE: {}.", event.notification.id, event.state) val eventEntity = NotificationStateEvent( - event.notification, event.state, event.time, info + event.notification, + event.state, + event.time, + info, ) notificationStateEventService.addNotificationStateEvent(eventEntity) } @@ -66,7 +69,10 @@ class MessageStateEventListener( val info = convertMapToString(event.additionalInfo) logger.debug("ID: {}, STATE: {}", event.dataMessage.id, event.state) val eventEntity = DataMessageStateEvent( - event.dataMessage, event.state, event.time, info + event.dataMessage, + event.state, + event.time, + info, ) dataMessageStateEventService.addDataMessageStateEvent(eventEntity) } diff --git a/src/main/java/org/radarbase/appserver/event/state/TaskState.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/state/TaskState.kt similarity index 100% rename from src/main/java/org/radarbase/appserver/event/state/TaskState.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/state/TaskState.kt diff --git a/src/main/java/org/radarbase/appserver/event/state/TaskStateEventListener.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/state/TaskStateEventListener.kt similarity index 96% rename from src/main/java/org/radarbase/appserver/event/state/TaskStateEventListener.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/state/TaskStateEventListener.kt index 3836139c6..b136ddde0 100644 --- a/src/main/java/org/radarbase/appserver/event/state/TaskStateEventListener.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/state/TaskStateEventListener.kt @@ -35,7 +35,7 @@ import org.springframework.transaction.event.TransactionalEventListener @Component class TaskStateEventListener( @field:Transient private val objectMapper: ObjectMapper, - @field:Transient private val taskStateEventService: TaskStateEventService + @field:Transient private val taskStateEventService: TaskStateEventService, ) { /** * Handle an application event. @@ -50,7 +50,10 @@ class TaskStateEventListener( val info = convertMapToString(event.additionalInfo) logger.debug("ID: {}, STATE: {}", event.task?.id, event.state) val eventEntity = TaskStateEvent( - event.task, event.state, event.time, info + event.task, + event.state, + event.time, + info, ) taskStateEventService.addTaskStateEvent(eventEntity) } diff --git a/src/main/java/org/radarbase/appserver/event/state/dto/DataMessageStateEventDto.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/state/dto/DataMessageStateEventDto.kt similarity index 98% rename from src/main/java/org/radarbase/appserver/event/state/dto/DataMessageStateEventDto.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/state/dto/DataMessageStateEventDto.kt index db02cb193..30df53025 100644 --- a/src/main/java/org/radarbase/appserver/event/state/dto/DataMessageStateEventDto.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/state/dto/DataMessageStateEventDto.kt @@ -39,7 +39,7 @@ class DataMessageStateEventDto( val dataMessage: DataMessage, state: MessageState, additionalInfo: Map?, - time: Instant + time: Instant, ) : MessageStateEventDto(source, state, additionalInfo, time) { override fun toString(): String { diff --git a/src/main/java/org/radarbase/appserver/event/state/dto/MessageStateEventDto.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/state/dto/MessageStateEventDto.kt similarity index 98% rename from src/main/java/org/radarbase/appserver/event/state/dto/MessageStateEventDto.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/state/dto/MessageStateEventDto.kt index f27b70ce6..cf4905a3f 100644 --- a/src/main/java/org/radarbase/appserver/event/state/dto/MessageStateEventDto.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/state/dto/MessageStateEventDto.kt @@ -32,7 +32,7 @@ class MessageStateEventDto( source: Any, var state: MessageState, var additionalInfo: Map?, - var time: Instant + var time: Instant, ) : ApplicationEvent(source) { companion object { diff --git a/src/main/java/org/radarbase/appserver/event/state/dto/NotificationStateEventDto.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/state/dto/NotificationStateEventDto.kt similarity index 95% rename from src/main/java/org/radarbase/appserver/event/state/dto/NotificationStateEventDto.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/state/dto/NotificationStateEventDto.kt index 97bed6873..5f5703d11 100644 --- a/src/main/java/org/radarbase/appserver/event/state/dto/NotificationStateEventDto.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/state/dto/NotificationStateEventDto.kt @@ -38,8 +38,8 @@ class NotificationStateEventDto( val notification: Notification, state: MessageState, additionalInfo: Map?, - time: Instant -): MessageStateEventDto(source, state, additionalInfo, time) { + time: Instant, +) : MessageStateEventDto(source, state, additionalInfo, time) { override fun toString(): String { return "NotificationStateEventDto(source=$source, notification=$notification) ${super.toString()}" diff --git a/src/main/java/org/radarbase/appserver/event/state/dto/TaskStateEventDto.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/state/dto/TaskStateEventDto.kt similarity index 96% rename from src/main/java/org/radarbase/appserver/event/state/dto/TaskStateEventDto.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/state/dto/TaskStateEventDto.kt index ecc7d5a0c..636f94689 100644 --- a/src/main/java/org/radarbase/appserver/event/state/dto/TaskStateEventDto.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/event/state/dto/TaskStateEventDto.kt @@ -33,14 +33,14 @@ class TaskStateEventDto( val task: Task?, val state: TaskState?, val additionalInfo: Map?, - val time: Instant? + val time: Instant?, ) : ApplicationEvent(source) { override fun toString(): String = stringRepresentation( TaskStateEventDto::task, TaskStateEventDto::state, TaskStateEventDto::additionalInfo, - TaskStateEventDto::time + TaskStateEventDto::time, ) companion object { diff --git a/src/main/java/org/radarbase/appserver/exception/AlreadyExistsException.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/AlreadyExistsException.kt similarity index 96% rename from src/main/java/org/radarbase/appserver/exception/AlreadyExistsException.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/AlreadyExistsException.kt index c8be2aef9..562a1524f 100644 --- a/src/main/java/org/radarbase/appserver/exception/AlreadyExistsException.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/AlreadyExistsException.kt @@ -31,7 +31,7 @@ import java.io.Serial * [HttpStatus.ALREADY_REPORTED]. * */ -@ResponseStatus(HttpStatus.ALREADY_REPORTED) +@ResponseStatus(HttpStatus.EXPECTATION_FAILED) class AlreadyExistsException : RuntimeException { companion object { @Serial @@ -41,4 +41,4 @@ class AlreadyExistsException : RuntimeException { constructor(message: String) : super(message) constructor(message: String, obj: Any) : super("$message $obj") -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/exception/EmailMessageTransmitException.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/EmailMessageTransmitException.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/exception/EmailMessageTransmitException.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/EmailMessageTransmitException.kt index 538587123..e32011a78 100644 --- a/src/main/java/org/radarbase/appserver/exception/EmailMessageTransmitException.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/EmailMessageTransmitException.kt @@ -13,4 +13,4 @@ class EmailMessageTransmitException : MessageTransmitException { constructor(message: String) : super(message) constructor(message: String, e: Throwable) : super(message, e) -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/exception/FcmMessageTransmitException.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/FcmMessageTransmitException.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/exception/FcmMessageTransmitException.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/FcmMessageTransmitException.kt index a9d923187..1907d4874 100644 --- a/src/main/java/org/radarbase/appserver/exception/FcmMessageTransmitException.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/FcmMessageTransmitException.kt @@ -11,4 +11,4 @@ class FcmMessageTransmitException : MessageTransmitException { @Serial private const val serialVersionUID: Long = -923871442166939L } -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/exception/FileStorageException.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/FileStorageException.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/exception/FileStorageException.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/FileStorageException.kt index fb21f7e93..49f9c6b21 100644 --- a/src/main/java/org/radarbase/appserver/exception/FileStorageException.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/FileStorageException.kt @@ -15,4 +15,4 @@ class FileStorageException : RuntimeException { constructor(message: String) : super(message) constructor(message: String, `object`: Any) : super("$message $`object`") -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/exception/InvalidFileDetailsException.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/InvalidFileDetailsException.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/exception/InvalidFileDetailsException.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/InvalidFileDetailsException.kt index 1a4e1161e..1da806ce5 100644 --- a/src/main/java/org/radarbase/appserver/exception/InvalidFileDetailsException.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/InvalidFileDetailsException.kt @@ -15,4 +15,4 @@ class InvalidFileDetailsException : IllegalArgumentException { constructor(message: String) : super(message) constructor(message: String, obj: Any) : super("$message $obj") -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/exception/InvalidNotificationDetailsException.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/InvalidNotificationDetailsException.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/exception/InvalidNotificationDetailsException.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/InvalidNotificationDetailsException.kt index 6383faac0..192ee4846 100644 --- a/src/main/java/org/radarbase/appserver/exception/InvalidNotificationDetailsException.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/InvalidNotificationDetailsException.kt @@ -45,4 +45,4 @@ class InvalidNotificationDetailsException : RuntimeException { constructor(message: String) : super(message) constructor(notificationDto: FcmNotificationDto) : super("Invalid details of the Notification supplied : $notificationDto") -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/exception/InvalidPathDetailsException.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/InvalidPathDetailsException.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/exception/InvalidPathDetailsException.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/InvalidPathDetailsException.kt index e109eb7f5..1460b727e 100644 --- a/src/main/java/org/radarbase/appserver/exception/InvalidPathDetailsException.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/InvalidPathDetailsException.kt @@ -15,4 +15,4 @@ class InvalidPathDetailsException : IllegalArgumentException { constructor(message: String) : super(message) constructor(message: String, obj: Any) : super("$message $obj") -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/exception/InvalidProjectDetailsException.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/InvalidProjectDetailsException.kt similarity index 98% rename from src/main/java/org/radarbase/appserver/exception/InvalidProjectDetailsException.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/InvalidProjectDetailsException.kt index f288b237c..7df124393 100644 --- a/src/main/java/org/radarbase/appserver/exception/InvalidProjectDetailsException.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/InvalidProjectDetailsException.kt @@ -46,6 +46,6 @@ class InvalidProjectDetailsException : RuntimeException { constructor( projectDto: ProjectDto, - cause: Throwable + cause: Throwable, ) : super("Invalid details supplied for the project $projectDto", cause) -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/exception/InvalidUserDetailsException.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/InvalidUserDetailsException.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/exception/InvalidUserDetailsException.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/InvalidUserDetailsException.kt index 1686872da..fba7211fe 100644 --- a/src/main/java/org/radarbase/appserver/exception/InvalidUserDetailsException.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/InvalidUserDetailsException.kt @@ -46,4 +46,4 @@ class InvalidUserDetailsException : RuntimeException { constructor(userDto: FcmUserDto) : super("Invalid details supplied for the user $userDto") constructor(userDto: FcmUserDto, cause: Throwable) : super("Invalid details supplied for the user $userDto", cause) -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/exception/MessageTransmitException.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/MessageTransmitException.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/exception/MessageTransmitException.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/MessageTransmitException.kt index 5bec05bba..08dec13b5 100644 --- a/src/main/java/org/radarbase/appserver/exception/MessageTransmitException.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/MessageTransmitException.kt @@ -12,4 +12,4 @@ class MessageTransmitException : Exception { constructor(message: String) : super(message) constructor(message: String, e: Throwable) : super(message, e) -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/exception/NotFoundException.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/NotFoundException.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/exception/NotFoundException.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/NotFoundException.kt index dbc50735e..b705c5816 100644 --- a/src/main/java/org/radarbase/appserver/exception/NotFoundException.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/NotFoundException.kt @@ -37,4 +37,4 @@ class NotFoundException(message: String) : RuntimeException(message) { @Serial private const val serialVersionUID = -281834508766939L } -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/exception/NotificationAlreadyExistsException.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/NotificationAlreadyExistsException.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/exception/NotificationAlreadyExistsException.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/NotificationAlreadyExistsException.kt index 91c58b236..311357b5b 100644 --- a/src/main/java/org/radarbase/appserver/exception/NotificationAlreadyExistsException.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/NotificationAlreadyExistsException.kt @@ -21,8 +21,8 @@ package org.radarbase.appserver.exception -import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonIgnoreProperties import org.radarbase.appserver.dto.fcm.FcmNotificationDto import java.io.Serial @@ -53,4 +53,4 @@ class NotificationAlreadyExistsException : RuntimeException { this.dto = `object` this.errorMessage = message } -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/exception/entity/ErrorDetails.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/entity/ErrorDetails.kt similarity index 88% rename from src/main/java/org/radarbase/appserver/exception/entity/ErrorDetails.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/entity/ErrorDetails.kt index a1668f776..362431a23 100644 --- a/src/main/java/org/radarbase/appserver/exception/entity/ErrorDetails.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/entity/ErrorDetails.kt @@ -8,5 +8,5 @@ class ErrorDetails( var timestamp: Instant? = null, var status: Int = 0, var message: String? = null, - var path: String? = null -) \ No newline at end of file + var path: String? = null, +) diff --git a/src/main/java/org/radarbase/appserver/exception/entity/ErrorDetailsWithCause.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/entity/ErrorDetailsWithCause.kt similarity index 73% rename from src/main/java/org/radarbase/appserver/exception/entity/ErrorDetailsWithCause.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/entity/ErrorDetailsWithCause.kt index 896dd2436..e97576d4f 100644 --- a/src/main/java/org/radarbase/appserver/exception/entity/ErrorDetailsWithCause.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/entity/ErrorDetailsWithCause.kt @@ -7,5 +7,5 @@ class ErrorDetailsWithCause( status: Int, val cause: String?, message: String?, - path: String? -) : ErrorDetails(timestamp, status, message, path) \ No newline at end of file + path: String?, +) : ErrorDetails(timestamp, status, message, path) diff --git a/src/main/java/org/radarbase/appserver/exception/handler/ResponseEntityExceptionHandler.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/handler/ResponseEntityExceptionHandler.kt similarity index 86% rename from src/main/java/org/radarbase/appserver/exception/handler/ResponseEntityExceptionHandler.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/handler/ResponseEntityExceptionHandler.kt index ae84f6558..813c7dbb9 100644 --- a/src/main/java/org/radarbase/appserver/exception/handler/ResponseEntityExceptionHandler.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/exception/handler/ResponseEntityExceptionHandler.kt @@ -1,6 +1,13 @@ package org.radarbase.appserver.exception.handler -import org.radarbase.appserver.exception.* +import org.radarbase.appserver.exception.AlreadyExistsException +import org.radarbase.appserver.exception.FileStorageException +import org.radarbase.appserver.exception.InvalidFileDetailsException +import org.radarbase.appserver.exception.InvalidNotificationDetailsException +import org.radarbase.appserver.exception.InvalidPathDetailsException +import org.radarbase.appserver.exception.InvalidProjectDetailsException +import org.radarbase.appserver.exception.InvalidUserDetailsException +import org.radarbase.appserver.exception.NotFoundException import org.radarbase.appserver.exception.entity.ErrorDetails import org.radarbase.appserver.exception.entity.ErrorDetailsWithCause import org.springframework.http.HttpStatus @@ -27,7 +34,7 @@ class ResponseEntityExceptionHandler { status.value(), ex.cause!!.message, ex.message, - request.getDescription(false) + request.getDescription(false), ) } return ResponseEntity(error, status) @@ -97,14 +104,14 @@ class ResponseEntityExceptionHandler { status.value(), cause, ex.message, - request.getDescription(false) + request.getDescription(false), ) } else { ErrorDetails( Instant.now(), status.value(), ex.message, - request.getDescription(false) + request.getDescription(false), ) } return ResponseEntity(error, status) @@ -116,8 +123,8 @@ class ResponseEntityExceptionHandler { Instant.now(), status.value(), ex.message, - request.getDescription(false) + request.getDescription(false), ) return ResponseEntity(error, status) } -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/mapper/DataMessageMapper.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/mapper/DataMessageMapper.kt similarity index 100% rename from src/main/java/org/radarbase/appserver/mapper/DataMessageMapper.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/mapper/DataMessageMapper.kt diff --git a/src/main/java/org/radarbase/appserver/mapper/Mapper.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/mapper/Mapper.kt similarity index 95% rename from src/main/java/org/radarbase/appserver/mapper/Mapper.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/mapper/Mapper.kt index a0fd68f92..de1fd90ab 100644 --- a/src/main/java/org/radarbase/appserver/mapper/Mapper.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/mapper/Mapper.kt @@ -34,5 +34,5 @@ interface Mapper { fun dtoToEntity(dto: D): E fun entityToDto(entity: E): D fun entitiesToDtos(entities: Collection): List = entities.parallelStream().map(::entityToDto).collect(Collectors.toList()) - fun dtosToEntities(dtos: Collection): List = dtos.parallelStream().map ( ::dtoToEntity).collect(Collectors.toList()) -} \ No newline at end of file + fun dtosToEntities(dtos: Collection): List = dtos.parallelStream().map(::dtoToEntity).collect(Collectors.toList()) +} diff --git a/src/main/java/org/radarbase/appserver/mapper/NotificationMapper.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/mapper/NotificationMapper.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/mapper/NotificationMapper.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/mapper/NotificationMapper.kt index d0eea67ae..525803672 100644 --- a/src/main/java/org/radarbase/appserver/mapper/NotificationMapper.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/mapper/NotificationMapper.kt @@ -54,4 +54,3 @@ class NotificationMapper : Mapper { return FcmNotificationDto(entity) } } - diff --git a/src/main/java/org/radarbase/appserver/mapper/ProjectMapper.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/mapper/ProjectMapper.kt similarity index 97% rename from src/main/java/org/radarbase/appserver/mapper/ProjectMapper.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/mapper/ProjectMapper.kt index 9f0c06866..180d864ef 100644 --- a/src/main/java/org/radarbase/appserver/mapper/ProjectMapper.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/mapper/ProjectMapper.kt @@ -50,6 +50,6 @@ class ProjectMapper : Mapper { id = entity.id, projectId = entity.projectId, createdAt = entity.createdAt?.toInstant(), - updatedAt = entity.updatedAt?.toInstant() + updatedAt = entity.updatedAt?.toInstant(), ) -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/mapper/UserMapper.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/mapper/UserMapper.kt similarity index 95% rename from src/main/java/org/radarbase/appserver/mapper/UserMapper.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/mapper/UserMapper.kt index 8a4521bb7..bce0806bd 100644 --- a/src/main/java/org/radarbase/appserver/mapper/UserMapper.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/mapper/UserMapper.kt @@ -31,7 +31,7 @@ import java.time.Instant @Component @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON) -class UserMapper: Mapper { +class UserMapper : Mapper { override fun dtoToEntity(dto: FcmUserDto): User = User().apply { this.fcmToken = dto.fcmToken this.subjectId = dto.subjectId @@ -50,8 +50,8 @@ class UserMapper: Mapper { val lastOpened: Instant = fcmUserDto.lastOpened ?: Instant.now() return UserMetrics( lastOpened, - fcmUserDto.lastDelivered + fcmUserDto.lastDelivered, ) } } -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/repository/DataMessageRepository.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/repository/DataMessageRepository.kt similarity index 98% rename from src/main/java/org/radarbase/appserver/repository/DataMessageRepository.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/repository/DataMessageRepository.kt index 6980a8975..3855428d0 100644 --- a/src/main/java/org/radarbase/appserver/repository/DataMessageRepository.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/repository/DataMessageRepository.kt @@ -39,7 +39,7 @@ interface DataMessageRepository : JpaRepository { userId: Long?, sourceId: String?, scheduledTime: Instant?, - ttlSeconds: Int + ttlSeconds: Int, ): Boolean fun existsByIdAndUserId(id: Long?, userId: Long?): Boolean diff --git a/src/main/java/org/radarbase/appserver/repository/DataMessageStateEventRepository.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/repository/DataMessageStateEventRepository.kt similarity index 100% rename from src/main/java/org/radarbase/appserver/repository/DataMessageStateEventRepository.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/repository/DataMessageStateEventRepository.kt diff --git a/src/main/java/org/radarbase/appserver/repository/NotificationRepository.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/repository/NotificationRepository.kt similarity index 96% rename from src/main/java/org/radarbase/appserver/repository/NotificationRepository.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/repository/NotificationRepository.kt index da9d7490f..2c9e56d70 100644 --- a/src/main/java/org/radarbase/appserver/repository/NotificationRepository.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/repository/NotificationRepository.kt @@ -20,7 +20,6 @@ */ package org.radarbase.appserver.repository -import jakarta.validation.constraints.NotNull import org.radarbase.appserver.entity.Notification import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @@ -51,7 +50,7 @@ interface NotificationRepository : JpaRepository { title: String?, body: String?, type: String?, - ttlSeconds: Int + ttlSeconds: Int, ): Boolean fun findByUserIdAndSourceIdAndScheduledTimeAndTitleAndBodyAndTypeAndTtlSeconds( @@ -61,7 +60,7 @@ interface NotificationRepository : JpaRepository { title: String?, body: String?, type: String?, - ttlSeconds: Int + ttlSeconds: Int, ): Notification? fun existsByIdAndUserId(id: Long?, userId: Long?): Boolean diff --git a/src/main/java/org/radarbase/appserver/repository/NotificationStateEventRepository.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/repository/NotificationStateEventRepository.kt similarity index 100% rename from src/main/java/org/radarbase/appserver/repository/NotificationStateEventRepository.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/repository/NotificationStateEventRepository.kt diff --git a/src/main/java/org/radarbase/appserver/repository/ProjectRepository.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/repository/ProjectRepository.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/repository/ProjectRepository.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/repository/ProjectRepository.kt index 7f001517a..a8e443a6a 100644 --- a/src/main/java/org/radarbase/appserver/repository/ProjectRepository.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/repository/ProjectRepository.kt @@ -29,4 +29,4 @@ import org.springframework.stereotype.Repository interface ProjectRepository : JpaRepository { fun findByProjectId(projectId: String?): Project? fun existsByProjectId(projectId: String?): Boolean -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/repository/TaskRepository.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/repository/TaskRepository.kt similarity index 98% rename from src/main/java/org/radarbase/appserver/repository/TaskRepository.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/repository/TaskRepository.kt index 32fe2a18d..b99d9e3d2 100644 --- a/src/main/java/org/radarbase/appserver/repository/TaskRepository.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/repository/TaskRepository.kt @@ -50,7 +50,7 @@ interface TaskRepository : JpaRepository, JpaSpecificationExecutor { fun findByFcmToken(fcmToken: String?): User? override fun deleteById(id: Long) -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/search/SearchCriteria.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/search/SearchCriteria.kt similarity index 97% rename from src/main/java/org/radarbase/appserver/search/SearchCriteria.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/search/SearchCriteria.kt index b60f8c292..e2fecc2c1 100644 --- a/src/main/java/org/radarbase/appserver/search/SearchCriteria.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/search/SearchCriteria.kt @@ -24,10 +24,10 @@ package org.radarbase.appserver.search data class SearchCriteria( val key: String, val operation: String, - val value: Any + val value: Any, ) { /*** * Only AND supported in first instance. Later we can add a new query param that can provide this value */ fun isOrPredicate(): Boolean = false -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/search/TaskSpecification.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/search/TaskSpecification.kt similarity index 96% rename from src/main/java/org/radarbase/appserver/search/TaskSpecification.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/search/TaskSpecification.kt index 0ac37dc9d..702fd6991 100644 --- a/src/main/java/org/radarbase/appserver/search/TaskSpecification.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/search/TaskSpecification.kt @@ -34,9 +34,9 @@ class TaskSpecification(private val criteria: SearchCriteria) : Specification, query: CriteriaQuery<*>?, - builder: CriteriaBuilder + builder: CriteriaBuilder, ): Predicate? { - return when(criteria.operation) { + return when (criteria.operation) { ">" -> builder.greaterThanOrEqualTo(root.get(criteria.key), criteria.value.toString()) "<" -> builder.lessThanOrEqualTo(root.get(criteria.key), criteria.value.toString()) ":" -> { diff --git a/src/main/java/org/radarbase/appserver/search/TaskSpecificationsBuilder.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/search/TaskSpecificationsBuilder.kt similarity index 100% rename from src/main/java/org/radarbase/appserver/search/TaskSpecificationsBuilder.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/search/TaskSpecificationsBuilder.kt diff --git a/src/main/java/org/radarbase/appserver/service/DataMessageService.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/DataMessageService.kt similarity index 96% rename from src/main/java/org/radarbase/appserver/service/DataMessageService.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/DataMessageService.kt index 859bacc01..39d3ab7f4 100644 --- a/src/main/java/org/radarbase/appserver/service/DataMessageService.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/DataMessageService.kt @@ -20,4 +20,4 @@ */ package org.radarbase.appserver.service -interface DataMessageService +interface DataMessageService diff --git a/src/main/java/org/radarbase/appserver/service/DataMessageStateEventService.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/DataMessageStateEventService.kt similarity index 86% rename from src/main/java/org/radarbase/appserver/service/DataMessageStateEventService.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/DataMessageStateEventService.kt index 47d94283b..f9900c782 100644 --- a/src/main/java/org/radarbase/appserver/service/DataMessageStateEventService.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/DataMessageStateEventService.kt @@ -37,7 +37,7 @@ class DataMessageStateEventService( private val dataMessageStateEventRepository: DataMessageStateEventRepository, private val dataMessageService: FcmDataMessageService, private val dataMessageApplicationEventPublisher: ApplicationEventPublisher, - private val objectMapper: ObjectMapper + private val objectMapper: ObjectMapper, ) { @Transactional fun addDataMessageStateEvent(dataMessageStateEvent: DataMessageStateEvent) { @@ -46,10 +46,14 @@ class DataMessageStateEventService( @Transactional(readOnly = true) fun getDataMessageStateEvents( - projectId: String?, subjectId: String?, dataMessageId: Long + projectId: String?, + subjectId: String?, + dataMessageId: Long, ): List { dataMessageService.getDataMessageByProjectIdAndSubjectIdAndDataMessageId( - projectId, subjectId, dataMessageId + projectId, + subjectId, + dataMessageId, ) val stateEvents: List = dataMessageStateEventRepository.findByDataMessageId(dataMessageId) @@ -59,14 +63,14 @@ class DataMessageStateEventService( stateEvent.dataMessage!!.id, stateEvent.state, stateEvent.time, - stateEvent.associatedInfo + stateEvent.associatedInfo, ) } } @Transactional(readOnly = true) fun getDataMessageStateEventsByDataMessageId( - dataMessageId: Long + dataMessageId: Long, ): List { val stateEvents: List = dataMessageStateEventRepository.findByDataMessageId(dataMessageId) @@ -76,7 +80,7 @@ class DataMessageStateEventService( stateEvent.dataMessage!!.id, stateEvent.state, stateEvent.time, - stateEvent.associatedInfo + stateEvent.associatedInfo, ) } } @@ -86,12 +90,14 @@ class DataMessageStateEventService( projectId: String?, subjectId: String?, dataMessageId: Long, - dataMessageStateEventDto: DataMessageStateEventDto + dataMessageStateEventDto: DataMessageStateEventDto, ) { checkState(dataMessageId, dataMessageStateEventDto.state) val dataMessage = dataMessageService.getDataMessageByProjectIdAndSubjectIdAndDataMessageId( - projectId, subjectId, dataMessageId + projectId, + subjectId, + dataMessageId, ) var additionalInfo: Map? = null @@ -101,10 +107,11 @@ class DataMessageStateEventService( objectMapper.readValue>( dataMessageStateEventDto.associatedInfo, object : TypeReference>() { - }) + }, + ) } catch (exc: IOException) { throw IllegalStateException( - "Cannot convert additionalInfo to Map. Please check its format." + "Cannot convert additionalInfo to Map. Please check its format.", ) } } @@ -115,18 +122,19 @@ class DataMessageStateEventService( dataMessage, dataMessageStateEventDto.state!!, additionalInfo, - dataMessageStateEventDto.time!! + dataMessageStateEventDto.time!!, ) dataMessageApplicationEventPublisher.publishEvent(stateEvent) } - @Throws( IllegalStateException::class) + @Throws(IllegalStateException::class) private fun checkState(dataMessageId: Long, state: MessageState?) { if (EXTERNAL_EVENTS.contains(state)) { if (dataMessageStateEventRepository.countByDataMessageId(dataMessageId) - >= MAX_NUMBER_OF_STATES) { + >= MAX_NUMBER_OF_STATES + ) { throw IllegalStateException( - ("The max limit of state changes($MAX_NUMBER_OF_STATES) has been reached. Cannot add new states.") + ("The max limit of state changes($MAX_NUMBER_OF_STATES) has been reached. Cannot add new states."), ) } } else { @@ -140,7 +148,7 @@ class DataMessageStateEventService( MessageState.DISMISSED, MessageState.OPENED, MessageState.UNKNOWN, - MessageState.ERRORED + MessageState.ERRORED, ) private const val MAX_NUMBER_OF_STATES = 20 } diff --git a/src/main/java/org/radarbase/appserver/service/FcmDataMessageService.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/FcmDataMessageService.kt similarity index 91% rename from src/main/java/org/radarbase/appserver/service/FcmDataMessageService.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/FcmDataMessageService.kt index 05956cdc2..51faf9a43 100644 --- a/src/main/java/org/radarbase/appserver/service/FcmDataMessageService.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/FcmDataMessageService.kt @@ -59,7 +59,7 @@ class FcmDataMessageService( private val projectRepository: ProjectRepository, private val schedulerService: MessageSchedulerService, private val dataMessageConverter: DataMessageMapper, - private val dataMessageStateEventPublisher: ApplicationEventPublisher? + private val dataMessageStateEventPublisher: ApplicationEventPublisher?, ) : DataMessageService { // TODO Add option to specify a scheduling provider (default will be fcm) @@ -92,7 +92,8 @@ class FcmDataMessageService( @Transactional(readOnly = true) fun getDataMessagesByProjectIdAndSubjectId( - projectId: String, subjectId: String + projectId: String, + subjectId: String, ): FcmDataMessages { val user = subjectAndProjectExistElseThrow(subjectId, projectId) @@ -137,12 +138,14 @@ class FcmDataMessageService( ttlSeconds: Int, startTime: LocalDateTime?, endTime: LocalDateTime?, - limit: Int + limit: Int, ): FcmDataMessages? = null @Transactional fun addDataMessage( - dataMessageDto: FcmDataMessageDto, subjectId: String?, projectId: String? + dataMessageDto: FcmDataMessageDto, + subjectId: String?, + projectId: String?, ): FcmDataMessageDto { val user = subjectAndProjectExistElseThrow(subjectId, projectId) if (!dataMessageRepository @@ -150,29 +153,34 @@ class FcmDataMessageService( user.id, dataMessageDto.sourceId, dataMessageDto.scheduledTime, - dataMessageDto.ttlSeconds + dataMessageDto.ttlSeconds, ) ) { val dataMessageSaved = this.dataMessageRepository.saveAndFlush( - DataMessageBuilder(dataMessageConverter.dtoToEntity(dataMessageDto)).user(user).build() + DataMessageBuilder(dataMessageConverter.dtoToEntity(dataMessageDto)).user(user).build(), ) user.usermetrics!!.lastOpened = Instant.now() this.userRepository.save(user) addDataMessageStateEvent( - dataMessageSaved, MessageState.ADDED, dataMessageSaved.createdAt!!.toInstant() + dataMessageSaved, + MessageState.ADDED, + dataMessageSaved.createdAt!!.toInstant(), ) this.schedulerService.schedule(dataMessageSaved) return dataMessageConverter.entityToDto(dataMessageSaved) } else { throw AlreadyExistsException( - "The Data Message Already exists. Please Use update endpoint", dataMessageDto + "The Data Message Already exists. Please Use update endpoint", + dataMessageDto, ) } } private fun addDataMessageStateEvent( - dataMessage: DataMessage, state: MessageState, time: Instant + dataMessage: DataMessage, + state: MessageState, + time: Instant, ) { if (dataMessageStateEventPublisher != null) { val dataMessageStateEvent = @@ -183,7 +191,9 @@ class FcmDataMessageService( @Transactional fun updateDataMessage( - dataMessageDto: FcmDataMessageDto, subjectId: String, projectId: String + dataMessageDto: FcmDataMessageDto, + subjectId: String, + projectId: String, ): FcmDataMessageDto { checkInvalidDetails ({ dataMessageDto.id == null @@ -210,7 +220,9 @@ class FcmDataMessageService( val dataMessageSaved = this.dataMessageRepository.saveAndFlush(newDataMessage) addDataMessageStateEvent( - dataMessageSaved, MessageState.UPDATED, dataMessageSaved.updatedAt!!.toInstant() + dataMessageSaved, + MessageState.UPDATED, + dataMessageSaved.updatedAt!!.toInstant(), ) if (!dataMessage.delivered) { this.schedulerService.updateScheduled(dataMessageSaved) @@ -240,7 +252,6 @@ class FcmDataMessageService( val newDataMessage = DataMessageBuilder(dataMessage).delivered(isDelivered).build() this.dataMessageRepository.save(newDataMessage) - } // TODO: Investigate if data messages/notifications can be marked in the state CANCELLED when deleted. @@ -251,11 +262,11 @@ class FcmDataMessageService( if (this.dataMessageRepository.existsByIdAndUserId(id, user.id)) { this.dataMessageRepository.deleteByIdAndUserId( id, - user.id + user.id, ) } else { throw InvalidNotificationDetailsException( - "Data message with the provided ID does not exist." + "Data message with the provided ID does not exist.", ) } } @@ -275,7 +286,9 @@ class FcmDataMessageService( @Transactional fun addDataMessages( - dataMessageDtos: FcmDataMessages, subjectId: String, projectId: String + dataMessageDtos: FcmDataMessages, + subjectId: String, + projectId: String, ): FcmDataMessages { val user = subjectAndProjectExistElseThrow(subjectId, projectId) val dataMessages = dataMessageRepository.findByUserId(user.id) @@ -288,13 +301,13 @@ class FcmDataMessageService( val savedDataMessages: List = this.dataMessageRepository.saveAll(newDataMessages) this.dataMessageRepository.flush() - savedDataMessages.forEach{ dm -> - addDataMessageStateEvent( - dm, - MessageState.ADDED, - dm.createdAt!!.toInstant() - ) - } + savedDataMessages.forEach { dm -> + addDataMessageStateEvent( + dm, + MessageState.ADDED, + dm.createdAt!!.toInstant(), + ) + } this.schedulerService.scheduleMultiple(savedDataMessages) return FcmDataMessages() @@ -305,7 +318,7 @@ class FcmDataMessageService( val project = this.projectRepository.findByProjectId(projectId) if (project == null || project.id == null) { throw NotFoundException( - "Project Id does not exist. Please create a project with the ID first" + "Project Id does not exist. Please create a project with the ID first", ) } @@ -319,7 +332,9 @@ class FcmDataMessageService( @Transactional(readOnly = true) fun getDataMessageByProjectIdAndSubjectIdAndDataMessageId( - projectId: String?, subjectId: String?, dataMessageId: Long + projectId: String?, + subjectId: String?, + dataMessageId: Long, ): DataMessage { val user = subjectAndProjectExistElseThrow(subjectId, projectId) diff --git a/src/main/java/org/radarbase/appserver/service/FcmNotificationService.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/FcmNotificationService.kt similarity index 90% rename from src/main/java/org/radarbase/appserver/service/FcmNotificationService.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/FcmNotificationService.kt index fe028c611..e69d4a71d 100644 --- a/src/main/java/org/radarbase/appserver/service/FcmNotificationService.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/FcmNotificationService.kt @@ -60,7 +60,7 @@ class FcmNotificationService( private val projectRepository: ProjectRepository, private val schedulerService: MessageSchedulerService, private val notificationConverter: NotificationMapper, - private val notificationStateEventPublisher: ApplicationEventPublisher? + private val notificationStateEventPublisher: ApplicationEventPublisher?, ) : NotificationService { // TODO Add option to specify a scheduling provider (default will be fcm) @@ -92,7 +92,8 @@ class FcmNotificationService( @Transactional(readOnly = true) fun getNotificationsByProjectIdAndSubjectId( - projectId: String?, subjectId: String? + projectId: String?, + subjectId: String?, ): FcmNotifications { return subjectAndProjectExistElseThrow(subjectId, projectId).let { user -> notificationRepository.findByUserId(user.id) @@ -142,12 +143,15 @@ class FcmNotificationService( ttlSeconds: Int, startTime: LocalDateTime?, endTime: LocalDateTime?, - limit: Int + limit: Int, ): FcmNotifications? = null @Transactional fun addNotification( - notificationDto: FcmNotificationDto, subjectId: String?, projectId: String?, schedule: Boolean + notificationDto: FcmNotificationDto, + subjectId: String?, + projectId: String?, + schedule: Boolean, ): FcmNotificationDto { val user = subjectAndProjectExistElseThrow(subjectId, projectId) val notification: Notification? = notificationRepository @@ -158,17 +162,19 @@ class FcmNotificationService( notificationDto.title, notificationDto.body, notificationDto.type, - notificationDto.ttlSeconds + notificationDto.ttlSeconds, ) if (notification == null) { val notificationSaved = this.notificationRepository.saveAndFlush( - NotificationBuilder(notificationConverter.dtoToEntity(notificationDto)).user(user).build() + NotificationBuilder(notificationConverter.dtoToEntity(notificationDto)).user(user).build(), ) user.usermetrics!!.lastOpened = Instant.now() this.userRepository.save(user) addNotificationStateEvent( - notificationSaved, MessageState.ADDED, notificationSaved.createdAt!!.toInstant() + notificationSaved, + MessageState.ADDED, + notificationSaved.createdAt!!.toInstant(), ) if (schedule) { this.schedulerService.schedule(notificationSaved) @@ -177,14 +183,16 @@ class FcmNotificationService( } else { throw NotificationAlreadyExistsException( "The Notification Already exists. Please Use update endpoint", - notificationConverter.entityToDto(notification) + notificationConverter.entityToDto(notification), ) } } @Transactional fun addNotification( - notificationDto: FcmNotificationDto, subjectId: String?, projectId: String? + notificationDto: FcmNotificationDto, + subjectId: String?, + projectId: String?, ): FcmNotificationDto { val user = subjectAndProjectExistElseThrow(subjectId, projectId) val notification: Notification? = notificationRepository @@ -195,30 +203,34 @@ class FcmNotificationService( notificationDto.title, notificationDto.body, notificationDto.type, - notificationDto.ttlSeconds + notificationDto.ttlSeconds, ) if (notification == null) { val notificationSaved = this.notificationRepository.saveAndFlush( - NotificationBuilder(notificationConverter.dtoToEntity(notificationDto)).user(user).build() + NotificationBuilder(notificationConverter.dtoToEntity(notificationDto)).user(user).build(), ) user.usermetrics!!.lastOpened = Instant.now() this.userRepository.save(user) addNotificationStateEvent( - notificationSaved, MessageState.ADDED, notificationSaved.createdAt!!.toInstant() + notificationSaved, + MessageState.ADDED, + notificationSaved.createdAt!!.toInstant(), ) this.schedulerService.schedule(notificationSaved) return notificationConverter.entityToDto(notificationSaved) } else { throw NotificationAlreadyExistsException( "The Notification Already exists. Please Use update endpoint", - notificationConverter.entityToDto(notification) + notificationConverter.entityToDto(notification), ) } } private fun addNotificationStateEvent( - notification: Notification, state: MessageState, time: Instant + notification: Notification, + state: MessageState, + time: Instant, ) { if (notificationStateEventPublisher != null) { val notificationStateEvent = @@ -229,7 +241,9 @@ class FcmNotificationService( @Transactional fun updateNotification( - notificationDto: FcmNotificationDto, subjectId: String?, projectId: String? + notificationDto: FcmNotificationDto, + subjectId: String?, + projectId: String?, ): FcmNotificationDto { val notificationId = notificationDto.id @@ -256,7 +270,9 @@ class FcmNotificationService( val notificationSaved = this.notificationRepository.saveAndFlush(newNotification) addNotificationStateEvent( - notificationSaved, MessageState.UPDATED, notificationSaved.updatedAt!!.toInstant() + notificationSaved, + MessageState.UPDATED, + notificationSaved.updatedAt!!.toInstant(), ) if (!notification.delivered) { this.schedulerService.updateScheduled(notificationSaved) @@ -303,7 +319,7 @@ class FcmNotificationService( { notification == null }, { "Notification with the provided FCM message ID does not exist." - } + }, ) val newNotif = NotificationBuilder(notification).delivered(isDelivered).build() this.notificationRepository.save(newNotif) @@ -317,12 +333,14 @@ class FcmNotificationService( if (this.notificationRepository.existsByIdAndUserId(id, userId)) { this.schedulerService.deleteScheduled( - this.notificationRepository.findByIdAndUserId(id, userId!!)!! + this.notificationRepository.findByIdAndUserId(id, userId!!)!!, ) this.notificationRepository.deleteByIdAndUserId(id, userId) - } else throw InvalidNotificationDetailsException( - "Notification with the provided ID does not exist." - ) + } else { + throw InvalidNotificationDetailsException( + "Notification with the provided ID does not exist.", + ) + } } @Transactional @@ -341,12 +359,11 @@ class FcmNotificationService( checkInvalidDetails( { user == null }, - { "The user with the given Fcm Token does not exist" } + { "The user with the given Fcm Token does not exist" }, ) - this.schedulerService.deleteScheduledMultiple( - this.notificationRepository.findByUserId(user!!.id) + this.notificationRepository.findByUserId(user!!.id), ) this.notificationRepository.deleteByUserId(user.id) @@ -364,9 +381,11 @@ class FcmNotificationService( @Transactional fun addNotifications( - notificationDtos: FcmNotifications, subjectId: String?, projectId: String?, schedule: Boolean + notificationDtos: FcmNotifications, + subjectId: String?, + projectId: String?, + schedule: Boolean, ): FcmNotifications { - val newNotifications: List = subjectAndProjectExistElseThrow(subjectId, projectId).let { user -> notificationRepository.findByUserId(user.id).let { notifications -> notificationDtos.notifications.map { dto: FcmNotificationDto -> @@ -385,7 +404,7 @@ class FcmNotificationService( addNotificationStateEvent( n, MessageState.ADDED, - n.createdAt!!.toInstant() + n.createdAt!!.toInstant(), ) } @@ -408,7 +427,7 @@ class FcmNotificationService( notification.title, notification.body, notification.type, - notification.ttlSeconds + notification.ttlSeconds, ) == null } @@ -417,7 +436,7 @@ class FcmNotificationService( addNotificationStateEvent( n!!, MessageState.ADDED, - n.createdAt!!.toInstant() + n.createdAt!!.toInstant(), ) } this.schedulerService.scheduleMultiple(savedNotifications) @@ -426,9 +445,10 @@ class FcmNotificationService( @Transactional fun addNotifications( - notificationDtos: FcmNotifications, subjectId: String?, projectId: String? + notificationDtos: FcmNotifications, + subjectId: String?, + projectId: String?, ): FcmNotifications { - val newNotifications: List = subjectAndProjectExistElseThrow(subjectId, projectId).let { user -> notificationRepository.findByUserId(user.id).let { notifications -> notificationDtos.notifications.map { dto: FcmNotificationDto -> @@ -447,7 +467,7 @@ class FcmNotificationService( addNotificationStateEvent( n, MessageState.ADDED, - n.createdAt!!.toInstant() + n.createdAt!!.toInstant(), ) } @@ -468,7 +488,9 @@ class FcmNotificationService( @Transactional(readOnly = true) fun getNotificationByProjectIdAndSubjectIdAndNotificationId( - projectId: String?, subjectId: String?, notificationId: Long + projectId: String?, + subjectId: String?, + notificationId: Long, ): Notification { val user = subjectAndProjectExistElseThrow(subjectId, projectId) diff --git a/src/main/java/org/radarbase/appserver/service/GithubClient.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/GithubClient.kt similarity index 93% rename from src/main/java/org/radarbase/appserver/service/GithubClient.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/GithubClient.kt index ecfacf5df..6670d7b51 100644 --- a/src/main/java/org/radarbase/appserver/service/GithubClient.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/GithubClient.kt @@ -21,12 +21,13 @@ package org.radarbase.appserver.service -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.engine.cio.* +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.engine.cio.CIO import io.ktor.client.plugins.defaultRequest -import io.ktor.client.request.* -import io.ktor.http.* +import io.ktor.client.request.header +import io.ktor.client.request.prepareGet +import io.ktor.http.HttpHeaders import kotlinx.coroutines.runBlocking import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value @@ -56,7 +57,7 @@ class GithubClient( private val maxContentLength: Int, @Value("\${security.github.client.timeout:10}") httpTimeout: Int, - @Value("\${security.github.client.token:}") githubToken: String + @Value("\${security.github.client.token:}") githubToken: String, ) { @Transient @@ -108,7 +109,8 @@ class GithubClient( } else { logger.error("Error getting Github content from URL {} : {}", url, response) throw ResponseStatusException( - HttpStatus.valueOf(response.status.value), "Github content could not be retrieved" + HttpStatus.valueOf(response.status.value), + "Github content could not be retrieved", ) } } @@ -124,7 +126,8 @@ class GithubClient( private fun checkContentLength(contentLength: Int) { if (contentLength > maxContentLength) { throw ResponseStatusException( - HttpStatus.BAD_REQUEST, "Github content is too large" + HttpStatus.BAD_REQUEST, + "Github content is too large", ) } } diff --git a/src/main/java/org/radarbase/appserver/service/GithubService.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/GithubService.kt similarity index 97% rename from src/main/java/org/radarbase/appserver/service/GithubService.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/GithubService.kt index 02e9f9de7..eb22f2bc7 100644 --- a/src/main/java/org/radarbase/appserver/service/GithubService.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/GithubService.kt @@ -44,12 +44,12 @@ import java.time.Duration */ @Component @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON) -class GithubService ( +class GithubService( @field:Transient private val githubClient: GithubClient, @Value("\${security.github.cache.duration:3600}") cacheTime: Int, @Value("\${security.github.cache.retryDuration:60}") retryTime: Int, - @Value("\${security.github.cache.size:10000}") maxSize: Int + @Value("\${security.github.cache.size:10000}") maxSize: Int, ) { @Transient @@ -57,7 +57,7 @@ class GithubService ( githubClient::getGithubContent, Duration.ofSeconds(cacheTime.toLong()), Duration.ofSeconds(retryTime.toLong()), - maxSize + maxSize, ) /** diff --git a/src/main/java/org/radarbase/appserver/service/ManagementPortalService.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/ManagementPortalService.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/service/ManagementPortalService.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/ManagementPortalService.kt index 27b083841..b496a77f4 100644 --- a/src/main/java/org/radarbase/appserver/service/ManagementPortalService.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/ManagementPortalService.kt @@ -22,4 +22,3 @@ package org.radarbase.appserver.service class ManagementPortalService // TODO WIP - Add MP client and get subjects and projects info if missing in any request. - diff --git a/src/main/java/org/radarbase/appserver/service/MessageType.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/MessageType.kt similarity index 98% rename from src/main/java/org/radarbase/appserver/service/MessageType.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/MessageType.kt index 5bd75bc05..ccfba2b56 100644 --- a/src/main/java/org/radarbase/appserver/service/MessageType.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/MessageType.kt @@ -23,5 +23,5 @@ package org.radarbase.appserver.service enum class MessageType { NOTIFICATION, DATA, - UNKNOWN + UNKNOWN, } diff --git a/src/main/java/org/radarbase/appserver/service/NotificationService.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/NotificationService.kt similarity index 96% rename from src/main/java/org/radarbase/appserver/service/NotificationService.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/NotificationService.kt index 4067fd43e..c26548fd5 100644 --- a/src/main/java/org/radarbase/appserver/service/NotificationService.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/NotificationService.kt @@ -20,4 +20,4 @@ */ package org.radarbase.appserver.service -interface NotificationService +interface NotificationService diff --git a/src/main/java/org/radarbase/appserver/service/NotificationStateEventService.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/NotificationStateEventService.kt similarity index 87% rename from src/main/java/org/radarbase/appserver/service/NotificationStateEventService.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/NotificationStateEventService.kt index 83bee9579..5ef7e6fb9 100644 --- a/src/main/java/org/radarbase/appserver/service/NotificationStateEventService.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/NotificationStateEventService.kt @@ -36,7 +36,7 @@ class NotificationStateEventService( private val notificationStateEventRepository: NotificationStateEventRepository, private val notificationService: FcmNotificationService, private val notificationApplicationEventPublisher: ApplicationEventPublisher, - private val objectMapper: ObjectMapper + private val objectMapper: ObjectMapper, ) { @Transactional fun addNotificationStateEvent(notificationStateEvent: NotificationStateEvent) { @@ -49,10 +49,14 @@ class NotificationStateEventService( @Transactional(readOnly = true) fun getNotificationStateEvents( - projectId: String?, subjectId: String?, notificationId: Long + projectId: String?, + subjectId: String?, + notificationId: Long, ): List { notificationService.getNotificationByProjectIdAndSubjectIdAndNotificationId( - projectId, subjectId, notificationId + projectId, + subjectId, + notificationId, ) val stateEvents: List = notificationStateEventRepository.findByNotificationId(notificationId) @@ -62,14 +66,14 @@ class NotificationStateEventService( notificationState?.notification?.id, notificationState?.state, notificationState?.time, - notificationState?.associatedInfo + notificationState?.associatedInfo, ) } } @Transactional(readOnly = true) fun getNotificationStateEventsByNotificationId( - notificationId: Long + notificationId: Long, ): List { val stateEvents = notificationStateEventRepository.findByNotificationId(notificationId) return stateEvents.map { notificationState: NotificationStateEvent? -> @@ -78,7 +82,7 @@ class NotificationStateEventService( notificationState?.notification?.id, notificationState?.state, notificationState?.time, - notificationState?.associatedInfo + notificationState?.associatedInfo, ) } } @@ -88,11 +92,13 @@ class NotificationStateEventService( projectId: String?, subjectId: String?, notificationId: Long, - notificationStateEventDto: NotificationStateEventDto + notificationStateEventDto: NotificationStateEventDto, ) { checkState(notificationId, notificationStateEventDto.state) val notification = notificationService.getNotificationByProjectIdAndSubjectIdAndNotificationId( - projectId, subjectId, notificationId + projectId, + subjectId, + notificationId, ) var additionalInfo: Map? = null @@ -101,21 +107,22 @@ class NotificationStateEventService( additionalInfo = objectMapper.readValue>( notificationStateEventDto.associatedInfo, object : TypeReference>() { - }) + }, + ) } catch (_: IOException) { throw IllegalStateException( - "Cannot convert additionalInfo to Map. Please check its format." + "Cannot convert additionalInfo to Map. Please check its format.", ) } } val stateEvent = org.radarbase.appserver.event.state.dto.NotificationStateEventDto( - this, - notification, - notificationStateEventDto.state!!, - additionalInfo, - notificationStateEventDto.time!! - ) + this, + notification, + notificationStateEventDto.state!!, + additionalInfo, + notificationStateEventDto.time!!, + ) notificationApplicationEventPublisher.publishEvent(stateEvent) } @@ -140,7 +147,7 @@ class NotificationStateEventService( MessageState.DISMISSED, MessageState.OPENED, MessageState.UNKNOWN, - MessageState.ERRORED + MessageState.ERRORED, ) } } diff --git a/src/main/java/org/radarbase/appserver/service/ProjectService.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/ProjectService.kt similarity index 96% rename from src/main/java/org/radarbase/appserver/service/ProjectService.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/ProjectService.kt index 9ec169fb5..ec9b875f5 100644 --- a/src/main/java/org/radarbase/appserver/service/ProjectService.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/ProjectService.kt @@ -48,7 +48,7 @@ import org.springframework.transaction.annotation.Transactional @Transactional class ProjectService( private val projectRepository: ProjectRepository, - private val projectMapper: ProjectMapper + private val projectMapper: ProjectMapper, ) { /** * Retrieves all projects from the repository. @@ -103,13 +103,13 @@ class ProjectService( checkInvalidProjectDetails( projectDTO, { projectDTO.id != null }, - { "'id' must not be supplied when creating a project, it is autogenerated" } + { "'id' must not be supplied when creating a project, it is autogenerated" }, ) checkInvalidProjectDetails( projectDTO, { projectId == null }, - { "At least 'project id' must be supplied" } + { "At least 'project id' must be supplied" }, ) if (projectRepository.existsByProjectId(projectId!!)) { @@ -130,10 +130,10 @@ class ProjectService( * @throws NotFoundException if the project to update does not exist */ fun updateProject(projectDTO: ProjectDto): ProjectDto { - checkInvalidProjectDetails ( + checkInvalidProjectDetails( projectDTO, { projectDTO.id == null }, - { "The 'id' of the project must be supplied for updating project" } + { "The 'id' of the project must be supplied for updating project" }, ) val existingProject: Project = @@ -143,4 +143,4 @@ class ProjectService( return projectMapper.entityToDto(savedProject) } -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/service/QuestionnaireScheduleService.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/QuestionnaireScheduleService.kt similarity index 97% rename from src/main/java/org/radarbase/appserver/service/QuestionnaireScheduleService.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/QuestionnaireScheduleService.kt index 4e936d3da..f19278d7d 100644 --- a/src/main/java/org/radarbase/appserver/service/QuestionnaireScheduleService.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/QuestionnaireScheduleService.kt @@ -55,7 +55,7 @@ class QuestionnaireScheduleService( private val scheduleGeneratorService: QuestionnaireScheduleGeneratorService, private val projectRepository: ProjectRepository, private val taskService: TaskService, - private val notificationService: FcmNotificationService + private val notificationService: FcmNotificationService, ) { private val subjectScheduleMap: MutableMap = hashMapOf() @@ -69,7 +69,7 @@ class QuestionnaireScheduleService( projectId: String?, subjectId: String?, type: AssessmentType?, - search: String? + search: String?, ): List { getSearchBuilder(projectId, subjectId, type, search).build().also { spec -> return this.taskService.getTasksBySpecification(spec) @@ -89,7 +89,7 @@ class QuestionnaireScheduleService( subjectId: String?, projectId: String?, startTime: Instant?, - endTime: Instant? + endTime: Instant?, ): List { val user: User = subjectAndProjectExistsElseThrow(subjectId, projectId) val tasks: MutableList = this.getTasksForUser(user).toMutableList() @@ -152,15 +152,14 @@ class QuestionnaireScheduleService( fun generateScheduleUsingProjectIdAndSubjectIdAndAssessment( projectId: String, subjectId: String, - assessment: Assessment + assessment: Assessment, ): Schedule { - val user: User = subjectAndProjectExistsElseThrow(subjectId, projectId) val protocol: Protocol? = protocolGenerator.getProtocolForSubject(subjectId) checkInvalidDetails( - { protocol == null || !protocol.hasAssessment(assessment.name) }, - { "Assessment not found in protocol. Add assessment to protocol first" } + { protocol == null || !protocol.hasAssessment(assessment.name) }, + { "Assessment not found in protocol. Add assessment to protocol first" }, ) val userTimeZone = user.timezone @@ -171,7 +170,7 @@ class QuestionnaireScheduleService( assessment, user, emptyList(), - userTimeZone + userTimeZone, ) schedule.addAssessmentSchedule(assessmentSchedule) @@ -223,7 +222,7 @@ class QuestionnaireScheduleService( projectId: String?, subjectId: String?, type: AssessmentType?, - search: String? + search: String?, ): TaskSpecificationsBuilder { val builder = TaskSpecificationsBuilder() @@ -251,4 +250,4 @@ class QuestionnaireScheduleService( private val TASK_SEARCH_PATTERN = Regex("(\\w+)([:<>])(\\w+)") private val COMMA_PATTERN = Regex(",") } -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/service/TaskService.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/TaskService.kt similarity index 91% rename from src/main/java/org/radarbase/appserver/service/TaskService.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/TaskService.kt index 05ba28b03..1306d54b3 100644 --- a/src/main/java/org/radarbase/appserver/service/TaskService.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/TaskService.kt @@ -48,10 +48,10 @@ import java.time.Instant class TaskService( private val taskRepository: TaskRepository, private val userRepository: UserRepository, - private val eventPublisher: ApplicationEventPublisher? + private val eventPublisher: ApplicationEventPublisher?, ) { @Transactional(readOnly = true) - fun getAllProjects(): List { + fun getAllTasks(): List { return taskRepository.findAll() } @@ -114,23 +114,26 @@ class TaskService( this.userRepository.save(user) addTaskStateEvent(saved, TaskState.ADDED, saved.createdAt!!.toInstant()) return saved - } else throw AlreadyExistsException( - "The Task Already exists. Please Use update endpoint", task - ) + } else { + throw AlreadyExistsException( + "The Task Already exists. Please Use update endpoint", + task, + ) + } } @Transactional fun addTasks(tasks: List?, user: User): List { val newTasks = tasks?.filter { task -> - !this.taskRepository.existsByUserIdAndNameAndTimestamp( - user.id, - task.name, - task.timestamp - ) - } ?: return emptyList() + !this.taskRepository.existsByUserIdAndNameAndTimestamp( + user.id, + task.name, + task.timestamp, + ) + } ?: return emptyList() val saved = this.taskRepository.saveAllAndFlush(newTasks) - saved.forEach{ t -> + saved.forEach { t -> addTaskStateEvent(t, TaskState.ADDED, t!!.createdAt!!.toInstant()) } diff --git a/src/main/java/org/radarbase/appserver/service/TaskStateEventService.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/TaskStateEventService.kt similarity index 90% rename from src/main/java/org/radarbase/appserver/service/TaskStateEventService.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/TaskStateEventService.kt index 8c96940ac..56a894d32 100644 --- a/src/main/java/org/radarbase/appserver/service/TaskStateEventService.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/TaskStateEventService.kt @@ -39,13 +39,13 @@ class TaskStateEventService( private val taskService: TaskService, private val notificationService: FcmNotificationService, private val taskApplicationEventPublisher: ApplicationEventPublisher, - private val objectMapper: ObjectMapper + private val objectMapper: ObjectMapper, ) { @Transactional fun addTaskStateEvent(taskStateEvent: TaskStateEvent) { taskStateEventRepository.save(taskStateEvent) - taskService.updateTaskStatus(taskStateEvent.task!!, taskStateEvent.state!! ) + taskService.updateTaskStatus(taskStateEvent.task!!, taskStateEvent.state!!) if (taskStateEvent.state == TaskState.COMPLETED) { notificationService.deleteNotificationsByTaskId(taskStateEvent.task) } @@ -53,7 +53,9 @@ class TaskStateEventService( @Transactional(readOnly = true) fun getTaskStateEvents( - projectId: String?, subjectId: String?, taskId: Long + projectId: String?, + subjectId: String?, + taskId: Long, ): List { val task = taskService.getTaskById(taskId) val stateEvents = taskStateEventRepository.findByTaskId(taskId) @@ -63,14 +65,14 @@ class TaskStateEventService( taskId = task.id, state = ns.state, time = ns.time, - associatedInfo = ns.associatedInfo + associatedInfo = ns.associatedInfo, ) } } @Transactional(readOnly = true) fun getTaskStateEventsByTaskId( - taskId: Long + taskId: Long, ): List { val stateEvents: List = taskStateEventRepository.findByTaskId(taskId) return stateEvents.map { ns -> @@ -79,7 +81,7 @@ class TaskStateEventService( taskId = ns.task?.id, state = ns.state, time = ns.time, - associatedInfo = ns.associatedInfo + associatedInfo = ns.associatedInfo, ) } } @@ -90,7 +92,7 @@ class TaskStateEventService( projectId: String, subjectId: String, taskId: Long, - taskStateEventDto: TaskStateEventDto + taskStateEventDto: TaskStateEventDto, ) { val taskState = requireNotNull(taskStateEventDto.state) { "State is missing" } checkState(taskId, taskState) @@ -100,12 +102,12 @@ class TaskStateEventService( try { objectMapper.readValue( taskStateEventDto.associatedInfo, - object : TypeReference>() {} + object : TypeReference>() {}, ) } catch (exc: IOException) { throw IllegalStateException( "Cannot convert additionalInfo to Map. Please check its format.", - exc + exc, ) } } else { @@ -117,7 +119,7 @@ class TaskStateEventService( task, taskStateEventDto.state, additionalInfo, - taskStateEventDto.time + taskStateEventDto.time, ) taskApplicationEventPublisher.publishEvent(stateEvent) } @@ -127,12 +129,12 @@ class TaskStateEventService( if (state in EXTERNAL_EVENTS) { if (taskStateEventRepository.countByTaskId(taskId) >= MAX_NUMBER_OF_STATES) { throw SizeLimitExceededException( - "The max limit of state changes($MAX_NUMBER_OF_STATES) has been reached. Cannot add new states." + "The max limit of state changes($MAX_NUMBER_OF_STATES) has been reached. Cannot add new states.", ) } } else { throw IllegalStateException( - "The state $state is not an external state and cannot be updated by this endpoint." + "The state $state is not an external state and cannot be updated by this endpoint.", ) } } @@ -141,7 +143,7 @@ class TaskStateEventService( private val EXTERNAL_EVENTS: Set = setOf( TaskState.COMPLETED, TaskState.UNKNOWN, - TaskState.ERRORED + TaskState.ERRORED, ) private const val MAX_NUMBER_OF_STATES = 20 diff --git a/src/main/java/org/radarbase/appserver/service/UserService.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/UserService.kt similarity index 95% rename from src/main/java/org/radarbase/appserver/service/UserService.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/UserService.kt index 42e3a41ed..cabf63707 100644 --- a/src/main/java/org/radarbase/appserver/service/UserService.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/UserService.kt @@ -136,8 +136,9 @@ class UserService( val user: User = checkPresence( userRepository.findBySubjectIdAndProjectId( - subjectId, project.id - ) + subjectId, + project.id, + ), ) { "User with subjectId $subjectId not found" } return userMapper.entityToDto(user) @@ -181,21 +182,25 @@ class UserService( } val user: User? = userRepository.findBySubjectIdAndProjectId( - userDto.subjectId, requireNotNull(project.id) { "Project id must not be null" }) + userDto.subjectId, + requireNotNull(project.id) { "Project id must not be null" }, + ) checkInvalidDetails( { user != null }, { "User with subjectId ${userDto.subjectId} already exists with projectId ${userDto.projectId}. " + - "Please use update endpoint if you need to update user" - }) + "Please use update endpoint if you need to update user" + }, + ) val email: String? = userDto.email if (sendEmailNotifications && (email == null || email.isEmpty())) { logger.warn( "No email address was provided for new subject '{}'. The option to send notifications via email " + - "('radar.notification.email.enabled') will not work for this subject. Consider to provide a valid email " + - "address for subject", userDto.subjectId + "('radar.notification.email.enabled') will not work for this subject. Consider to provide a valid email " + + "address for subject", + userDto.subjectId, ) } @@ -233,14 +238,15 @@ class UserService( val user: User? = userRepository.findBySubjectIdAndProjectId( userDto.subjectId, - project.id) + project.id, + ) checkInvalidDetails( { user == null }, { "The user with specified subject ID ${userDto.subjectId} does not exist in project ID " "${userDto.projectId} Please use CreateUser endpoint to create the user." - } + }, ) user!!.apply { @@ -296,13 +302,14 @@ class UserService( val user = userRepository.findBySubjectIdAndProjectId( subjectId, - project.id) + project.id, + ) checkInvalidDetails( { user == null }, { "The user with specified subject ID $subjectId does not exist in project ID $projectId. Please specify a valid user for deleting." - } + }, ) this.userRepository.deleteById(user!!.id!!) @@ -313,4 +320,4 @@ class UserService( private val logger: Logger = LoggerFactory.getLogger(UserService::class.java) } -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/service/fcm/ScheduleNotificationHandler.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/fcm/ScheduleNotificationHandler.kt similarity index 90% rename from src/main/java/org/radarbase/appserver/service/fcm/ScheduleNotificationHandler.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/fcm/ScheduleNotificationHandler.kt index 6735225b7..6d408c539 100644 --- a/src/main/java/org/radarbase/appserver/service/fcm/ScheduleNotificationHandler.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/fcm/ScheduleNotificationHandler.kt @@ -25,6 +25,8 @@ import org.radarbase.appserver.dto.fcm.FcmUserDto interface ScheduleNotificationHandler { fun handleScheduleNotification( - notificationDto: FcmNotificationDto, userDto: FcmUserDto, projectId: String? + notificationDto: FcmNotificationDto, + userDto: FcmUserDto, + projectId: String?, ) } diff --git a/src/main/java/org/radarbase/appserver/service/fcm/SimpleScheduleNotificationHandler.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/fcm/SimpleScheduleNotificationHandler.kt similarity index 94% rename from src/main/java/org/radarbase/appserver/service/fcm/SimpleScheduleNotificationHandler.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/fcm/SimpleScheduleNotificationHandler.kt index 5953f34cd..e6e22dbf1 100644 --- a/src/main/java/org/radarbase/appserver/service/fcm/SimpleScheduleNotificationHandler.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/fcm/SimpleScheduleNotificationHandler.kt @@ -33,10 +33,12 @@ import org.slf4j.LoggerFactory class SimpleScheduleNotificationHandler( private val notificationService: FcmNotificationService, private val projectService: ProjectService, - private val userService: UserService + private val userService: UserService, ) : ScheduleNotificationHandler { override fun handleScheduleNotification( - notificationDto: FcmNotificationDto, userDto: FcmUserDto, projectId: String? + notificationDto: FcmNotificationDto, + userDto: FcmUserDto, + projectId: String?, ) { try { notificationService.addNotification(notificationDto, userDto.subjectId, projectId) diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/notification/NotificationType.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/notification/NotificationType.kt similarity index 98% rename from src/main/java/org/radarbase/appserver/service/questionnaire/notification/NotificationType.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/notification/NotificationType.kt index a78062cae..3f29f093b 100644 --- a/src/main/java/org/radarbase/appserver/service/questionnaire/notification/NotificationType.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/notification/NotificationType.kt @@ -28,5 +28,5 @@ enum class NotificationType { MISSED_SOON, MISSED, TEST, - OTHER -} \ No newline at end of file + OTHER, +} diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/notification/TaskNotificationGeneratorService.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/notification/TaskNotificationGeneratorService.kt similarity index 94% rename from src/main/java/org/radarbase/appserver/service/questionnaire/notification/TaskNotificationGeneratorService.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/notification/TaskNotificationGeneratorService.kt index ebd78eea7..a25bfd178 100644 --- a/src/main/java/org/radarbase/appserver/service/questionnaire/notification/TaskNotificationGeneratorService.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/notification/TaskNotificationGeneratorService.kt @@ -25,8 +25,11 @@ import java.time.Instant class TaskNotificationGeneratorService { fun createNotification( - task: Task, notificationTimestamp: Instant, - title: String?, body: String?, emailEnabled: Boolean + task: Task, + notificationTimestamp: Instant, + title: String?, + body: String?, + emailEnabled: Boolean, ): Notification { return NotificationBuilder().apply { scheduledTime(notificationTimestamp) diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/ClinicalProtocolHandler.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/ClinicalProtocolHandler.kt similarity index 98% rename from src/main/java/org/radarbase/appserver/service/questionnaire/protocol/ClinicalProtocolHandler.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/ClinicalProtocolHandler.kt index 46b391bfd..363e4a300 100644 --- a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/ClinicalProtocolHandler.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/ClinicalProtocolHandler.kt @@ -40,7 +40,7 @@ class ClinicalProtocolHandler : ProtocolHandler { override fun handle( assessmentSchedule: AssessmentSchedule, assessment: Assessment, - user: User + user: User, ): AssessmentSchedule { assessmentSchedule.name = assessment.name return assessmentSchedule diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/CompletedQuestionnaireHandler.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/CompletedQuestionnaireHandler.kt similarity index 96% rename from src/main/java/org/radarbase/appserver/service/questionnaire/protocol/CompletedQuestionnaireHandler.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/CompletedQuestionnaireHandler.kt index c195b4c70..037febc38 100644 --- a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/CompletedQuestionnaireHandler.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/CompletedQuestionnaireHandler.kt @@ -32,13 +32,13 @@ import java.util.TimeZone @OpenClass class CompletedQuestionnaireHandler( private val prevTasks: List, - private val prevTimezone: String + private val prevTimezone: String, ) : ProtocolHandler { override fun handle( assessmentSchedule: AssessmentSchedule, assessment: Assessment, - user: User + user: User, ): AssessmentSchedule { val currentTimezone = checkPresence(user.timezone) { "User's timezone can't be null in completed questionnaire handler" @@ -55,7 +55,7 @@ class CompletedQuestionnaireHandler( currentTasks: List, previousTasks: List, currentTimezone: String, - prevTimezone: String + prevTimezone: String, ): List { currentTasks.parallelStream().forEach { newTask -> val matching = if (currentTimezone != prevTimezone) { @@ -93,7 +93,7 @@ class CompletedQuestionnaireHandler( private fun getPreviousTimezoneEquivalent( taskTimestamp: Timestamp, newTimezone: String, - prevTimezone: String + prevTimezone: String, ): Timestamp { val timezoneDiff = TimeZone.getTimeZone(newTimezone).rawOffset - TimeZone.getTimeZone(prevTimezone).rawOffset return Timestamp(taskTimestamp.time + timezoneDiff) diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/DefaultProtocolGenerator.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/DefaultProtocolGenerator.kt similarity index 97% rename from src/main/java/org/radarbase/appserver/service/questionnaire/protocol/DefaultProtocolGenerator.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/DefaultProtocolGenerator.kt index 0fdbbfb09..2f6fffd26 100644 --- a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/DefaultProtocolGenerator.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/DefaultProtocolGenerator.kt @@ -43,7 +43,7 @@ import java.time.Duration @Service @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON) class DefaultProtocolGenerator( - @field:Transient private val protocolFetcherStrategy: ProtocolFetcherStrategy + @field:Transient private val protocolFetcherStrategy: ProtocolFetcherStrategy, ) : ProtocolGenerator { /** * Caches a mapping of user identifiers to their corresponding protocol configurations. @@ -52,7 +52,7 @@ class DefaultProtocolGenerator( private var cachedProtocolMap: CachedMap = CachedMap( protocolFetcherStrategy::fetchProtocols, CACHE_INVALIDATE_DEFAULT, - CACHE_RETRY_DEFAULT + CACHE_RETRY_DEFAULT, ) /** @@ -62,10 +62,9 @@ class DefaultProtocolGenerator( private var cachedProjectProtocolMap: CachedMap = CachedMap( protocolFetcherStrategy::fetchProtocolsPerProject, CACHE_INVALIDATE_DEFAULT, - CACHE_RETRY_DEFAULT + CACHE_RETRY_DEFAULT, ) - /** * Retrieves all available protocols from the cache or previously fetched data. * If an error occurs during protocol retrieval, cached values are used as a fallback. @@ -97,14 +96,14 @@ class DefaultProtocolGenerator( logger.warn( "Cannot retrieve Protocols for project {} : {}, Using cached values.", projectId, - ex.toString() + ex.toString(), ) return cachedProjectProtocolMap.getCachedMap()[projectId]!! } catch (ex: Exception) { logger.warn( "Exception while fetching protocols for project {}", projectId, - ex + ex, ) throw IOException("Exception while fetching protocols for project $projectId", ex) } @@ -128,7 +127,7 @@ class DefaultProtocolGenerator( logger.warn( "Exception while fetching protocols for subject {}", subjectId, - ex + ex, ) throw IOException("Exception while fetching protocols for subject $subjectId", ex) } @@ -153,7 +152,7 @@ class DefaultProtocolGenerator( logger.warn( "Cannot retrieve Protocols for subject {} : {}, Using cached values.", subjectId, - ex.toString() + ex.toString(), ) return cachedProtocolMap.getCachedMap()[subjectId]!! } catch (_: NoSuchElementException) { @@ -163,7 +162,7 @@ class DefaultProtocolGenerator( logger.warn( "Exception while fetching protocols for subject {}", subjectId, - ex + ex, ) throw IOException("Exception while fetching protocols for subject $subjectId", ex) } @@ -177,6 +176,7 @@ class DefaultProtocolGenerator( * and must be refreshed. */ private val CACHE_INVALIDATE_DEFAULT: Duration = Duration.ofHours(1) + /** * The default duration to retry caching operations. * Used when no specific retry interval is configured. diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/DisabledNotificationHandler.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/DisabledNotificationHandler.kt similarity index 94% rename from src/main/java/org/radarbase/appserver/service/questionnaire/protocol/DisabledNotificationHandler.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/DisabledNotificationHandler.kt index 3b1fdff79..63d950e3b 100644 --- a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/DisabledNotificationHandler.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/DisabledNotificationHandler.kt @@ -18,14 +18,13 @@ package org.radarbase.appserver.service.questionnaire.protocol import org.radarbase.appserver.dto.protocol.Assessment import org.radarbase.appserver.dto.questionnaire.AssessmentSchedule -import org.radarbase.appserver.entity.Notification import org.radarbase.appserver.entity.User class DisabledNotificationHandler : ProtocolHandler { override fun handle( assessmentSchedule: AssessmentSchedule, assessment: Assessment, - user: User + user: User, ): AssessmentSchedule { return assessmentSchedule.also { it.notifications = emptyList() diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/GithubProtocolFetcherStrategy.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/GithubProtocolFetcherStrategy.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/service/questionnaire/protocol/GithubProtocolFetcherStrategy.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/GithubProtocolFetcherStrategy.kt index 18fb2e3a5..347df0a4f 100644 --- a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/GithubProtocolFetcherStrategy.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/GithubProtocolFetcherStrategy.kt @@ -79,7 +79,7 @@ class GithubProtocolFetcherStrategy( @field:Transient private val projectRepository: ProjectRepository, @field:Transient - private val githubService: GithubService + private val githubService: GithubService, ) : ProtocolFetcherStrategy { @Transient @@ -95,7 +95,6 @@ class GithubProtocolFetcherStrategy( private val projectProtocolUriMap = CachedMap(::retrieveProtocolDirectories, Duration.ofHours(3), Duration.ofMinutes(4)) - @PostConstruct fun validateConfiguration() { require(!protocolRepo.isNullOrBlank() && !protocolFileName.isNullOrBlank() && !protocolBranch.isNullOrBlank()) { @@ -103,7 +102,6 @@ class GithubProtocolFetcherStrategy( } } - /** * Fetches protocol configurations for all users stored in the user repository and associates each * user with their respective protocol based on a set of predefined paths. @@ -135,7 +133,7 @@ class GithubProtocolFetcherStrategy( private fun fetchProtocolForSingleUser( user: User, projectId: String, - protocolPaths: Set + protocolPaths: Set, ): ProtocolCacheEntry { val attributes: Map? = user.attributes ?: emptyMap() @@ -250,7 +248,7 @@ class GithubProtocolFetcherStrategy( try { val treeContent = githubService.getGithubContentWithoutCache( - "$GITHUB_API_URI$protocolRepo/branches/$protocolBranch" + "$GITHUB_API_URI$protocolRepo/branches/$protocolBranch", ).run { this@GithubProtocolFetcherStrategy.getArrayNode(this) }.run { diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/ProtocolFetcherStrategy.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/ProtocolFetcherStrategy.kt similarity index 100% rename from src/main/java/org/radarbase/appserver/service/questionnaire/protocol/ProtocolFetcherStrategy.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/ProtocolFetcherStrategy.kt diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/ProtocolGenerator.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/ProtocolGenerator.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/service/questionnaire/protocol/ProtocolGenerator.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/ProtocolGenerator.kt index 8dfb209e1..eab1345cd 100644 --- a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/ProtocolGenerator.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/ProtocolGenerator.kt @@ -54,4 +54,4 @@ interface ProtocolGenerator { * @return The protocol corresponding to the specified subject. */ fun getProtocolForSubject(subjectId: String): Protocol? -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/ProtocolHandler.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/ProtocolHandler.kt similarity index 100% rename from src/main/java/org/radarbase/appserver/service/questionnaire/protocol/ProtocolHandler.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/ProtocolHandler.kt diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/RandomRepeatQuestionnaireHandler.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/RandomRepeatQuestionnaireHandler.kt similarity index 95% rename from src/main/java/org/radarbase/appserver/service/questionnaire/protocol/RandomRepeatQuestionnaireHandler.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/RandomRepeatQuestionnaireHandler.kt index 1d8e97326..3fecd721b 100644 --- a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/RandomRepeatQuestionnaireHandler.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/RandomRepeatQuestionnaireHandler.kt @@ -29,7 +29,7 @@ import org.radarbase.appserver.entity.User import java.time.Instant import java.util.* -class RandomRepeatQuestionnaireHandler: ProtocolHandler { +class RandomRepeatQuestionnaireHandler : ProtocolHandler { private val defaultTaskCompletionWindow = 86_400_000L private val timeCalculatorService = TimeCalculatorService() @@ -38,9 +38,8 @@ class RandomRepeatQuestionnaireHandler: ProtocolHandler { override fun handle( assessmentSchedule: AssessmentSchedule, assessment: Assessment, - user: User + user: User, ): AssessmentSchedule { - val referenceTimestamps = assessmentSchedule.referenceTimestamps ?: return assessmentSchedule.apply { tasks = emptyList() } @@ -48,7 +47,7 @@ class RandomRepeatQuestionnaireHandler: ProtocolHandler { val tasks = generateTasks( assessment, referenceTimestamps, - user + user, ) assessmentSchedule.tasks = tasks return assessmentSchedule @@ -57,7 +56,7 @@ class RandomRepeatQuestionnaireHandler: ProtocolHandler { private fun generateTasks( assessment: Assessment, referenceTimestamps: List, - user: User + user: User, ): List { val timezone = TimeZone.getTimeZone(user.timezone) val repeatQuestionnaire = assessment.protocol?.repeatQuestionnaire @@ -81,7 +80,7 @@ class RandomRepeatQuestionnaireHandler: ProtocolHandler { private fun getRandomAmountInRange(range: Array): Int { val (lowerLimit, upperLimit) = range - return (lowerLimit .. upperLimit).random() + return (lowerLimit..upperLimit).random() } private fun calculateCompletionWindow(completionWindow: TimePeriod?): Long { diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/SimpleNotificationHandler.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/SimpleNotificationHandler.kt similarity index 92% rename from src/main/java/org/radarbase/appserver/service/questionnaire/protocol/SimpleNotificationHandler.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/SimpleNotificationHandler.kt index 74df98266..fc8a1005c 100644 --- a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/SimpleNotificationHandler.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/SimpleNotificationHandler.kt @@ -33,7 +33,7 @@ class SimpleNotificationHandler : ProtocolHandler { override fun handle( assessmentSchedule: AssessmentSchedule, assessment: Assessment, - user: User + user: User, ): AssessmentSchedule { val notificationProtocol = assessment.protocol?.notification val estimatedCompletionTime = assessment.estimatedCompletionTime @@ -48,13 +48,13 @@ class SimpleNotificationHandler : ProtocolHandler { val title = this.taskNotificationGeneratorService.getTitleText( user.language, notificationProtocol.title, - NotificationType.NOW + NotificationType.NOW, ) val body = this.taskNotificationGeneratorService.getBodyText( user.language, notificationProtocol.body, NotificationType.NOW, - estimatedCompletionTime + estimatedCompletionTime, ) generateNotifications(assessmentScheduleTasks, user, title, body, notificationProtocol.email.enabled).also { notifications -> @@ -65,8 +65,11 @@ class SimpleNotificationHandler : ProtocolHandler { } fun generateNotifications( - tasks: List, user: User, - title: String, body: String, emailEnabled: Boolean + tasks: List, + user: User, + title: String, + body: String, + emailEnabled: Boolean, ): List { return tasks.parallelStream() .map { task: Task -> @@ -76,7 +79,7 @@ class SimpleNotificationHandler : ProtocolHandler { taskTimeStamp.toInstant(), title, body, - emailEnabled + emailEnabled, ).apply { this.user = user } @@ -88,8 +91,8 @@ class SimpleNotificationHandler : ProtocolHandler { scheduledTime != null && Instant.now().isBefore( scheduledTime - .plus(ttlSeconds.toLong(), ChronoUnit.SECONDS) + .plus(ttlSeconds.toLong(), ChronoUnit.SECONDS), ) }.collect(Collectors.toList()) } -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/SimpleProtocolHandler.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/SimpleProtocolHandler.kt similarity index 84% rename from src/main/java/org/radarbase/appserver/service/questionnaire/protocol/SimpleProtocolHandler.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/SimpleProtocolHandler.kt index b577c15a8..fb02f28dc 100644 --- a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/SimpleProtocolHandler.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/SimpleProtocolHandler.kt @@ -23,7 +23,7 @@ package org.radarbase.appserver.service.questionnaire.protocol import org.radarbase.appserver.dto.protocol.Assessment import org.radarbase.appserver.dto.protocol.ReferenceTimestamp -import org.radarbase.appserver.dto.protocol.ReferenceTimestampType.* +import org.radarbase.appserver.dto.protocol.ReferenceTimestampType import org.radarbase.appserver.dto.questionnaire.AssessmentSchedule import org.radarbase.appserver.entity.User import java.time.Instant @@ -52,9 +52,10 @@ class SimpleProtocolHandler : ProtocolHandler { * is not provided. */ override fun handle( - assessmentSchedule: AssessmentSchedule, assessment: Assessment, user: User + assessmentSchedule: AssessmentSchedule, + assessment: Assessment, + user: User, ): AssessmentSchedule { - val referenceTimestamp: ReferenceTimestamp? = assessment.protocol?.referenceTimestamp val timezone = TimeZone.getTimeZone(user.timezone) val timezoneId = timezone.toZoneId() @@ -66,15 +67,15 @@ class SimpleProtocolHandler : ProtocolHandler { "Reference timestamp format is null when handling SimpleProtocolHandler." } when (timestampFormat) { - DATE -> LocalDate.parse(timestamp).atStartOfDay(timezoneId).toInstant() + ReferenceTimestampType.DATE -> LocalDate.parse(timestamp).atStartOfDay(timezoneId).toInstant() - DATETIME -> LocalDateTime.parse(timestamp).atZone(timezoneId).toInstant() + ReferenceTimestampType.DATETIME -> LocalDateTime.parse(timestamp).atZone(timezoneId).toInstant() - DATETIMEUTC -> Instant.parse(timestamp) + ReferenceTimestampType.DATETIMEUTC -> Instant.parse(timestamp) - NOW -> Instant.now() + ReferenceTimestampType.NOW -> Instant.now() - TODAY -> timeCalculatorService.setDateTimeToMidnight(Instant.now(), timezone) + ReferenceTimestampType.TODAY -> timeCalculatorService.setDateTimeToMidnight(Instant.now(), timezone) } } else { val userEnrolmentDate = user.enrolmentDate diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/SimpleReminderHandler.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/SimpleReminderHandler.kt similarity index 77% rename from src/main/java/org/radarbase/appserver/service/questionnaire/protocol/SimpleReminderHandler.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/SimpleReminderHandler.kt index 49ccd6795..688805812 100644 --- a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/SimpleReminderHandler.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/SimpleReminderHandler.kt @@ -23,7 +23,7 @@ class SimpleReminderHandler : ProtocolHandler { override fun handle( assessmentSchedule: AssessmentSchedule, assessment: Assessment, - user: User + user: User, ): AssessmentSchedule { val timezone = TimeZone.getTimeZone(user.timezone) val protocol = assessment.protocol?.notification @@ -42,7 +42,7 @@ class SimpleReminderHandler : ProtocolHandler { user.language, protocol.body, NotificationType.REMINDER, - estimatedCompletionTime + estimatedCompletionTime, ) val notifications = generateReminders( @@ -52,39 +52,50 @@ class SimpleReminderHandler : ProtocolHandler { user, title, body, - protocol.email.enabled + protocol.email.enabled, ) assessmentSchedule.reminders = notifications return assessmentSchedule } fun generateReminders( - tasks: List, assessment: Assessment, timezone: TimeZone, - user: User, title: String, body: String, emailEnabled: Boolean + tasks: List, + assessment: Assessment, + timezone: TimeZone, + user: User, + title: String, + body: String, + emailEnabled: Boolean, ): List { return tasks.parallelStream() .flatMap { task: Task -> - val reminders = assessment.protocol?.reminders ?: return@flatMap null + val reminders = assessment.protocol?.reminders ?: return@flatMap null val repeatReminders = reminders.repeat ?: return@flatMap null val offset = TimePeriod(reminders.unit, reminders.amount) - (1 .. repeatReminders).map { repeat : Int -> + (1..repeatReminders).map { repeat: Int -> offset.amount = reminders.amount!! * repeat val timestamp = timeCalculatorService.advanceRepeat(task.timestamp!!.toInstant(), offset, timezone) taskNotificationGeneratorService.createNotification( - task, timestamp, title, body, emailEnabled + task, + timestamp, + title, + body, + emailEnabled, ).also { it.user = user } }.stream() } .filter { notification -> - (Instant.now().isBefore( - notification.scheduledTime!! - .plus(notification.ttlSeconds.toLong(), ChronoUnit.SECONDS) - )) + ( + Instant.now().isBefore( + notification.scheduledTime!! + .plus(notification.ttlSeconds.toLong(), ChronoUnit.SECONDS), + ) + ) }.collect(Collectors.toList()) } } diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/SimpleRepeatProtocolHandler.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/SimpleRepeatProtocolHandler.kt similarity index 90% rename from src/main/java/org/radarbase/appserver/service/questionnaire/protocol/SimpleRepeatProtocolHandler.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/SimpleRepeatProtocolHandler.kt index 17c1ed494..7a1316f74 100644 --- a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/SimpleRepeatProtocolHandler.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/SimpleRepeatProtocolHandler.kt @@ -35,7 +35,7 @@ class SimpleRepeatProtocolHandler : ProtocolHandler { override fun handle( assessmentSchedule: AssessmentSchedule, assessment: Assessment, - user: User + user: User, ): AssessmentSchedule { val timezone = user.timezone requireNotNull(timezone) { @@ -46,15 +46,17 @@ class SimpleRepeatProtocolHandler : ProtocolHandler { "Reference timestamp is null when handling SimpleRepeatProtocolHandler." } - val referenceTimestamps = generateReferenceTimestamps(assessment, referenceTimestamp, timezone) - assessmentSchedule.referenceTimestamps = referenceTimestamps - return assessmentSchedule + return generateReferenceTimestamps(assessment, referenceTimestamp, timezone).let { refTimeStamps -> + assessmentSchedule.apply { + referenceTimestamps = refTimeStamps + } + } } private fun generateReferenceTimestamps( assessment: Assessment, startTime: Instant, - timezoneId: String + timezoneId: String, ): List { val timezone = TimeZone.getTimeZone(timezoneId) val repeatProtocol: RepeatProtocol? = assessment.protocol?.repeatProtocol @@ -62,7 +64,7 @@ class SimpleRepeatProtocolHandler : ProtocolHandler { val repeatProtocolUnit: String? = repeatProtocol?.unit val repeatProtocolAmount: Int? = repeatProtocol?.amount - if (repeatProtocol == null || repeatProtocolUnit == null || repeatProtocolAmount == null) { + if (repeatProtocol == null || repeatProtocolUnit == null || repeatProtocolAmount == null) { logger.warn("Repeat protocol is null for assessment in SimpleRepeatProtocolHandler") return emptyList() } @@ -80,13 +82,13 @@ class SimpleRepeatProtocolHandler : ProtocolHandler { private fun isValidReferenceTimestamp(referenceTime: Instant, timezone: TimeZone): Boolean { val defaultEndTime = timeCalculatorService.advanceRepeat(Instant.now(), PLUS_ONE_WEEK, timezone) return referenceTime.isBefore(defaultEndTime) && - referenceTime.atZone(timezone.toZoneId()).year < MAX_YEAR + referenceTime.atZone(timezone.toZoneId()).year < MAX_YEAR } private fun calculateValidStartTime( startTime: Instant, timezone: TimeZone, - simpleRepeatProtocol: TimePeriod + simpleRepeatProtocol: TimePeriod, ): Instant { var referenceTime = startTime val defaultStartTime = timeCalculatorService.advanceRepeat(Instant.now(), MINUS_ONE_WEEK, timezone) diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/SimpleRepeatQuestionnaireHandler.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/SimpleRepeatQuestionnaireHandler.kt similarity index 94% rename from src/main/java/org/radarbase/appserver/service/questionnaire/protocol/SimpleRepeatQuestionnaireHandler.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/SimpleRepeatQuestionnaireHandler.kt index 3f05d7665..78d545d47 100644 --- a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/SimpleRepeatQuestionnaireHandler.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/SimpleRepeatQuestionnaireHandler.kt @@ -36,7 +36,9 @@ class SimpleRepeatQuestionnaireHandler : ProtocolHandler { private val taskGeneratorService = TaskGeneratorService() override fun handle( - assessmentSchedule: AssessmentSchedule, assessment: Assessment, user: User + assessmentSchedule: AssessmentSchedule, + assessment: Assessment, + user: User, ): AssessmentSchedule { val referenceTimestamp = assessmentSchedule.referenceTimestamps ?: return assessmentSchedule.also { it.tasks = emptyList() @@ -47,7 +49,9 @@ class SimpleRepeatQuestionnaireHandler : ProtocolHandler { } private fun generateTasks( - assessment: Assessment, referenceTimestamps: List, user: User + assessment: Assessment, + referenceTimestamps: List, + user: User, ): List { val timezone = TimeZone.getTimeZone(user.timezone) val repeatQuestionnaire: RepeatQuestionnaire? = assessment.protocol?.repeatQuestionnaire diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/TaskGeneratorService.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/TaskGeneratorService.kt similarity index 97% rename from src/main/java/org/radarbase/appserver/service/questionnaire/protocol/TaskGeneratorService.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/TaskGeneratorService.kt index c5105bf2e..47ae37ab9 100644 --- a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/TaskGeneratorService.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/TaskGeneratorService.kt @@ -41,7 +41,7 @@ class TaskGeneratorService { showInCalendar(assessment.showInCalendar) isDemo(assessment.isDemo) nQuestions(assessment.nQuestions) - .isClinical(isClinical) + .isClinical(isClinical) }.build() } } diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/TimeCalculatorService.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/TimeCalculatorService.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/service/questionnaire/protocol/TimeCalculatorService.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/TimeCalculatorService.kt index dd963e4aa..b7b217e11 100644 --- a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/TimeCalculatorService.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/TimeCalculatorService.kt @@ -100,11 +100,13 @@ class TimeCalculatorService { * and days, where a week is assumed to always contain 7 days. */ private const val WEEK_TO_DAYS = 7 + /** * Constant representing the number of days in a month. It is used as an approximate value * where 31 days is considered the standard number of days in a month for calculations. */ private const val MONTH_TO_DAYS = 31 + /** * Represents the number of days in a standard non-leap year. * diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/factory/CompletedQuestionnaireHandlerFactory.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/factory/CompletedQuestionnaireHandlerFactory.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/service/questionnaire/protocol/factory/CompletedQuestionnaireHandlerFactory.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/factory/CompletedQuestionnaireHandlerFactory.kt index 72b195a60..946991c19 100644 --- a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/factory/CompletedQuestionnaireHandlerFactory.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/factory/CompletedQuestionnaireHandlerFactory.kt @@ -24,7 +24,6 @@ import org.radarbase.appserver.entity.Task import org.radarbase.appserver.service.questionnaire.protocol.CompletedQuestionnaireHandler import org.radarbase.appserver.service.questionnaire.protocol.ProtocolHandler - object CompletedQuestionnaireHandlerFactory { fun getCompletedQuestionnaireHandler(prevTasks: List, prevTimezone: String): ProtocolHandler { return CompletedQuestionnaireHandler(prevTasks, prevTimezone) diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/factory/NotificationHandlerFactory.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/factory/NotificationHandlerFactory.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/service/questionnaire/protocol/factory/NotificationHandlerFactory.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/factory/NotificationHandlerFactory.kt index d893a5931..906ad45ec 100644 --- a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/factory/NotificationHandlerFactory.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/factory/NotificationHandlerFactory.kt @@ -27,7 +27,6 @@ import org.radarbase.appserver.service.questionnaire.protocol.ProtocolHandler import org.radarbase.appserver.service.questionnaire.protocol.SimpleNotificationHandler import java.io.IOException - object NotificationHandlerFactory { @Throws(IOException::class) fun getNotificationHandler(protocol: NotificationProtocol): ProtocolHandler { diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/factory/ProtocolHandlerFactory.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/factory/ProtocolHandlerFactory.kt similarity index 100% rename from src/main/java/org/radarbase/appserver/service/questionnaire/protocol/factory/ProtocolHandlerFactory.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/factory/ProtocolHandlerFactory.kt diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/factory/ProtocolHandlerType.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/factory/ProtocolHandlerType.kt similarity index 98% rename from src/main/java/org/radarbase/appserver/service/questionnaire/protocol/factory/ProtocolHandlerType.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/factory/ProtocolHandlerType.kt index 21ae7a655..575604cd2 100644 --- a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/factory/ProtocolHandlerType.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/factory/ProtocolHandlerType.kt @@ -24,5 +24,5 @@ package org.radarbase.appserver.service.questionnaire.protocol.factory enum class ProtocolHandlerType { SIMPLE, CLINICAL, - OTHER + OTHER, } diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/factory/ReminderHandlerFactory.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/factory/ReminderHandlerFactory.kt similarity index 100% rename from src/main/java/org/radarbase/appserver/service/questionnaire/protocol/factory/ReminderHandlerFactory.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/factory/ReminderHandlerFactory.kt diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/factory/RepeatProtocolHandlerFactory.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/factory/RepeatProtocolHandlerFactory.kt similarity index 100% rename from src/main/java/org/radarbase/appserver/service/questionnaire/protocol/factory/RepeatProtocolHandlerFactory.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/factory/RepeatProtocolHandlerFactory.kt diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/factory/RepeatProtocolHandlerType.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/factory/RepeatProtocolHandlerType.kt similarity index 98% rename from src/main/java/org/radarbase/appserver/service/questionnaire/protocol/factory/RepeatProtocolHandlerType.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/factory/RepeatProtocolHandlerType.kt index 912587390..d2d125822 100644 --- a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/factory/RepeatProtocolHandlerType.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/factory/RepeatProtocolHandlerType.kt @@ -24,5 +24,5 @@ package org.radarbase.appserver.service.questionnaire.protocol.factory enum class RepeatProtocolHandlerType { SIMPLE, DAYOFWEEK, - OTHER + OTHER, } diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/factory/RepeatQuestionnaireHandlerFactory.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/factory/RepeatQuestionnaireHandlerFactory.kt similarity index 100% rename from src/main/java/org/radarbase/appserver/service/questionnaire/protocol/factory/RepeatQuestionnaireHandlerFactory.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/factory/RepeatQuestionnaireHandlerFactory.kt diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/factory/RepeatQuestionnaireHandlerType.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/factory/RepeatQuestionnaireHandlerType.kt similarity index 98% rename from src/main/java/org/radarbase/appserver/service/questionnaire/protocol/factory/RepeatQuestionnaireHandlerType.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/factory/RepeatQuestionnaireHandlerType.kt index b7f16a2f6..f10010490 100644 --- a/src/main/java/org/radarbase/appserver/service/questionnaire/protocol/factory/RepeatQuestionnaireHandlerType.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/protocol/factory/RepeatQuestionnaireHandlerType.kt @@ -25,5 +25,5 @@ enum class RepeatQuestionnaireHandlerType { SIMPLE, DAYOFWEEKMAP, RANDOM, - OTHER + OTHER, } diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/schedule/ProtocolHandlerRunner.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/schedule/ProtocolHandlerRunner.kt similarity index 100% rename from src/main/java/org/radarbase/appserver/service/questionnaire/schedule/ProtocolHandlerRunner.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/schedule/ProtocolHandlerRunner.kt diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/schedule/QuestionnaireScheduleGeneratorService.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/schedule/QuestionnaireScheduleGeneratorService.kt similarity index 77% rename from src/main/java/org/radarbase/appserver/service/questionnaire/schedule/QuestionnaireScheduleGeneratorService.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/schedule/QuestionnaireScheduleGeneratorService.kt index eb62599d8..d9d06888f 100644 --- a/src/main/java/org/radarbase/appserver/service/questionnaire/schedule/QuestionnaireScheduleGeneratorService.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/schedule/QuestionnaireScheduleGeneratorService.kt @@ -27,7 +27,15 @@ import org.radarbase.appserver.dto.protocol.RepeatProtocol import org.radarbase.appserver.dto.protocol.RepeatQuestionnaire import org.radarbase.appserver.entity.Task import org.radarbase.appserver.service.questionnaire.protocol.ProtocolHandler -import org.radarbase.appserver.service.questionnaire.protocol.factory.* +import org.radarbase.appserver.service.questionnaire.protocol.factory.CompletedQuestionnaireHandlerFactory +import org.radarbase.appserver.service.questionnaire.protocol.factory.NotificationHandlerFactory +import org.radarbase.appserver.service.questionnaire.protocol.factory.ProtocolHandlerFactory +import org.radarbase.appserver.service.questionnaire.protocol.factory.ProtocolHandlerType +import org.radarbase.appserver.service.questionnaire.protocol.factory.ReminderHandlerFactory +import org.radarbase.appserver.service.questionnaire.protocol.factory.RepeatProtocolHandlerFactory +import org.radarbase.appserver.service.questionnaire.protocol.factory.RepeatProtocolHandlerType +import org.radarbase.appserver.service.questionnaire.protocol.factory.RepeatQuestionnaireHandlerFactory +import org.radarbase.appserver.service.questionnaire.protocol.factory.RepeatQuestionnaireHandlerType import org.slf4j.LoggerFactory import org.springframework.stereotype.Service import java.io.IOException @@ -82,15 +90,21 @@ class QuestionnaireScheduleGeneratorService : ScheduleGeneratorService { override fun getReminderHandler(assessment: Assessment): ProtocolHandler? { return if (assessment.type == CLINICAL) { null - } else ReminderHandlerFactory.reminderHandler + } else { + ReminderHandlerFactory.reminderHandler + } } override fun getCompletedQuestionnaireHandler( - assessment: Assessment, prevTasks: List, prevTimezone: String + assessment: Assessment, + prevTasks: List, + prevTimezone: String, ): ProtocolHandler? { return if (assessment.type == CLINICAL) { null - } else CompletedQuestionnaireHandlerFactory.getCompletedQuestionnaireHandler(prevTasks, prevTimezone) + } else { + CompletedQuestionnaireHandlerFactory.getCompletedQuestionnaireHandler(prevTasks, prevTimezone) + } } companion object { diff --git a/src/main/java/org/radarbase/appserver/service/questionnaire/schedule/ScheduleGeneratorService.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/schedule/ScheduleGeneratorService.kt similarity index 97% rename from src/main/java/org/radarbase/appserver/service/questionnaire/schedule/ScheduleGeneratorService.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/schedule/ScheduleGeneratorService.kt index 59cf0f8ab..a281df716 100644 --- a/src/main/java/org/radarbase/appserver/service/questionnaire/schedule/ScheduleGeneratorService.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/questionnaire/schedule/ScheduleGeneratorService.kt @@ -44,7 +44,7 @@ interface ScheduleGeneratorService { fun getCompletedQuestionnaireHandler( assessment: Assessment, prevTasks: List, - prevTimezone: String + prevTimezone: String, ): ProtocolHandler? fun generateScheduleForUser(user: User, protocol: Protocol, prevSchedule: Schedule): Schedule { @@ -65,7 +65,7 @@ interface ScheduleGeneratorService { assessment: Assessment, user: User, previousTasks: List, - prevTimezone: String + prevTimezone: String, ): AssessmentSchedule { val protocolHandlerRunner = ProtocolHandlerRunner().apply { addProtocolHandler(getProtocolHandler(assessment)) @@ -74,7 +74,7 @@ interface ScheduleGeneratorService { addProtocolHandler(getNotificationHandler(assessment)) addProtocolHandler(getReminderHandler(assessment)) addProtocolHandler( - getCompletedQuestionnaireHandler(assessment, previousTasks, prevTimezone) + getCompletedQuestionnaireHandler(assessment, previousTasks, prevTimezone), ) } return protocolHandlerRunner.runProtocolHandlers(assessment, user) diff --git a/src/main/java/org/radarbase/appserver/service/scheduler/AdminEmailNotifierScheduler.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/scheduler/AdminEmailNotifierScheduler.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/service/scheduler/AdminEmailNotifierScheduler.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/scheduler/AdminEmailNotifierScheduler.kt index f2ceed698..bab8d4ec6 100644 --- a/src/main/java/org/radarbase/appserver/service/scheduler/AdminEmailNotifierScheduler.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/scheduler/AdminEmailNotifierScheduler.kt @@ -26,5 +26,3 @@ package org.radarbase.appserver.service.scheduler * @author yatharthranjan */ class AdminEmailNotifierScheduler - - diff --git a/src/main/java/org/radarbase/appserver/service/scheduler/MessageSchedulerService.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/scheduler/MessageSchedulerService.kt similarity index 89% rename from src/main/java/org/radarbase/appserver/service/scheduler/MessageSchedulerService.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/scheduler/MessageSchedulerService.kt index d889b1c89..87d513e14 100644 --- a/src/main/java/org/radarbase/appserver/service/scheduler/MessageSchedulerService.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/scheduler/MessageSchedulerService.kt @@ -20,7 +20,12 @@ */ package org.radarbase.appserver.service.scheduler -import org.quartz.* +import org.quartz.JobDataMap +import org.quartz.JobDetail +import org.quartz.JobKey +import org.quartz.SimpleTrigger +import org.quartz.Trigger +import org.quartz.TriggerKey import org.radarbase.appserver.entity.DataMessage import org.radarbase.appserver.entity.Message import org.radarbase.appserver.entity.Notification @@ -42,7 +47,7 @@ import java.util.Date @Suppress("unused") class MessageSchedulerService( @param:Qualifier("fcmSenderProps") val fcmSender: FcmSender?, - val schedulerService: SchedulerService + val schedulerService: SchedulerService, ) { fun schedule(message: T) { val jobDetail = getJobDetailForMessage(message, getMessageType(message)).`object` @@ -75,12 +80,14 @@ class MessageSchedulerService( fun updateScheduled(message: T) { val jobKeyString: String = NAMING_STRATEGY.getJobKeyName( - message.user!!.subjectId!!, message.id.toString() + message.user!!.subjectId!!, + message.id.toString(), ) val jobKey = JobKey(jobKeyString) val triggerKeyString: String = NAMING_STRATEGY.getTriggerName( - message.user!!.subjectId!!, message.id.toString() + message.user!!.subjectId!!, + message.id.toString(), ) val triggerKey = TriggerKey(triggerKeyString) val jobDataMap = JobDataMap() @@ -95,7 +102,6 @@ class MessageSchedulerService( schedulerService.deleteScheduledJobs(keys) } - fun deleteScheduled(message: T) { val key = JobKey(NAMING_STRATEGY.getJobKeyName(message.user!!.subjectId!!, message.id.toString())) schedulerService.deleteScheduledJob(key) @@ -119,8 +125,9 @@ class MessageSchedulerService( this.setJobDetail(jobDetail) this.setName( NAMING_STRATEGY.getTriggerName( - message.user!!.subjectId!!, message.id.toString() - ) + message.user!!.subjectId!!, + message.id.toString(), + ), ) this.setRepeatCount(0) this.setRepeatInterval(0L) @@ -137,20 +144,19 @@ class MessageSchedulerService( setDurability(true) this.setName( NAMING_STRATEGY.getJobKeyName( - message.user!!.subjectId!!, message.id.toString() - ) + message.user!!.subjectId!!, + message.id.toString(), + ), ) val map = hashMapOf( "subjectId" to message.user!!.subjectId, "projectId" to message.user!!.project!!.projectId, "messageId" to message.id, - "messageType" to messageType.toString() + "messageType" to messageType.toString(), ) setJobDataAsMap(map) afterPropertiesSet() } } } - } - diff --git a/src/main/java/org/radarbase/appserver/service/scheduler/quartz/MessageJob.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/scheduler/quartz/MessageJob.kt similarity index 94% rename from src/main/java/org/radarbase/appserver/service/scheduler/quartz/MessageJob.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/scheduler/quartz/MessageJob.kt index 80c458619..bcc351a07 100644 --- a/src/main/java/org/radarbase/appserver/service/scheduler/quartz/MessageJob.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/scheduler/quartz/MessageJob.kt @@ -44,11 +44,11 @@ class MessageJob( @field:Transient private val notificationTransmitters: List, @field:Transient private val dataMessageTransmitters: List, @field:Transient private val notificationService: FcmNotificationService, - @field:Transient private val dataMessageService: FcmDataMessageService + @field:Transient private val dataMessageService: FcmDataMessageService, ) : Job { /** * Called by the `[org.quartz.Scheduler]` when a `[org.quartz.Trigger] - `* fires that is associated with the `Job`. + `* fires that is associated with the `Job`. * * * The implementation may wish to set a [result][JobExecutionContext.setResult] @@ -71,7 +71,9 @@ class MessageJob( when (type) { MessageType.NOTIFICATION -> { val notification = notificationService.getNotificationByProjectIdAndSubjectIdAndNotificationId( - projectId, subjectId, messageId + projectId, + subjectId, + messageId, ) notificationTransmitters.forEach { transmitter -> try { @@ -84,7 +86,9 @@ class MessageJob( MessageType.DATA -> { val dataMessage = dataMessageService.getDataMessageByProjectIdAndSubjectIdAndDataMessageId( - projectId, subjectId, messageId + projectId, + subjectId, + messageId, ) dataMessageTransmitters.forEach { transmitter -> try { diff --git a/src/main/java/org/radarbase/appserver/service/scheduler/quartz/QuartzNamingStrategy.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/scheduler/quartz/QuartzNamingStrategy.kt similarity index 100% rename from src/main/java/org/radarbase/appserver/service/scheduler/quartz/QuartzNamingStrategy.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/scheduler/quartz/QuartzNamingStrategy.kt diff --git a/src/main/java/org/radarbase/appserver/service/scheduler/quartz/SchedulerService.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/scheduler/quartz/SchedulerService.kt similarity index 82% rename from src/main/java/org/radarbase/appserver/service/scheduler/quartz/SchedulerService.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/scheduler/quartz/SchedulerService.kt index ad484b4a4..d33d6c2e1 100644 --- a/src/main/java/org/radarbase/appserver/service/scheduler/quartz/SchedulerService.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/scheduler/quartz/SchedulerService.kt @@ -20,7 +20,11 @@ */ package org.radarbase.appserver.service.scheduler.quartz -import org.quartz.* +import org.quartz.JobDataMap +import org.quartz.JobDetail +import org.quartz.JobKey +import org.quartz.Trigger +import org.quartz.TriggerKey /** * Generic Service for implementing an interface to the [Scheduler]. @@ -37,7 +41,10 @@ interface SchedulerService { fun checkJobExists(jobKey: JobKey): Boolean fun updateScheduledJob( - jobKey: JobKey, triggerKey: TriggerKey, jobDataMap: JobDataMap, associatedObject: Any? + jobKey: JobKey, + triggerKey: TriggerKey, + jobDataMap: JobDataMap, + associatedObject: Any?, ) fun deleteScheduledJobs(jobKeys: List) diff --git a/src/main/java/org/radarbase/appserver/service/scheduler/quartz/SchedulerServiceImpl.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/scheduler/quartz/SchedulerServiceImpl.kt similarity index 90% rename from src/main/java/org/radarbase/appserver/service/scheduler/quartz/SchedulerServiceImpl.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/scheduler/quartz/SchedulerServiceImpl.kt index fd8b18e8b..59d32256f 100644 --- a/src/main/java/org/radarbase/appserver/service/scheduler/quartz/SchedulerServiceImpl.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/scheduler/quartz/SchedulerServiceImpl.kt @@ -20,7 +20,15 @@ */ package org.radarbase.appserver.service.scheduler.quartz -import org.quartz.* +import org.quartz.JobDataMap +import org.quartz.JobDetail +import org.quartz.JobKey +import org.quartz.JobListener +import org.quartz.Scheduler +import org.quartz.SchedulerException +import org.quartz.SchedulerListener +import org.quartz.Trigger +import org.quartz.TriggerKey import org.quartz.impl.triggers.SimpleTriggerImpl import org.radarbase.appserver.entity.Scheduled import org.slf4j.LoggerFactory @@ -28,7 +36,6 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.scheduling.annotation.Async import org.springframework.stereotype.Service import java.util.* -import java.util.function.Consumer /** * An implementation of the [SchedulerService] providing async access to schedule, @@ -76,7 +83,10 @@ class SchedulerServiceImpl : SchedulerService { @Async override fun updateScheduledJob( - jobKey: JobKey, triggerKey: TriggerKey, jobDataMap: JobDataMap, associatedObject: Any? + jobKey: JobKey, + triggerKey: TriggerKey, + jobDataMap: JobDataMap, + associatedObject: Any?, ) { require(scheduler.checkExists(jobKey)) { "The Specified Job Key does not exist : $jobKey" } diff --git a/src/main/java/org/radarbase/appserver/service/scheduler/quartz/SimpleQuartzNamingStrategy.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/scheduler/quartz/SimpleQuartzNamingStrategy.kt similarity index 100% rename from src/main/java/org/radarbase/appserver/service/scheduler/quartz/SimpleQuartzNamingStrategy.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/scheduler/quartz/SimpleQuartzNamingStrategy.kt diff --git a/src/main/java/org/radarbase/appserver/service/storage/MinioClientInitializer.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/storage/MinioClientInitializer.kt similarity index 96% rename from src/main/java/org/radarbase/appserver/service/storage/MinioClientInitializer.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/storage/MinioClientInitializer.kt index d78fcd4c9..c376452bc 100644 --- a/src/main/java/org/radarbase/appserver/service/storage/MinioClientInitializer.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/storage/MinioClientInitializer.kt @@ -25,7 +25,7 @@ import org.springframework.stereotype.Component @Component @ConditionalOnExpression("\${radar.file-upload.enabled:false} and 's3' == '\${radar.storage.type:}'") class MinioClientInitializer( - private val s3StorageProperties: S3StorageProperties + private val s3StorageProperties: S3StorageProperties, ) { private var minioClient: MinioClient? = null private var bucketName: String? = null @@ -51,7 +51,7 @@ class MinioClientInitializer( bucketName = s3StorageProperties.bucketName val bucketExists = minioClient!!.bucketExists( - BucketExistsArgs.builder().bucket(bucketName!!).build() + BucketExistsArgs.builder().bucket(bucketName!!).build(), ) if (!bucketExists) { diff --git a/src/main/java/org/radarbase/appserver/service/storage/S3StorageService.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/storage/S3StorageService.kt similarity index 97% rename from src/main/java/org/radarbase/appserver/service/storage/S3StorageService.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/storage/S3StorageService.kt index 53a30a3df..19e5a5a6b 100644 --- a/src/main/java/org/radarbase/appserver/service/storage/S3StorageService.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/storage/S3StorageService.kt @@ -30,7 +30,7 @@ import org.springframework.web.multipart.MultipartFile @ConditionalOnExpression("\${radar.file-upload.enabled:false} && 's3' == '\${radar.storage.type:}'") class S3StorageService @Autowired constructor( private val s3StorageProperties: S3StorageProperties, - private val bucketClient: MinioClientInitializer + private val bucketClient: MinioClientInitializer, ) : StorageService { override fun store(file: MultipartFile?, projectId: String?, subjectId: String?, topicId: String?): String { @@ -55,7 +55,7 @@ class S3StorageService @Autowired constructor( .bucket(bucketClient.bucketNameOrThrow) .`object`(filePath.pathInBucket) .stream(file.inputStream, file.size, -1) - .build() + .build(), ) return filePath.pathInBucket diff --git a/src/main/java/org/radarbase/appserver/service/storage/StoragePath.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/storage/StoragePath.kt similarity index 96% rename from src/main/java/org/radarbase/appserver/service/storage/StoragePath.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/storage/StoragePath.kt index e563933d8..5b0c291ba 100644 --- a/src/main/java/org/radarbase/appserver/service/storage/StoragePath.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/storage/StoragePath.kt @@ -94,10 +94,12 @@ data class StoragePath(val pathInBucket: String, val pathInTopicDir: String) { topic = topicId } - fun dayFolderPattern(pattern: String) = apply { folderPattern = pattern + fun dayFolderPattern(pattern: String) = apply { + folderPattern = pattern } - fun fileTimestampPattern(pattern: String) = apply { filePattern = pattern + fun fileTimestampPattern(pattern: String) = apply { + filePattern = pattern } fun build(): StoragePath { diff --git a/src/main/java/org/radarbase/appserver/service/storage/StorageService.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/storage/StorageService.kt similarity index 100% rename from src/main/java/org/radarbase/appserver/service/storage/StorageService.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/storage/StorageService.kt diff --git a/src/main/java/org/radarbase/appserver/service/transmitter/DataMessageTransmitter.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/transmitter/DataMessageTransmitter.kt similarity index 100% rename from src/main/java/org/radarbase/appserver/service/transmitter/DataMessageTransmitter.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/transmitter/DataMessageTransmitter.kt diff --git a/src/main/java/org/radarbase/appserver/service/transmitter/EmailNotificationTransmitter.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/transmitter/EmailNotificationTransmitter.kt similarity index 96% rename from src/main/java/org/radarbase/appserver/service/transmitter/EmailNotificationTransmitter.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/transmitter/EmailNotificationTransmitter.kt index e605b68cd..66261a369 100644 --- a/src/main/java/org/radarbase/appserver/service/transmitter/EmailNotificationTransmitter.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/transmitter/EmailNotificationTransmitter.kt @@ -33,7 +33,7 @@ import org.springframework.stereotype.Component @Component @ConditionalOnProperty(value = ["radar.notification.email.enabled"], havingValue = "true") class EmailNotificationTransmitter( - private val emailSender: JavaMailSender + private val emailSender: JavaMailSender, ) : NotificationTransmitter { @Value("\${radar.notification.email.from}") @Transient @@ -50,7 +50,7 @@ class EmailNotificationTransmitter( if (notification.user!!.emailAddress.isNullOrBlank()) { logger.warn( "Could not transmit a notification via email because subject {} has no email address.", - notification.user!!.subjectId + notification.user!!.subjectId, ) return } diff --git a/src/main/java/org/radarbase/appserver/service/transmitter/FcmTransmitter.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/transmitter/FcmTransmitter.kt similarity index 93% rename from src/main/java/org/radarbase/appserver/service/transmitter/FcmTransmitter.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/transmitter/FcmTransmitter.kt index 2e8f8abb2..61f6bea6e 100644 --- a/src/main/java/org/radarbase/appserver/service/transmitter/FcmTransmitter.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/transmitter/FcmTransmitter.kt @@ -44,7 +44,7 @@ class FcmTransmitter( @param:Qualifier("fcmSenderProps") val fcmSender: FcmSender, private val notificationService: FcmNotificationService, private val dataMessageService: FcmDataMessageService, - private val userService: UserService + private val userService: UserService, ) : NotificationTransmitter, DataMessageTransmitter { @Throws(FcmMessageTransmitException::class) @@ -88,7 +88,7 @@ class FcmTransmitter( // More info on ErrorCode: https://firebase.google.com/docs/reference/fcm/rest/v1/ErrorCode when (errorCode) { ErrorCode.INVALID_ARGUMENT, ErrorCode.INTERNAL, ErrorCode.ABORTED, ErrorCode.CONFLICT, ErrorCode.CANCELLED, ErrorCode.DATA_LOSS, ErrorCode.NOT_FOUND, ErrorCode.OUT_OF_RANGE, ErrorCode.ALREADY_EXISTS, ErrorCode.DEADLINE_EXCEEDED, ErrorCode.PERMISSION_DENIED, ErrorCode.RESOURCE_EXHAUSTED, ErrorCode.FAILED_PRECONDITION, ErrorCode.UNAUTHENTICATED, ErrorCode.UNKNOWN -> {} - ErrorCode.UNAVAILABLE -> // TODO: Could schedule for retry. + ErrorCode.UNAVAILABLE -> // TODO: Could schedule for retry. log.warn("The FCM service is unavailable.") } } @@ -96,17 +96,19 @@ class FcmTransmitter( fun handleFCMErrorCode(errorCode: MessagingErrorCode, message: Message) { when (errorCode) { MessagingErrorCode.INTERNAL, MessagingErrorCode.QUOTA_EXCEEDED, MessagingErrorCode.INVALID_ARGUMENT, MessagingErrorCode.SENDER_ID_MISMATCH, MessagingErrorCode.THIRD_PARTY_AUTH_ERROR -> {} - MessagingErrorCode.UNAVAILABLE -> // TODO: Could schedule for retry. + MessagingErrorCode.UNAVAILABLE -> // TODO: Could schedule for retry. log.warn("The FCM service is unavailable.") MessagingErrorCode.UNREGISTERED -> { val userDto = FcmUserDto(message.user!!) log.warn("The Device for user {} was unregistered.", userDto.subjectId) notificationService.removeNotificationsForUser( - userDto.projectId, userDto.subjectId + userDto.projectId, + userDto.subjectId, ) dataMessageService.removeDataMessagesForUser( - userDto.projectId, userDto.subjectId + userDto.projectId, + userDto.subjectId, ) userService.checkFcmTokenExistsAndReplace(userDto) } @@ -123,7 +125,8 @@ class FcmTransmitter( private fun createMessageFromNotification(notification: Notification): FcmNotificationMessage { val to = Objects.requireNonNullElseGet( - notification.fcmTopic, notification.user!!::fcmToken + notification.fcmTopic, + notification.user!!::fcmToken, ) return FcmNotificationMessage.Builder() .to(to) @@ -141,7 +144,8 @@ class FcmTransmitter( private fun createMessageFromDataMessage(dataMessage: DataMessage): FcmDataMessage { val to = Objects.requireNonNullElseGet( - dataMessage.fcmTopic, dataMessage.user!!::fcmToken + dataMessage.fcmTopic, + dataMessage.user!!::fcmToken, ) return FcmDataMessage.Builder() .to(to) diff --git a/src/main/java/org/radarbase/appserver/service/transmitter/NotificationTransmitter.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/transmitter/NotificationTransmitter.kt similarity index 100% rename from src/main/java/org/radarbase/appserver/service/transmitter/NotificationTransmitter.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/service/transmitter/NotificationTransmitter.kt diff --git a/src/main/java/org/radarbase/appserver/util/Base64Deserializer.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/util/Base64Deserializer.kt similarity index 97% rename from src/main/java/org/radarbase/appserver/util/Base64Deserializer.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/util/Base64Deserializer.kt index 3cd9933b8..cc53b8628 100644 --- a/src/main/java/org/radarbase/appserver/util/Base64Deserializer.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/util/Base64Deserializer.kt @@ -27,13 +27,13 @@ import java.util.* class Base64Deserializer : JsonDeserializer(), ContextualDeserializer { override fun createContextual( context: DeserializationContext, - property: BeanProperty + property: BeanProperty, ): JsonDeserializer<*> { if (!String::class.java.isAssignableFrom(property.type.rawClass)) { throw context.invalidTypeIdException( property.type, "String", - "Base64 decoding is only applied to String fields." + "Base64 decoding is only applied to String fields.", ) } return this @@ -54,7 +54,7 @@ class Base64Deserializer : JsonDeserializer(), ContextualDeserializer { parser, "Value for '$fieldName' is not a base64 encoded JSON", value, - wrapperClass + wrapperClass, ) } } diff --git a/src/main/java/org/radarbase/appserver/util/CachedFunction.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/util/CachedFunction.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/util/CachedFunction.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/util/CachedFunction.kt index e521ba1a6..9adbf245a 100644 --- a/src/main/java/org/radarbase/appserver/util/CachedFunction.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/util/CachedFunction.kt @@ -47,7 +47,6 @@ class CachedFunction( val maxEntries: Int = 0, ) : CustomThrowingFunction { - private val cachedMap: MutableMap = LinkedHashMap(16, 0.75f, false) /** @@ -184,6 +183,5 @@ class CachedFunction( companion object { private val logger: Logger = LoggerFactory.getLogger(CachedFunction::class.java) - } -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/util/CachedMap.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/util/CachedMap.kt similarity index 92% rename from src/main/java/org/radarbase/appserver/util/CachedMap.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/util/CachedMap.kt index 020636355..3527cbee5 100644 --- a/src/main/java/org/radarbase/appserver/util/CachedMap.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/util/CachedMap.kt @@ -30,16 +30,16 @@ import java.time.Instant * * This class is thread-safe if the given supplier is thread-safe. */ -class CachedMap ( +class CachedMap ( private val supplier: ThrowingSupplier>, private val invalidateAfter: Duration, - private val retryAfter: Duration + private val retryAfter: Duration, ) { /** * Cache holding the result. Initialized with an empty map and a minimum timestamp. */ - private val cache = NonNullableAtomicReference(Result(emptyMap(), Instant.MIN)) + private val cache = NonNullAtomicReference(Result(emptyMap(), Instant.MIN)) /** * Get the cached map, or retrieve a new one if the current one is old. @@ -98,7 +98,7 @@ class CachedMap ( /** * Supplier that may throw an IOException, Otherwise similar to [java.util.function.Supplier]. */ - fun interface ThrowingSupplier { + fun interface ThrowingSupplier { @Throws(IOException::class) fun get(): T } @@ -108,7 +108,7 @@ class CachedMap ( */ private data class Result( val map: Map, - private val fetchTime: Instant = Instant.now() + private val fetchTime: Instant = Instant.now(), ) { fun isStale(freshDuration: Duration): Boolean = Duration.between(fetchTime, Instant.now()) > freshDuration diff --git a/src/main/java/org/radarbase/appserver/util/CustomThrowingFunction.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/util/CustomThrowingFunction.kt similarity index 99% rename from src/main/java/org/radarbase/appserver/util/CustomThrowingFunction.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/util/CustomThrowingFunction.kt index ec7fd7f42..a2376eccb 100644 --- a/src/main/java/org/radarbase/appserver/util/CustomThrowingFunction.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/util/CustomThrowingFunction.kt @@ -40,4 +40,4 @@ fun interface CustomThrowingFunction { */ @Throws(Exception::class) fun applyWithException(t: T): R -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/util/Extensions.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/util/Extensions.kt similarity index 100% rename from src/main/java/org/radarbase/appserver/util/Extensions.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/util/Extensions.kt diff --git a/src/main/java/org/radarbase/appserver/util/GenerateZeroArgs.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/util/GenerateZeroArgs.kt similarity index 96% rename from src/main/java/org/radarbase/appserver/util/GenerateZeroArgs.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/util/GenerateZeroArgs.kt index 120da069d..86b24ae42 100644 --- a/src/main/java/org/radarbase/appserver/util/GenerateZeroArgs.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/util/GenerateZeroArgs.kt @@ -26,4 +26,4 @@ package org.radarbase.appserver.util */ @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) -annotation class GenerateZeroArgs \ No newline at end of file +annotation class GenerateZeroArgs diff --git a/src/main/java/org/radarbase/appserver/util/NonNullAtomicReference.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/util/NonNullAtomicReference.kt similarity index 97% rename from src/main/java/org/radarbase/appserver/util/NonNullAtomicReference.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/util/NonNullAtomicReference.kt index a9a2d4ee1..04b5586cc 100644 --- a/src/main/java/org/radarbase/appserver/util/NonNullAtomicReference.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/util/NonNullAtomicReference.kt @@ -30,7 +30,7 @@ import java.util.concurrent.atomic.AtomicReference * @param T the type of the value, constrained to be non-nullable. * @param initialValue the initial value to be set in the atomic reference. */ -class NonNullableAtomicReference(initialValue: T) { +class NonNullAtomicReference(initialValue: T) { private val reference = AtomicReference(initialValue) diff --git a/src/main/java/org/radarbase/appserver/util/OpenClass.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/util/OpenClass.kt similarity index 96% rename from src/main/java/org/radarbase/appserver/util/OpenClass.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/util/OpenClass.kt index 346f41c7c..8066a2447 100644 --- a/src/main/java/org/radarbase/appserver/util/OpenClass.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/util/OpenClass.kt @@ -23,4 +23,4 @@ package org.radarbase.appserver.util @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.SOURCE) -annotation class OpenClass \ No newline at end of file +annotation class OpenClass diff --git a/src/main/java/org/radarbase/appserver/util/Utils.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/util/Utils.kt similarity index 94% rename from src/main/java/org/radarbase/appserver/util/Utils.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/util/Utils.kt index 71d12886f..919033df8 100644 --- a/src/main/java/org/radarbase/appserver/util/Utils.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/util/Utils.kt @@ -33,7 +33,6 @@ import kotlin.contracts.contract */ @OptIn(ExperimentalContracts::class) inline fun checkPresence(value: T?, messageProvider: () -> String): T { - contract { returns() implies (value != null) } @@ -57,12 +56,12 @@ inline fun checkPresence(value: T?, messageProvider: () -> String): T inline fun checkInvalidProjectDetails( projectDTO: ProjectDto, invalidation: () -> Boolean, - messageProvider: () -> String + messageProvider: () -> String, ) { if (invalidation()) { throw InvalidProjectDetailsException( projectDTO, - IllegalArgumentException(messageProvider()) + IllegalArgumentException(messageProvider()), ) } } @@ -78,11 +77,11 @@ inline fun checkInvalidProjectDetails( */ inline fun checkInvalidDetails( invalidation: () -> Boolean, - messageProvider: () -> String + messageProvider: () -> String, ) { if (invalidation()) { throw E::class.java.getDeclaredConstructor( - String::class.java + String::class.java, ).newInstance(messageProvider()) } -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/appserver/validation/CheckExactlyOneNotNull.kt b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/validation/CheckExactlyOneNotNull.kt similarity index 98% rename from src/main/java/org/radarbase/appserver/validation/CheckExactlyOneNotNull.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/appserver/validation/CheckExactlyOneNotNull.kt index baf19f7e2..ce3ac4d67 100644 --- a/src/main/java/org/radarbase/appserver/validation/CheckExactlyOneNotNull.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/appserver/validation/CheckExactlyOneNotNull.kt @@ -33,7 +33,7 @@ import kotlin.annotation.AnnotationTarget.CLASS @Constraint(validatedBy = [CheckExactlyOneNotNull.CheckExactlyOneNotNullValidator::class]) @MustBeDocumented annotation class CheckExactlyOneNotNull( - val fieldNames: Array + val fieldNames: Array, ) { class CheckExactlyOneNotNullValidator : ConstraintValidator { diff --git a/src/main/java/org/radarbase/fcm/common/ObjectMapperConfig.kt b/appserver-legacy/src/main/kotlin/org/radarbase/fcm/common/ObjectMapperConfig.kt similarity index 96% rename from src/main/java/org/radarbase/fcm/common/ObjectMapperConfig.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/fcm/common/ObjectMapperConfig.kt index 120f9e6ad..698269cc1 100644 --- a/src/main/java/org/radarbase/fcm/common/ObjectMapperConfig.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/fcm/common/ObjectMapperConfig.kt @@ -20,7 +20,7 @@ class ObjectMapperConfig { registerModule( KotlinModule.Builder() .configure(KotlinFeature.NullIsSameAsDefault, true) - .build() + .build(), ) registerModule(JavaTimeModule()) } diff --git a/src/main/java/org/radarbase/fcm/config/FcmSenderConfig.kt b/appserver-legacy/src/main/kotlin/org/radarbase/fcm/config/FcmSenderConfig.kt similarity index 94% rename from src/main/java/org/radarbase/fcm/config/FcmSenderConfig.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/fcm/config/FcmSenderConfig.kt index 4e065a337..9b194544e 100644 --- a/src/main/java/org/radarbase/fcm/config/FcmSenderConfig.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/fcm/config/FcmSenderConfig.kt @@ -18,7 +18,7 @@ import java.util.* @Configuration class FcmSenderConfig( @Transient private val serverConfig: FcmServerConfig, - @Transient private val beanFactory: BeanFactory + @Transient private val beanFactory: BeanFactory, ) { @Bean fun fcmSenderProps(): FcmSender { @@ -29,8 +29,9 @@ class FcmSenderConfig( return when (sender) { "rest", "org.radarbase.fcm.downstream.AdminSdkFcmSender" -> AdminSdkFcmSender( beanFactory.getBean( - "firebaseOptions", FirebaseOptions::class.java - ) + "firebaseOptions", + FirebaseOptions::class.java, + ), ) "disabled" -> DisabledFcmSender() diff --git a/src/main/java/org/radarbase/fcm/config/FcmServerConfig.kt b/appserver-legacy/src/main/kotlin/org/radarbase/fcm/config/FcmServerConfig.kt similarity index 93% rename from src/main/java/org/radarbase/fcm/config/FcmServerConfig.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/fcm/config/FcmServerConfig.kt index 927f93678..e9f8372b9 100644 --- a/src/main/java/org/radarbase/fcm/config/FcmServerConfig.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/fcm/config/FcmServerConfig.kt @@ -28,7 +28,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties * @author yatharthranjan */ @ConfigurationProperties(value = "fcmserver") -data class FcmServerConfig ( +data class FcmServerConfig( var fcmsender: String? = null, - var credentials: String? = null - ) + var credentials: String? = null, +) diff --git a/src/main/java/org/radarbase/fcm/downstream/AdminSdkFcmSender.kt b/appserver-legacy/src/main/kotlin/org/radarbase/fcm/downstream/AdminSdkFcmSender.kt similarity index 86% rename from src/main/java/org/radarbase/fcm/downstream/AdminSdkFcmSender.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/fcm/downstream/AdminSdkFcmSender.kt index 5ecc35fdf..6026098a0 100644 --- a/src/main/java/org/radarbase/fcm/downstream/AdminSdkFcmSender.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/fcm/downstream/AdminSdkFcmSender.kt @@ -22,7 +22,16 @@ package org.radarbase.fcm.downstream import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseOptions -import com.google.firebase.messaging.* +import com.google.firebase.messaging.AndroidConfig +import com.google.firebase.messaging.AndroidNotification +import com.google.firebase.messaging.ApnsConfig +import com.google.firebase.messaging.Aps +import com.google.firebase.messaging.ApsAlert +import com.google.firebase.messaging.FcmOptions +import com.google.firebase.messaging.FirebaseMessaging +import com.google.firebase.messaging.FirebaseMessagingException +import com.google.firebase.messaging.Message +import com.google.firebase.messaging.Notification import org.radarbase.fcm.model.FcmDataMessage import org.radarbase.fcm.model.FcmDownstreamMessage import org.radarbase.fcm.model.FcmNotificationMessage @@ -69,34 +78,38 @@ class AdminSdkFcmSender(options: FirebaseOptions) : FcmSender { AndroidConfig.builder() .setCollapseKey(downstreamMessage.collapseKey) .setPriority( - if (priority == null) AndroidConfig.Priority.HIGH else AndroidConfig.Priority.valueOf( - priority - ) + if (priority == null) { + AndroidConfig.Priority.HIGH + } else { + AndroidConfig.Priority.valueOf( + priority, + ) + }, ) .setTtl(ttl.toMillis()) .setNotification(getAndroidNotification(notificationMessage)) .putAllData(notificationMessage.data) - .build() + .build(), ) .setApnsConfig( getApnsConfigBuilder(notificationMessage, ttl)!! .putHeader("apns-push-type", "alert") - .build() + .build(), ) .putAllData(notificationMessage.data) .setCondition(notificationMessage.condition) .setNotification( Notification.builder() .setBody( - notificationMessage.notification!!.getOrDefault("body", "").toString() + notificationMessage.notification!!.getOrDefault("body", "").toString(), ) .setTitle( - notificationMessage.notification!!.getOrDefault("title", "").toString() + notificationMessage.notification!!.getOrDefault("title", "").toString(), ) .setImage( - notificationMessage.notification!!.getOrDefault("image_url", "").toString() + notificationMessage.notification!!.getOrDefault("image_url", "").toString(), ) - .build() + .build(), ) } else if (downstreamMessage is FcmDataMessage) { val dataMessage = downstreamMessage @@ -105,20 +118,24 @@ class AdminSdkFcmSender(options: FirebaseOptions) : FcmSender { AndroidConfig.builder() .setCollapseKey(downstreamMessage.collapseKey) .setPriority( - if (priority == null) AndroidConfig.Priority.NORMAL else AndroidConfig.Priority.valueOf( - priority - ) + if (priority == null) { + AndroidConfig.Priority.NORMAL + } else { + AndroidConfig.Priority.valueOf( + priority, + ) + }, ) .setTtl(ttl.toMillis()) .putAllData(dataMessage.data) - .build() + .build(), ) .setApnsConfig(getApnsConfigBuilder(dataMessage, ttl)!!.build()) .setCondition(dataMessage.condition) .putAllData(dataMessage.data) } else { throw IllegalArgumentException( - "The Message type is not known." + downstreamMessage.javaClass + "The Message type is not known." + downstreamMessage.javaClass, ) } @@ -131,10 +148,10 @@ class AdminSdkFcmSender(options: FirebaseOptions) : FcmSender { AndroidNotification.builder() .setBody(notificationMessage.notification!!.getOrDefault("body", "").toString()) .setTitle( - notificationMessage.notification!!.getOrDefault("title", "").toString() + notificationMessage.notification!!.getOrDefault("title", "").toString(), ) .setChannelId( - getString(notificationMessage.notification!!["android_channel_id"]) + getString(notificationMessage.notification!!["android_channel_id"]), ) .setColor(getString(notificationMessage.notification!!["color"])) .setTag(getString(notificationMessage.notification!!["tag"])) @@ -148,20 +165,20 @@ class AdminSdkFcmSender(options: FirebaseOptions) : FcmSender { if (bodyLocKey != null) { builder .setBodyLocalizationKey( - getString(notificationMessage.notification!!["body_loc_key"]) + getString(notificationMessage.notification!!["body_loc_key"]), ) .addBodyLocalizationArg( - getString(notificationMessage.notification!!["body_loc_args"]) + getString(notificationMessage.notification!!["body_loc_args"]), ) } if (titleLocKey != null) { builder .addTitleLocalizationArg( - getString(notificationMessage.notification!!["title_loc_args"]) + getString(notificationMessage.notification!!["title_loc_args"]), ) .setTitleLocalizationKey( - getString(notificationMessage.notification!!["title_loc_key"]) + getString(notificationMessage.notification!!["title_loc_key"]), ) } @@ -183,7 +200,7 @@ class AdminSdkFcmSender(options: FirebaseOptions) : FcmSender { // expressed in seconds (UTC). config.putHeader( "apns-expiration", - Instant.now().plus(ttl).epochSecond.toString() + Instant.now().plus(ttl).epochSecond.toString(), ) if (message is FcmNotificationMessage) { diff --git a/src/main/java/org/radarbase/fcm/downstream/DisabledFcmSender.kt b/appserver-legacy/src/main/kotlin/org/radarbase/fcm/downstream/DisabledFcmSender.kt similarity index 100% rename from src/main/java/org/radarbase/fcm/downstream/DisabledFcmSender.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/fcm/downstream/DisabledFcmSender.kt diff --git a/src/main/java/org/radarbase/fcm/downstream/FcmSender.kt b/appserver-legacy/src/main/kotlin/org/radarbase/fcm/downstream/FcmSender.kt similarity index 100% rename from src/main/java/org/radarbase/fcm/downstream/FcmSender.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/fcm/downstream/FcmSender.kt diff --git a/src/main/java/org/radarbase/fcm/model/FcmDataMessage.kt b/appserver-legacy/src/main/kotlin/org/radarbase/fcm/model/FcmDataMessage.kt similarity index 99% rename from src/main/java/org/radarbase/fcm/model/FcmDataMessage.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/fcm/model/FcmDataMessage.kt index a2c4afe05..80d6c75a2 100644 --- a/src/main/java/org/radarbase/fcm/model/FcmDataMessage.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/fcm/model/FcmDataMessage.kt @@ -43,6 +43,5 @@ class FcmDataMessage : FcmDownstreamMessage() { message.data = this.data return message } - } -} \ No newline at end of file +} diff --git a/src/main/java/org/radarbase/fcm/model/FcmDownstreamMessage.kt b/appserver-legacy/src/main/kotlin/org/radarbase/fcm/model/FcmDownstreamMessage.kt similarity index 86% rename from src/main/java/org/radarbase/fcm/model/FcmDownstreamMessage.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/fcm/model/FcmDownstreamMessage.kt index 8c2ea9e88..7ac6c0515 100644 --- a/src/main/java/org/radarbase/fcm/model/FcmDownstreamMessage.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/fcm/model/FcmDownstreamMessage.kt @@ -75,45 +75,45 @@ abstract class FcmDownstreamMessage : FcmMessage { protected var deliveryReceiptRequested: Boolean? = null protected var dryRun: Boolean? = null - fun to(to: String) = apply { + fun to(to: String) = apply { this.to = to } as T - fun condition(condition: String?) = apply { - this.condition = condition + fun condition(condition: String?) = apply { + this.condition = condition } as T fun messageId(messageId: String) = apply { - this.messageId = messageId + this.messageId = messageId } as T fun collapseKey(collapseKey: String?) = apply { - this.collapseKey = collapseKey + this.collapseKey = collapseKey } as T fun priority(priority: String?) = apply { - this.priority = priority + this.priority = priority } as T fun contentAvailable(contentAvailable: Boolean?) = apply { - this.contentAvailable = contentAvailable + this.contentAvailable = contentAvailable } as T fun mutableContent(mutableContent: Boolean?) = apply { - this.mutableContent = mutableContent + this.mutableContent = mutableContent } as T fun timeToLive(timeToLive: Int?) = apply { - this.timeToLive = timeToLive + this.timeToLive = timeToLive } as T fun deliveryReceiptRequested(deliveryReceiptRequested: Boolean?) = - apply { - this.deliveryReceiptRequested = deliveryReceiptRequested - } as T + apply { + this.deliveryReceiptRequested = deliveryReceiptRequested + } as T - fun dryRun(dryRun: Boolean?) = apply { - this.dryRun = dryRun + fun dryRun(dryRun: Boolean?) = apply { + this.dryRun = dryRun } as T protected fun applyTo(message: FcmDownstreamMessage) { diff --git a/src/main/java/org/radarbase/fcm/model/FcmMessage.kt b/appserver-legacy/src/main/kotlin/org/radarbase/fcm/model/FcmMessage.kt similarity index 97% rename from src/main/java/org/radarbase/fcm/model/FcmMessage.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/fcm/model/FcmMessage.kt index d21fff1a8..95bd3a434 100644 --- a/src/main/java/org/radarbase/fcm/model/FcmMessage.kt +++ b/appserver-legacy/src/main/kotlin/org/radarbase/fcm/model/FcmMessage.kt @@ -22,4 +22,4 @@ package org.radarbase.fcm.model /** @author yatharthranjan */ -interface FcmMessage +interface FcmMessage diff --git a/src/main/java/org/radarbase/fcm/model/FcmNotificationMessage.kt b/appserver-legacy/src/main/kotlin/org/radarbase/fcm/model/FcmNotificationMessage.kt similarity index 100% rename from src/main/java/org/radarbase/fcm/model/FcmNotificationMessage.kt rename to appserver-legacy/src/main/kotlin/org/radarbase/fcm/model/FcmNotificationMessage.kt diff --git a/src/main/resources/META-INF/spring-configuration-metadata.json b/appserver-legacy/src/main/resources/META-INF/spring-configuration-metadata.json similarity index 100% rename from src/main/resources/META-INF/spring-configuration-metadata.json rename to appserver-legacy/src/main/resources/META-INF/spring-configuration-metadata.json diff --git a/src/main/resources/META-INF/spring-devtools.properties b/appserver-legacy/src/main/resources/META-INF/spring-devtools.properties similarity index 100% rename from src/main/resources/META-INF/spring-devtools.properties rename to appserver-legacy/src/main/resources/META-INF/spring-devtools.properties diff --git a/src/main/resources/application-dev.properties b/appserver-legacy/src/main/resources/application-dev.properties similarity index 100% rename from src/main/resources/application-dev.properties rename to appserver-legacy/src/main/resources/application-dev.properties diff --git a/src/main/resources/application-prod.properties b/appserver-legacy/src/main/resources/application-prod.properties similarity index 96% rename from src/main/resources/application-prod.properties rename to appserver-legacy/src/main/resources/application-prod.properties index 38e44407a..5fad42184 100644 --- a/src/main/resources/application-prod.properties +++ b/appserver-legacy/src/main/resources/application-prod.properties @@ -21,9 +21,9 @@ # DATASOURCE spring.jpa.hibernate.ddl-auto=none -spring.datasource.username=postgres -spring.datasource.password=radar -spring.datasource.url=jdbc:postgresql://localhost:5432/radar +spring.datasource.username=appserveruser +spring.datasource.password=appserverpwd +spring.datasource.url=jdbc:postgresql://localhost:5432/appserverdb # jdbc:hsqldb:hsql://hsqldb:9001/appserver for docker deployment # jdbc:hsqldb:mem:/appserver for dev testing #jdbc:hsqldb:hsql://localhost:9001/appserver for running hsql separately in dev or testing diff --git a/src/main/resources/application.properties b/appserver-legacy/src/main/resources/application.properties similarity index 100% rename from src/main/resources/application.properties rename to appserver-legacy/src/main/resources/application.properties diff --git a/src/main/resources/db/changelog/changes/00000000000000_initial_schema-20181129171324_changelog.yml b/appserver-legacy/src/main/resources/db/changelog/changes/00000000000000_initial_schema-20181129171324_changelog.yml similarity index 100% rename from src/main/resources/db/changelog/changes/00000000000000_initial_schema-20181129171324_changelog.yml rename to appserver-legacy/src/main/resources/db/changelog/changes/00000000000000_initial_schema-20181129171324_changelog.yml diff --git a/src/main/resources/db/changelog/changes/00000000000000_update_schema-20190408172324_changelog.yml b/appserver-legacy/src/main/resources/db/changelog/changes/00000000000000_update_schema-20190408172324_changelog.yml similarity index 100% rename from src/main/resources/db/changelog/changes/00000000000000_update_schema-20190408172324_changelog.yml rename to appserver-legacy/src/main/resources/db/changelog/changes/00000000000000_update_schema-20190408172324_changelog.yml diff --git a/src/main/resources/db/changelog/changes/00000000000000_update_schema-20190415142024_changelog.yml b/appserver-legacy/src/main/resources/db/changelog/changes/00000000000000_update_schema-20190415142024_changelog.yml similarity index 100% rename from src/main/resources/db/changelog/changes/00000000000000_update_schema-20190415142024_changelog.yml rename to appserver-legacy/src/main/resources/db/changelog/changes/00000000000000_update_schema-20190415142024_changelog.yml diff --git a/src/main/resources/db/changelog/changes/00000000000000_update_schema-20190418153624_changelog.yml b/appserver-legacy/src/main/resources/db/changelog/changes/00000000000000_update_schema-20190418153624_changelog.yml similarity index 100% rename from src/main/resources/db/changelog/changes/00000000000000_update_schema-20190418153624_changelog.yml rename to appserver-legacy/src/main/resources/db/changelog/changes/00000000000000_update_schema-20190418153624_changelog.yml diff --git a/src/main/resources/db/changelog/changes/00000000000000_update_schema-20190722162524_changelog.yml b/appserver-legacy/src/main/resources/db/changelog/changes/00000000000000_update_schema-20190722162524_changelog.yml similarity index 100% rename from src/main/resources/db/changelog/changes/00000000000000_update_schema-20190722162524_changelog.yml rename to appserver-legacy/src/main/resources/db/changelog/changes/00000000000000_update_schema-20190722162524_changelog.yml diff --git a/src/main/resources/db/changelog/changes/00000000000000_update_schema-20190722184824_changelog.yml b/appserver-legacy/src/main/resources/db/changelog/changes/00000000000000_update_schema-20190722184824_changelog.yml similarity index 100% rename from src/main/resources/db/changelog/changes/00000000000000_update_schema-20190722184824_changelog.yml rename to appserver-legacy/src/main/resources/db/changelog/changes/00000000000000_update_schema-20190722184824_changelog.yml diff --git a/src/main/resources/db/changelog/changes/00000000000000_update_schema-20190722185424_changelog.yml b/appserver-legacy/src/main/resources/db/changelog/changes/00000000000000_update_schema-20190722185424_changelog.yml similarity index 100% rename from src/main/resources/db/changelog/changes/00000000000000_update_schema-20190722185424_changelog.yml rename to appserver-legacy/src/main/resources/db/changelog/changes/00000000000000_update_schema-20190722185424_changelog.yml diff --git a/src/main/resources/db/changelog/changes/00000000000000_update_schema-20191106172924_changelog.yml b/appserver-legacy/src/main/resources/db/changelog/changes/00000000000000_update_schema-20191106172924_changelog.yml similarity index 100% rename from src/main/resources/db/changelog/changes/00000000000000_update_schema-20191106172924_changelog.yml rename to appserver-legacy/src/main/resources/db/changelog/changes/00000000000000_update_schema-20191106172924_changelog.yml diff --git a/src/main/resources/db/changelog/changes/00000000000000_update_schema-20200305184827_changelog.yml b/appserver-legacy/src/main/resources/db/changelog/changes/00000000000000_update_schema-20200305184827_changelog.yml similarity index 100% rename from src/main/resources/db/changelog/changes/00000000000000_update_schema-20200305184827_changelog.yml rename to appserver-legacy/src/main/resources/db/changelog/changes/00000000000000_update_schema-20200305184827_changelog.yml diff --git a/src/main/resources/db/changelog/changes/00000000000001_update_schema-20190726152824_changelog.yml b/appserver-legacy/src/main/resources/db/changelog/changes/00000000000001_update_schema-20190726152824_changelog.yml similarity index 100% rename from src/main/resources/db/changelog/changes/00000000000001_update_schema-20190726152824_changelog.yml rename to appserver-legacy/src/main/resources/db/changelog/changes/00000000000001_update_schema-20190726152824_changelog.yml diff --git a/src/main/resources/db/changelog/changes/00000000000001_update_schema-20190726164324_changelog.yml b/appserver-legacy/src/main/resources/db/changelog/changes/00000000000001_update_schema-20190726164324_changelog.yml similarity index 100% rename from src/main/resources/db/changelog/changes/00000000000001_update_schema-20190726164324_changelog.yml rename to appserver-legacy/src/main/resources/db/changelog/changes/00000000000001_update_schema-20190726164324_changelog.yml diff --git a/src/main/resources/db/changelog/changes/00000000000001_update_schema-20191009192824_changelog.yml b/appserver-legacy/src/main/resources/db/changelog/changes/00000000000001_update_schema-20191009192824_changelog.yml similarity index 100% rename from src/main/resources/db/changelog/changes/00000000000001_update_schema-20191009192824_changelog.yml rename to appserver-legacy/src/main/resources/db/changelog/changes/00000000000001_update_schema-20191009192824_changelog.yml diff --git a/src/main/resources/db/changelog/changes/00000000000002_update_schema-20200309192824_changelog.yml b/appserver-legacy/src/main/resources/db/changelog/changes/00000000000002_update_schema-20200309192824_changelog.yml similarity index 100% rename from src/main/resources/db/changelog/changes/00000000000002_update_schema-20200309192824_changelog.yml rename to appserver-legacy/src/main/resources/db/changelog/changes/00000000000002_update_schema-20200309192824_changelog.yml diff --git a/src/main/resources/db/changelog/changes/00000000000002_update_schema-20220705192824_changelog.yml b/appserver-legacy/src/main/resources/db/changelog/changes/00000000000002_update_schema-20220705192824_changelog.yml similarity index 100% rename from src/main/resources/db/changelog/changes/00000000000002_update_schema-20220705192824_changelog.yml rename to appserver-legacy/src/main/resources/db/changelog/changes/00000000000002_update_schema-20220705192824_changelog.yml diff --git a/src/main/resources/db/changelog/changes/00000000000002_update_schema-20220802152824_changelog.yml b/appserver-legacy/src/main/resources/db/changelog/changes/00000000000002_update_schema-20220802152824_changelog.yml similarity index 100% rename from src/main/resources/db/changelog/changes/00000000000002_update_schema-20220802152824_changelog.yml rename to appserver-legacy/src/main/resources/db/changelog/changes/00000000000002_update_schema-20220802152824_changelog.yml diff --git a/src/main/resources/db/changelog/changes/00000000000002_update_schema-20220802184827_changelog.yml b/appserver-legacy/src/main/resources/db/changelog/changes/00000000000002_update_schema-20220802184827_changelog.yml similarity index 100% rename from src/main/resources/db/changelog/changes/00000000000002_update_schema-20220802184827_changelog.yml rename to appserver-legacy/src/main/resources/db/changelog/changes/00000000000002_update_schema-20220802184827_changelog.yml diff --git a/src/main/resources/db/changelog/changes/00000000000002_update_schema-20231020184827_changelog.yml b/appserver-legacy/src/main/resources/db/changelog/changes/00000000000002_update_schema-20231020184827_changelog.yml similarity index 100% rename from src/main/resources/db/changelog/changes/00000000000002_update_schema-20231020184827_changelog.yml rename to appserver-legacy/src/main/resources/db/changelog/changes/00000000000002_update_schema-20231020184827_changelog.yml diff --git a/src/main/resources/db/changelog/changes/00000000000002_update_schema-20240522185927_changelog.yml b/appserver-legacy/src/main/resources/db/changelog/changes/00000000000002_update_schema-20240522185927_changelog.yml similarity index 100% rename from src/main/resources/db/changelog/changes/00000000000002_update_schema-20240522185927_changelog.yml rename to appserver-legacy/src/main/resources/db/changelog/changes/00000000000002_update_schema-20240522185927_changelog.yml diff --git a/src/main/resources/db/changelog/changes/00000000000002_update_schema-20240704090100_changelog.yml b/appserver-legacy/src/main/resources/db/changelog/changes/00000000000002_update_schema-20240704090100_changelog.yml similarity index 100% rename from src/main/resources/db/changelog/changes/00000000000002_update_schema-20240704090100_changelog.yml rename to appserver-legacy/src/main/resources/db/changelog/changes/00000000000002_update_schema-20240704090100_changelog.yml diff --git a/src/main/resources/db/changelog/changes/00000000000002_update_schema-20240705131000_changelog.yml b/appserver-legacy/src/main/resources/db/changelog/changes/00000000000002_update_schema-20240705131000_changelog.yml similarity index 100% rename from src/main/resources/db/changelog/changes/00000000000002_update_schema-20240705131000_changelog.yml rename to appserver-legacy/src/main/resources/db/changelog/changes/00000000000002_update_schema-20240705131000_changelog.yml diff --git a/src/main/resources/db/changelog/changes/quartz_init.xml b/appserver-legacy/src/main/resources/db/changelog/changes/quartz_init.xml similarity index 100% rename from src/main/resources/db/changelog/changes/quartz_init.xml rename to appserver-legacy/src/main/resources/db/changelog/changes/quartz_init.xml diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/appserver-legacy/src/main/resources/db/changelog/db.changelog-master.yaml similarity index 100% rename from src/main/resources/db/changelog/db.changelog-master.yaml rename to appserver-legacy/src/main/resources/db/changelog/db.changelog-master.yaml diff --git a/src/main/resources/inbound-xmpp.xml b/appserver-legacy/src/main/resources/inbound-xmpp.xml similarity index 100% rename from src/main/resources/inbound-xmpp.xml rename to appserver-legacy/src/main/resources/inbound-xmpp.xml diff --git a/src/main/resources/liquibase-task.properties b/appserver-legacy/src/main/resources/liquibase-task.properties similarity index 100% rename from src/main/resources/liquibase-task.properties rename to appserver-legacy/src/main/resources/liquibase-task.properties diff --git a/src/main/resources/logback-spring.xml b/appserver-legacy/src/main/resources/logback-spring.xml similarity index 100% rename from src/main/resources/logback-spring.xml rename to appserver-legacy/src/main/resources/logback-spring.xml diff --git a/src/main/resources/outbound-xmpp.xml b/appserver-legacy/src/main/resources/outbound-xmpp.xml similarity index 100% rename from src/main/resources/outbound-xmpp.xml rename to appserver-legacy/src/main/resources/outbound-xmpp.xml diff --git a/src/main/resources/radar-is.yml b/appserver-legacy/src/main/resources/radar-is.yml similarity index 100% rename from src/main/resources/radar-is.yml rename to appserver-legacy/src/main/resources/radar-is.yml diff --git a/src/test/java/org/radarbase/appserver/AppserverApplicationTests.kt b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/AppserverApplicationTests.kt similarity index 82% rename from src/test/java/org/radarbase/appserver/AppserverApplicationTests.kt rename to appserver-legacy/src/test/kotlin/org/radarbase/appserver/AppserverApplicationTests.kt index af8d92471..4e66d4ba6 100644 --- a/src/test/java/org/radarbase/appserver/AppserverApplicationTests.kt +++ b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/AppserverApplicationTests.kt @@ -3,8 +3,8 @@ package org.radarbase.appserver import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test -//@RunWith(SpringRunner.class) -//@SpringBootTest +// @RunWith(SpringRunner.class) +// @SpringBootTest class AppserverApplicationTests { @Test @Disabled("No needed to test") diff --git a/src/test/java/org/radarbase/appserver/controller/FcmDataMessageControllerTest.kt b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/controller/FcmDataMessageControllerTest.kt similarity index 88% rename from src/test/java/org/radarbase/appserver/controller/FcmDataMessageControllerTest.kt rename to appserver-legacy/src/test/kotlin/org/radarbase/appserver/controller/FcmDataMessageControllerTest.kt index 07dc74d1d..a93d8c67a 100644 --- a/src/test/java/org/radarbase/appserver/controller/FcmDataMessageControllerTest.kt +++ b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/controller/FcmDataMessageControllerTest.kt @@ -49,7 +49,6 @@ import java.net.URI import java.time.Duration import java.time.Instant - @ExtendWith(SpringExtension::class) @WebMvcTest(FcmDataMessageController::class) @AutoConfigureMockMvc(addFilters = false) @@ -144,11 +143,10 @@ class FcmDataMessageControllerTest { }.`when`(dataMessageService).removeDataMessagesForUser(any(), any()) } - @Test fun getAllDataMessages() { mockMvc.perform( - MockMvcRequestBuilders.get(URI.create("/${PathsUtil.MESSAGING_DATA_PATH}")) + MockMvcRequestBuilders.get(URI.create("/${PathsUtil.MESSAGING_DATA_PATH}")), ) .andExpect(status().isOk) .andExpect(jsonPath(DATA_MESSAGES_JSON_PATH, hasSize(1))) @@ -158,7 +156,7 @@ class FcmDataMessageControllerTest { @Test fun getDataMessageUsingId() { mockMvc.perform( - MockMvcRequestBuilders.get(URI.create("/${PathsUtil.MESSAGING_DATA_PATH}/1")) + MockMvcRequestBuilders.get(URI.create("/${PathsUtil.MESSAGING_DATA_PATH}/1")), ) .andExpect(status().isOk) .andExpect(jsonPath("$.fcmMessageId", `is`(FCM_MESSAGE_ID))) @@ -173,9 +171,11 @@ class FcmDataMessageControllerTest { @Test fun getDataMessagesUsingProjectIdAndSubjectId() { mockMvc.perform( - MockMvcRequestBuilders.get(URI.create( - "/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.USER_PATH}/$USER_ID/${PathsUtil.MESSAGING_DATA_PATH}" - )) + MockMvcRequestBuilders.get( + URI.create( + "/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.USER_PATH}/$USER_ID/${PathsUtil.MESSAGING_DATA_PATH}", + ), + ), ) .andExpect(status().isOk) .andExpect(jsonPath(DATA_MESSAGES_JSON_PATH, hasSize(1))) @@ -185,9 +185,11 @@ class FcmDataMessageControllerTest { @Test fun getDataMessagesUsingProjectId() { mockMvc.perform( - MockMvcRequestBuilders.get(URI.create( - "/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.MESSAGING_DATA_PATH}" - )) + MockMvcRequestBuilders.get( + URI.create( + "/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.MESSAGING_DATA_PATH}", + ), + ), ) .andExpect(status().isOk) .andExpect(jsonPath(DATA_MESSAGES_JSON_PATH, hasSize(1))) @@ -207,11 +209,13 @@ class FcmDataMessageControllerTest { } mockMvc.perform( - MockMvcRequestBuilders.post(URI.create( - "/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.USER_PATH}/$USER_ID/${PathsUtil.MESSAGING_DATA_PATH}" - )) + MockMvcRequestBuilders.post( + URI.create( + "/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.USER_PATH}/$USER_ID/${PathsUtil.MESSAGING_DATA_PATH}", + ), + ) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsBytes(dataMessageDto2)) + .content(objectMapper.writeValueAsBytes(dataMessageDto2)), ) .andExpect(status().isCreated) .andExpect(jsonPath("$.fcmMessageId", `is`(FCM_MESSAGE_ID + "7"))) @@ -245,11 +249,13 @@ class FcmDataMessageControllerTest { } mockMvc.perform( - MockMvcRequestBuilders.post(URI.create( - "/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.USER_PATH}/$USER_ID/${PathsUtil.MESSAGING_DATA_PATH}/batch" - )) + MockMvcRequestBuilders.post( + URI.create( + "/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.USER_PATH}/$USER_ID/${PathsUtil.MESSAGING_DATA_PATH}/batch", + ), + ) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsBytes(fcmDataMessages)) + .content(objectMapper.writeValueAsBytes(fcmDataMessages)), ) .andExpect(status().isOk) .andExpect(jsonPath(DATA_MESSAGES_JSON_PATH, hasSize(2))) @@ -259,11 +265,11 @@ class FcmDataMessageControllerTest { @Test fun deleteDataMessagesForUser() { mockMvc.perform( - MockMvcRequestBuilders.delete(URI.create( - "/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.USER_PATH}/$USER_ID/${PathsUtil.MESSAGING_DATA_PATH}" - )) + MockMvcRequestBuilders.delete( + URI.create( + "/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.USER_PATH}/$USER_ID/${PathsUtil.MESSAGING_DATA_PATH}", + ), + ), ) } - } - diff --git a/src/test/java/org/radarbase/appserver/controller/FcmNotificationControllerTest.kt b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/controller/FcmNotificationControllerTest.kt similarity index 96% rename from src/test/java/org/radarbase/appserver/controller/FcmNotificationControllerTest.kt rename to appserver-legacy/src/test/kotlin/org/radarbase/appserver/controller/FcmNotificationControllerTest.kt index 0f900d84f..f0fe79a46 100644 --- a/src/test/java/org/radarbase/appserver/controller/FcmNotificationControllerTest.kt +++ b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/controller/FcmNotificationControllerTest.kt @@ -110,8 +110,8 @@ class FcmNotificationControllerTest { eq(notificationDto2), eq(USER_ID), eq(PROJECT_ID), - eq(SCHEDULE_FALSE) - ) + eq(SCHEDULE_FALSE), + ), ) .willReturn(notificationDto2) @@ -138,8 +138,8 @@ class FcmNotificationControllerTest { eq(fcmNotifications), eq(USER_ID), eq(PROJECT_ID), - eq(SCHEDULE_TRUE) - ) + eq(SCHEDULE_TRUE), + ), ) .willReturn(fcmNotifications) @@ -148,8 +148,8 @@ class FcmNotificationControllerTest { eq(fcmNotifications), eq(USER_ID), eq(PROJECT_ID), - eq(SCHEDULE_FALSE) - ) + eq(SCHEDULE_FALSE), + ), ) .willReturn(fcmNotifications) @@ -167,7 +167,6 @@ class FcmNotificationControllerTest { }.`when`(notificationService).removeNotificationsForUser(any(String::class.java), any(String::class.java)) } - @Test fun getAllNotifications() { mockMvc.perform(MockMvcRequestBuilders.get(URI.create("/${PathsUtil.MESSAGING_NOTIFICATION_PATH}"))) @@ -196,9 +195,9 @@ class FcmNotificationControllerTest { mockMvc.perform( MockMvcRequestBuilders.get( URI.create( - "/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.USER_PATH}/$USER_ID/${PathsUtil.MESSAGING_NOTIFICATION_PATH}" - ) - ) + "/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.USER_PATH}/$USER_ID/${PathsUtil.MESSAGING_NOTIFICATION_PATH}", + ), + ), ) .andExpect(status().isOk) .andExpect(jsonPath(NOTIFICATIONS_JSON_PATH).value(hasSize(1))) @@ -210,8 +209,8 @@ class FcmNotificationControllerTest { fun getNotificationsUsingProjectId() { mockMvc.perform( MockMvcRequestBuilders.get( - URI.create("/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.MESSAGING_NOTIFICATION_PATH}") - ) + URI.create("/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.MESSAGING_NOTIFICATION_PATH}"), + ), ) .andExpect(status().isOk) .andExpect(jsonPath(NOTIFICATIONS_JSON_PATH).value(hasSize(1))) @@ -237,11 +236,11 @@ class FcmNotificationControllerTest { mockMvc.perform( MockMvcRequestBuilders.post( URI.create( - "/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.USER_PATH}/$USER_ID/${PathsUtil.MESSAGING_NOTIFICATION_PATH}" - ) + "/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.USER_PATH}/$USER_ID/${PathsUtil.MESSAGING_NOTIFICATION_PATH}", + ), ) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsBytes(notificationDto2)) + .content(objectMapper.writeValueAsBytes(notificationDto2)), ) .andExpect(status().isCreated) .andExpect(jsonPath("$.title").value(TITLE_2)) @@ -267,11 +266,11 @@ class FcmNotificationControllerTest { mockMvc.perform( MockMvcRequestBuilders.post( URI.create( - "/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.USER_PATH}/$USER_ID/${PathsUtil.MESSAGING_NOTIFICATION_PATH}?schedule=false" - ) + "/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.USER_PATH}/$USER_ID/${PathsUtil.MESSAGING_NOTIFICATION_PATH}?schedule=false", + ), ) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsBytes(notificationDto2)) + .content(objectMapper.writeValueAsBytes(notificationDto2)), ) .andExpect { status().isCreated } .andExpect { jsonPath("$.title").value(TITLE_2) } @@ -284,9 +283,9 @@ class FcmNotificationControllerTest { mockMvc.perform( MockMvcRequestBuilders.post( URI.create( - "/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.USER_PATH}/$USER_ID/${PathsUtil.MESSAGING_NOTIFICATION_PATH}/2/schedule" - ) - ) + "/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.USER_PATH}/$USER_ID/${PathsUtil.MESSAGING_NOTIFICATION_PATH}/2/schedule", + ), + ), ) .andExpect(status().isOk) .andExpect(jsonPath("$.title").value(TITLE_2)) @@ -329,11 +328,11 @@ class FcmNotificationControllerTest { mockMvc.perform( MockMvcRequestBuilders.post( URI.create( - "/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.USER_PATH}/$USER_ID/${PathsUtil.MESSAGING_NOTIFICATION_PATH}/batch" - ) + "/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.USER_PATH}/$USER_ID/${PathsUtil.MESSAGING_NOTIFICATION_PATH}/batch", + ), ) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsBytes(fcmNotifications)) + .content(objectMapper.writeValueAsBytes(fcmNotifications)), ) .andExpect(status().isOk) .andExpect(jsonPath(NOTIFICATIONS_JSON_PATH).value(hasSize(2))) @@ -376,11 +375,11 @@ class FcmNotificationControllerTest { mockMvc.perform( MockMvcRequestBuilders.post( URI.create( - "/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.USER_PATH}/$USER_ID/${PathsUtil.MESSAGING_NOTIFICATION_PATH}/batch?schedule=false" - ) + "/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.USER_PATH}/$USER_ID/${PathsUtil.MESSAGING_NOTIFICATION_PATH}/batch?schedule=false", + ), ) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsBytes(fcmNotifications)) + .content(objectMapper.writeValueAsBytes(fcmNotifications)), ) .andExpect(status().isOk) .andExpect(jsonPath(NOTIFICATIONS_JSON_PATH).value(hasSize(2))) @@ -393,9 +392,9 @@ class FcmNotificationControllerTest { mockMvc.perform( MockMvcRequestBuilders.post( URI.create( - "/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.USER_PATH}/$USER_ID/${PathsUtil.MESSAGING_NOTIFICATION_PATH}/schedule" - ) - ) + "/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.USER_PATH}/$USER_ID/${PathsUtil.MESSAGING_NOTIFICATION_PATH}/schedule", + ), + ), ) .andExpect(status().isOk) .andExpect(jsonPath(NOTIFICATIONS_JSON_PATH).value(hasSize(2))) @@ -408,13 +407,12 @@ class FcmNotificationControllerTest { mockMvc.perform( MockMvcRequestBuilders.delete( URI.create( - "/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.USER_PATH}/$USER_ID/${PathsUtil.MESSAGING_NOTIFICATION_PATH}" - ) - ) + "/${PathsUtil.PROJECT_PATH}/$PROJECT_ID/${PathsUtil.USER_PATH}/$USER_ID/${PathsUtil.MESSAGING_NOTIFICATION_PATH}", + ), + ), ) } - companion object { const val FCM_MESSAGE_ID = "123456" const val PROJECT_ID = "test-project" @@ -431,4 +429,4 @@ class FcmNotificationControllerTest { const val NOTIFICATION_TITLE_JSON_PATH = "$.notifications[0].title" const val NOTIFICATION_FCMID_JSON_PATH = "$.notifications[0].fcmMessageId" } -} \ No newline at end of file +} diff --git a/src/test/java/org/radarbase/appserver/controller/RadarProjectControllerTest.kt b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/controller/RadarProjectControllerTest.kt similarity index 96% rename from src/test/java/org/radarbase/appserver/controller/RadarProjectControllerTest.kt rename to appserver-legacy/src/test/kotlin/org/radarbase/appserver/controller/RadarProjectControllerTest.kt index 09ea4c692..96c0ace59 100644 --- a/src/test/java/org/radarbase/appserver/controller/RadarProjectControllerTest.kt +++ b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/controller/RadarProjectControllerTest.kt @@ -93,7 +93,7 @@ class RadarProjectControllerTest { mockMvc.perform( MockMvcRequestBuilders.post(URI("/projects")) .content(objectMapper.writeValueAsString(projectDtoNew)) - .contentType(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON), ) .andExpect(status().isCreated) .andExpect(jsonPath(PROJECT_ID_JSON_PATH, `is`("$PROJECT_ID-new"))) @@ -110,7 +110,7 @@ class RadarProjectControllerTest { mockMvc.perform( MockMvcRequestBuilders.put(URI("/projects/$PROJECT_ID")) .content(objectMapper.writeValueAsString(projectDtoUpdated)) - .contentType(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON), ) .andExpect(status().isOk) .andExpect(jsonPath(PROJECT_ID_JSON_PATH, `is`("$PROJECT_ID-updated"))) @@ -120,7 +120,7 @@ class RadarProjectControllerTest { @Test fun getAllProjects() { mockMvc.perform( - MockMvcRequestBuilders.get(URI("/projects")) + MockMvcRequestBuilders.get(URI("/projects")), ) .andExpect(status().isOk) .andExpect(jsonPath("$.projects[0].projectId", `is`(PROJECT_ID))) @@ -130,7 +130,7 @@ class RadarProjectControllerTest { @Test fun getProjectsUsingId() { mockMvc.perform( - MockMvcRequestBuilders.get(URI("/projects/project?id=1")) + MockMvcRequestBuilders.get(URI("/projects/project?id=1")), ) .andExpect(status().isOk) .andExpect(jsonPath(PROJECT_ID_JSON_PATH, `is`(PROJECT_ID))) @@ -140,7 +140,7 @@ class RadarProjectControllerTest { @Test fun getProjectsUsingProjectId() { mockMvc.perform( - MockMvcRequestBuilders.get(URI("/projects/$PROJECT_ID")) + MockMvcRequestBuilders.get(URI("/projects/$PROJECT_ID")), ) .andExpect(status().isOk) .andExpect(jsonPath(PROJECT_ID_JSON_PATH, `is`(PROJECT_ID))) diff --git a/src/test/java/org/radarbase/appserver/controller/RadarUserControllerTest.kt b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/controller/RadarUserControllerTest.kt similarity index 95% rename from src/test/java/org/radarbase/appserver/controller/RadarUserControllerTest.kt rename to appserver-legacy/src/test/kotlin/org/radarbase/appserver/controller/RadarUserControllerTest.kt index 334a94404..497ab1001 100644 --- a/src/test/java/org/radarbase/appserver/controller/RadarUserControllerTest.kt +++ b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/controller/RadarUserControllerTest.kt @@ -141,7 +141,7 @@ class RadarUserControllerTest { mockMvc.perform( MockMvcRequestBuilders.post(URI("/projects/test-project/users")) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(userDtoNew)) + .content(objectMapper.writeValueAsString(userDtoNew)), ) .andExpect { status().isCreated } .andExpect { jsonPath(FCM_TOKEN_JSON_PATH).value(FCM_TOKEN_2) } @@ -161,7 +161,7 @@ class RadarUserControllerTest { mockMvc.perform( MockMvcRequestBuilders.post(URI("/projects/test-project/users")) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(userDtoSameToken)) + .content(objectMapper.writeValueAsString(userDtoSameToken)), ).andExpect(status().isExpectationFailed) } @@ -179,7 +179,7 @@ class RadarUserControllerTest { mockMvc.perform( MockMvcRequestBuilders.put(URI("/projects/test-project/users/test-user-updated")) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(userDtoUpdated)) + .content(objectMapper.writeValueAsString(userDtoUpdated)), ) .andExpect(status().isOk) .andExpect(jsonPath(FCM_TOKEN_JSON_PATH).value(FCM_TOKEN_3)) @@ -207,13 +207,13 @@ class RadarUserControllerTest { } @Test - fun getRadarUserUsingSubjectId() { + fun getRadarUserUsingSubjectId() { mockMvc .perform(MockMvcRequestBuilders.get(URI("/users/test-user"))) .andExpect(status().isOk()) .andExpect(jsonPath(FCM_TOKEN_JSON_PATH).value(FCM_TOKEN_1)) - .andExpect(jsonPath(LANGUAGE_JSON_PATH).value("es")) - .andExpect(jsonPath(ENROLMENT_DATE_JSON_PATH).value(enrolmentDate.toString())) + .andExpect(jsonPath(LANGUAGE_JSON_PATH).value("es")) + .andExpect(jsonPath(ENROLMENT_DATE_JSON_PATH).value(enrolmentDate.toString())) } @Test @@ -222,8 +222,7 @@ class RadarUserControllerTest { .perform(MockMvcRequestBuilders.get(URI("/projects/test-project/users"))) .andExpect(status().isOk()) .andExpect(jsonPath("$.users[0].fcmToken").value(FCM_TOKEN_1)) - .andExpect(jsonPath("$.users[0].language").value("es")) - .andExpect(jsonPath("$.users[0].enrolmentDate").value(enrolmentDate.toString())) + .andExpect(jsonPath("$.users[0].language").value("es")) + .andExpect(jsonPath("$.users[0].enrolmentDate").value(enrolmentDate.toString())) } - -} \ No newline at end of file +} diff --git a/src/test/java/org/radarbase/appserver/controller/UploadControllerTest.kt b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/controller/UploadControllerTest.kt similarity index 90% rename from src/test/java/org/radarbase/appserver/controller/UploadControllerTest.kt rename to appserver-legacy/src/test/kotlin/org/radarbase/appserver/controller/UploadControllerTest.kt index 5a4b43428..5ea80ba80 100644 --- a/src/test/java/org/radarbase/appserver/controller/UploadControllerTest.kt +++ b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/controller/UploadControllerTest.kt @@ -35,10 +35,12 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status @WebMvcTest(UploadController::class) @AutoConfigureMockMvc(addFilters = false) -@TestPropertySource(properties = [ - "radar.file-upload.enabled=true", - "security.radar.managementportal.enabled=false" -]) +@TestPropertySource( + properties = [ + "radar.file-upload.enabled=true", + "security.radar.managementportal.enabled=false", + ], +) class UploadControllerTest { @Autowired @@ -56,7 +58,10 @@ class UploadControllerTest { } private val multipartFile = MockMultipartFile( - "file", "my-file.txt", "text/plain", file + "file", + "my-file.txt", + "text/plain", + file, ) @BeforeEach @@ -70,7 +75,7 @@ class UploadControllerTest { val uri = "/projects/$PROJECT_ID/users/$SUBJECT_ID/files/topics/$TOPIC_ID/upload" mockMvc.perform( - multipart(uri).file(multipartFile) + multipart(uri).file(multipartFile), ) .andExpect(status().isCreated) .andExpect(header().exists("Location")) diff --git a/src/test/java/org/radarbase/appserver/repository/DataMessageRepositoryTest.kt b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/repository/DataMessageRepositoryTest.kt similarity index 95% rename from src/test/java/org/radarbase/appserver/repository/DataMessageRepositoryTest.kt rename to appserver-legacy/src/test/kotlin/org/radarbase/appserver/repository/DataMessageRepositoryTest.kt index 650eb5bbc..2e3f795d8 100644 --- a/src/test/java/org/radarbase/appserver/repository/DataMessageRepositoryTest.kt +++ b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/repository/DataMessageRepositoryTest.kt @@ -22,7 +22,9 @@ package org.radarbase.appserver.repository import jakarta.validation.ConstraintViolationException -import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -150,7 +152,11 @@ class DataMessageRepositoryTest { @Test fun `when exists then return true`() { val exists = dataMessageRepository.existsByUserIdAndSourceIdAndScheduledTimeAndTtlSeconds( - user.id!!, DATA_MESSAGE_SOURCE_ID, scheduledTime, 86400) + user.id!!, + DATA_MESSAGE_SOURCE_ID, + scheduledTime, + 86400, + ) assertTrue(exists) assertTrue(dataMessageRepository.existsById(id!!)) diff --git a/src/test/java/org/radarbase/appserver/repository/NotificationRepositoryTest.kt b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/repository/NotificationRepositoryTest.kt similarity index 97% rename from src/test/java/org/radarbase/appserver/repository/NotificationRepositoryTest.kt rename to appserver-legacy/src/test/kotlin/org/radarbase/appserver/repository/NotificationRepositoryTest.kt index 3203b6302..1c30b0293 100644 --- a/src/test/java/org/radarbase/appserver/repository/NotificationRepositoryTest.kt +++ b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/repository/NotificationRepositoryTest.kt @@ -159,7 +159,13 @@ class NotificationRepositoryTest { @Test fun `when exists then return true`() { val exists = notificationRepository.existsByUserIdAndSourceIdAndScheduledTimeAndTitleAndBodyAndTypeAndTtlSeconds( - user.id!!, NOTIFICATION_SOURCE_ID, scheduledTime, NOTIFICATION_TITLE, NOTIFICATION_BODY, null, 86400 + user.id!!, + NOTIFICATION_SOURCE_ID, + scheduledTime, + NOTIFICATION_TITLE, + NOTIFICATION_BODY, + null, + 86400, ) assertTrue(exists) @@ -180,4 +186,3 @@ class NotificationRepositoryTest { assertNull(notification) } } - diff --git a/src/test/java/org/radarbase/appserver/repository/UserRepositoryTest.kt b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/repository/UserRepositoryTest.kt similarity index 94% rename from src/test/java/org/radarbase/appserver/repository/UserRepositoryTest.kt rename to appserver-legacy/src/test/kotlin/org/radarbase/appserver/repository/UserRepositoryTest.kt index 9d43bab28..33e48bb24 100644 --- a/src/test/java/org/radarbase/appserver/repository/UserRepositoryTest.kt +++ b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/repository/UserRepositoryTest.kt @@ -40,7 +40,6 @@ import org.springframework.test.context.junit.jupiter.SpringExtension import java.sql.SQLIntegrityConstraintViolationException import java.time.Instant - @ExtendWith(SpringExtension::class) @DataJpaTest @EnableJpaAuditing @@ -63,7 +62,7 @@ class UserRepositoryTest { val user = User( null, USER_ID, null, FCM_TOKEN_1, project, - Instant.now(), null, TIMEZONE, "en", null + Instant.now(), null, TIMEZONE, "en", null, ) this.userId = entityManager.persistAndGetId(user) as Long @@ -74,7 +73,7 @@ class UserRepositoryTest { fun `when insert with transient project then throw exception`() { val user1 = User( null, USER_ID, null, FCM_TOKEN_1, Project(), - Instant.now(), null, TIMEZONE, "en", null + Instant.now(), null, TIMEZONE, "en", null, ) val ex = assertThrows { @@ -99,7 +98,7 @@ class UserRepositoryTest { fun `when find by subject id and project id then return user`() { assertEquals( userRepository.findBySubjectIdAndProjectId(USER_ID, this.projectId!!), - entityManager.find(User::class.java, this.userId) + entityManager.find(User::class.java, this.userId), ) } @@ -112,7 +111,7 @@ class UserRepositoryTest { fun `when insert with existing FCM token then throw exception`() { val user1 = User( null, "$USER_ID-2", null, FCM_TOKEN_1, this.project, - Instant.now(), null, TIMEZONE, "en", null + Instant.now(), null, TIMEZONE, "en", null, ) val ex = assertThrows { @@ -123,4 +122,3 @@ class UserRepositoryTest { assertEquals(SQLIntegrityConstraintViolationException::class.java, ex.cause!!::class.java) } } - diff --git a/src/test/java/org/radarbase/appserver/service/FcmNotificationServiceTest.kt b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/service/FcmNotificationServiceTest.kt similarity index 97% rename from src/test/java/org/radarbase/appserver/service/FcmNotificationServiceTest.kt rename to appserver-legacy/src/test/kotlin/org/radarbase/appserver/service/FcmNotificationServiceTest.kt index ae14f0fd2..619db8c44 100644 --- a/src/test/java/org/radarbase/appserver/service/FcmNotificationServiceTest.kt +++ b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/service/FcmNotificationServiceTest.kt @@ -94,7 +94,7 @@ class FcmNotificationServiceTest { project = projectNew, fcmToken = FCM_TOKEN_1, enrolmentDate = Instant.now(), - usermetrics = userMetrics + usermetrics = userMetrics, ) Mockito.`when`(userRepository.save(Mockito.any())).thenReturn(userNew) @@ -126,12 +126,11 @@ class FcmNotificationServiceTest { NOTIFICATION_TITLE_3, NOTIFICATION_BODY, null, - 86400 - ) + 86400, + ), ) .thenReturn(false) - val notification4 = Notification.NotificationBuilder() .body(NOTIFICATION_BODY) .title(NOTIFICATION_TITLE_4) @@ -161,8 +160,8 @@ class FcmNotificationServiceTest { NOTIFICATION_TITLE_4, NOTIFICATION_BODY, null, - 86400 - ) + 86400, + ), ) .thenReturn(false) @@ -200,7 +199,7 @@ class FcmNotificationServiceTest { enrolmentDate = Instant.now(), usermetrics = UserMetrics(Instant.now(), Instant.now()), timezone = TIMEZONE, - language = "en" + language = "en", ) Mockito.`when`(userRepository.findBySubjectId(user.subjectId)).thenReturn(user) @@ -227,7 +226,6 @@ class FcmNotificationServiceTest { notification1.createdAt = Date() notification1.updatedAt = Date() - val notification2 = Notification.NotificationBuilder() .user(user) .body(NOTIFICATION_BODY) @@ -268,8 +266,8 @@ class FcmNotificationServiceTest { NOTIFICATION_TITLE, NOTIFICATION_BODY, null, - 86400 - ) + 86400, + ), ) .thenReturn(true) @@ -282,8 +280,8 @@ class FcmNotificationServiceTest { NOTIFICATION_TITLE_2, NOTIFICATION_BODY, null, - 86400 - ) + 86400, + ), ) .thenReturn(true) @@ -291,13 +289,12 @@ class FcmNotificationServiceTest { notificationRepository .existsByIdAndUserId( 1L, - 1L - ) + 1L, + ), ) .thenReturn(true) Mockito.`when`(notificationRepository.findByIdAndUserId(1L, 1L)).thenReturn(notification1) - } @Test @@ -361,8 +358,9 @@ class FcmNotificationServiceTest { // A random notification should not exist assertFalse( notificationService.checkIfNotificationExists( - FcmNotificationDto().withScheduledTime(Instant.now()), USER_ID - ) + FcmNotificationDto().withScheduledTime(Instant.now()), + USER_ID, + ), ) } @@ -406,7 +404,7 @@ class FcmNotificationServiceTest { } assertTrue( - ex.message!!.contains("The supplied Subject ID is invalid. No user found. Please Create a User First.") + ex.message!!.contains("The supplied Subject ID is invalid. No user found. Please Create a User First."), ) } @@ -439,7 +437,6 @@ class FcmNotificationServiceTest { .withTtlSeconds(86400) .withDelivered(false) - val notificationDto2 = FcmNotificationDto() .withBody(NOTIFICATION_BODY + "2") .withTitle(NOTIFICATION_TITLE_4 + "3") @@ -449,11 +446,10 @@ class FcmNotificationServiceTest { .withTtlSeconds(86400) .withDelivered(false) - notificationService.addNotifications( FcmNotifications().withNotifications(listOf(notificationDto1, notificationDto2)), USER_ID, - PROJECT_ID + PROJECT_ID, ) val savedNotifications = notificationService.getNotificationsBySubjectId(USER_ID) @@ -539,7 +535,7 @@ class FcmNotificationServiceTest { projectRepository, schedulerService, notificationConverter, - eventPublisher + eventPublisher, ) } } diff --git a/src/test/java/org/radarbase/appserver/service/ProjectServiceTest.kt b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/service/ProjectServiceTest.kt similarity index 99% rename from src/test/java/org/radarbase/appserver/service/ProjectServiceTest.kt rename to appserver-legacy/src/test/kotlin/org/radarbase/appserver/service/ProjectServiceTest.kt index e716be257..605b46501 100644 --- a/src/test/java/org/radarbase/appserver/service/ProjectServiceTest.kt +++ b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/service/ProjectServiceTest.kt @@ -83,7 +83,6 @@ class ProjectServiceTest { fun getAllProjects() { val projectDtos: ProjectDtos = projectService.getAllProjects() - println("Checkpoint project: $projectDtos") assertEquals(PROJECT_ID, projectDtos.projects[0].projectId) assertEquals(1L, projectDtos.projects[0].id) diff --git a/src/test/java/org/radarbase/appserver/service/UserServiceTest.kt b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/service/UserServiceTest.kt similarity index 99% rename from src/test/java/org/radarbase/appserver/service/UserServiceTest.kt rename to appserver-legacy/src/test/kotlin/org/radarbase/appserver/service/UserServiceTest.kt index 157e1d606..4cfeca120 100644 --- a/src/test/java/org/radarbase/appserver/service/UserServiceTest.kt +++ b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/service/UserServiceTest.kt @@ -63,7 +63,6 @@ class UserServiceTest { @BeforeEach fun setUp() { - val project = Project().apply { projectId = PROJECT_ID id = 1L @@ -156,7 +155,6 @@ class UserServiceTest { timezone = TIMEZONE } - val userDto = userService.saveUserInProject(userDtoNew) assertEquals("$USER_ID-2", userDto.subjectId) diff --git a/src/test/java/org/radarbase/appserver/service/questionnaire/protocol/DefaultProtocolGeneratorTest.kt b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/service/questionnaire/protocol/DefaultProtocolGeneratorTest.kt similarity index 99% rename from src/test/java/org/radarbase/appserver/service/questionnaire/protocol/DefaultProtocolGeneratorTest.kt rename to appserver-legacy/src/test/kotlin/org/radarbase/appserver/service/questionnaire/protocol/DefaultProtocolGeneratorTest.kt index 23851e9b2..d4083d1e4 100644 --- a/src/test/java/org/radarbase/appserver/service/questionnaire/protocol/DefaultProtocolGeneratorTest.kt +++ b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/service/questionnaire/protocol/DefaultProtocolGeneratorTest.kt @@ -40,7 +40,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension @ExtendWith(SpringExtension::class) @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.NONE, - classes = [GithubProtocolFetcherStrategy::class, DefaultProtocolGenerator::class] + classes = [GithubProtocolFetcherStrategy::class, DefaultProtocolGenerator::class], ) class DefaultProtocolGeneratorTest { @@ -106,4 +106,3 @@ class DefaultProtocolGeneratorTest { assertEquals("PHQ8", protocol?.protocols?.first()?.name) } } - diff --git a/src/test/java/org/radarbase/appserver/service/scheduler/NotificationSchedulerServiceTest.kt b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/service/scheduler/NotificationSchedulerServiceTest.kt similarity index 92% rename from src/test/java/org/radarbase/appserver/service/scheduler/NotificationSchedulerServiceTest.kt rename to appserver-legacy/src/test/kotlin/org/radarbase/appserver/service/scheduler/NotificationSchedulerServiceTest.kt index 954aa5423..cd7ffb2ab 100644 --- a/src/test/java/org/radarbase/appserver/service/scheduler/NotificationSchedulerServiceTest.kt +++ b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/service/scheduler/NotificationSchedulerServiceTest.kt @@ -21,12 +21,21 @@ package org.radarbase.appserver.service.scheduler -import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Assertions.fail import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.mockito.Mockito.mock -import org.quartz.* +import org.quartz.JobExecutionContext +import org.quartz.JobExecutionException +import org.quartz.JobKey +import org.quartz.JobListener +import org.quartz.Scheduler +import org.quartz.TriggerKey import org.radarbase.appserver.config.SchedulerConfig import org.radarbase.appserver.entity.Notification import org.radarbase.appserver.entity.Project @@ -54,8 +63,8 @@ import java.time.temporal.ChronoUnit DataSourceAutoConfiguration::class, QuartzAutoConfiguration::class, SchedulerConfig::class, - NotificationSchedulerServiceTest.SchedulerServiceTestConfig::class - ] + NotificationSchedulerServiceTest.SchedulerServiceTestConfig::class, + ], ) class NotificationSchedulerServiceTest { @@ -121,7 +130,7 @@ class NotificationSchedulerServiceTest { assertTrue(scheduler.checkExists(JobKey(JOB_DETAIL_ID))) assertEquals( updatedNotification.scheduledTime?.truncatedTo(ChronoUnit.MILLIS), - scheduler.getTrigger(TriggerKey("message-trigger-test-subject-1")).startTime.toInstant() + scheduler.getTrigger(TriggerKey("message-trigger-test-subject-1")).startTime.toInstant(), ) } @@ -165,6 +174,3 @@ class NotificationSchedulerServiceTest { } } } - - - diff --git a/src/test/java/org/radarbase/appserver/service/scheduler/quartz/MessageJobTest.kt b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/service/scheduler/quartz/MessageJobTest.kt similarity index 98% rename from src/test/java/org/radarbase/appserver/service/scheduler/quartz/MessageJobTest.kt rename to appserver-legacy/src/test/kotlin/org/radarbase/appserver/service/scheduler/quartz/MessageJobTest.kt index 3481bdd13..793323046 100644 --- a/src/test/java/org/radarbase/appserver/service/scheduler/quartz/MessageJobTest.kt +++ b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/service/scheduler/quartz/MessageJobTest.kt @@ -21,6 +21,8 @@ package org.radarbase.appserver.service.scheduler.quartz +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -31,7 +33,6 @@ import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.`when` -import org.junit.jupiter.api.Assertions.* import org.quartz.JobDataMap import org.quartz.JobExecutionException import org.quartz.impl.JobDetailImpl diff --git a/src/test/java/org/radarbase/appserver/service/storage/S3StorageServiceTest.kt b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/service/storage/S3StorageServiceTest.kt similarity index 94% rename from src/test/java/org/radarbase/appserver/service/storage/S3StorageServiceTest.kt rename to appserver-legacy/src/test/kotlin/org/radarbase/appserver/service/storage/S3StorageServiceTest.kt index 3e40399e1..598b70df9 100644 --- a/src/test/java/org/radarbase/appserver/service/storage/S3StorageServiceTest.kt +++ b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/service/storage/S3StorageServiceTest.kt @@ -20,7 +20,7 @@ package org.radarbase.appserver.service.storage import io.minio.MinioClient import io.minio.PutObjectArgs -import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows @@ -29,6 +29,7 @@ import org.mockito.ArgumentMatchers.any import org.mockito.BDDMockito.given import org.mockito.Mock import org.mockito.Mockito.mock +import org.mockito.Mockito.verify import org.radarbase.appserver.config.S3StorageProperties import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.context.properties.EnableConfigurationProperties @@ -37,7 +38,6 @@ import org.springframework.boot.test.mock.mockito.MockBean import org.springframework.mock.web.MockMultipartFile import org.springframework.test.context.junit.jupiter.SpringExtension import org.springframework.web.multipart.MultipartFile -import org.mockito.Mockito.verify @ExtendWith(SpringExtension::class) @SpringBootTest( @@ -46,8 +46,8 @@ import org.mockito.Mockito.verify "radar.file-upload.enabled=true", "radar.storage.type=s3", "radar.storage.s3.bucket-name=my-bucket", - "radar.storage.s3.path.prefix=my-sub-path" - ] + "radar.storage.s3.path.prefix=my-sub-path", + ], ) @EnableConfigurationProperties(S3StorageProperties::class) class S3StorageServiceTest { @@ -62,7 +62,10 @@ class S3StorageServiceTest { private lateinit var minioClient: MinioClient private val multipartFile = MockMultipartFile( - "file", "my-file.txt", "text/plain", "my-file-content".toByteArray() + "file", + "my-file.txt", + "text/plain", + "my-file-content".toByteArray(), ) companion object { diff --git a/src/test/java/org/radarbase/appserver/service/storage/StoragePathTest.kt b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/service/storage/StoragePathTest.kt similarity index 100% rename from src/test/java/org/radarbase/appserver/service/storage/StoragePathTest.kt rename to appserver-legacy/src/test/kotlin/org/radarbase/appserver/service/storage/StoragePathTest.kt diff --git a/src/test/java/org/radarbase/appserver/service/transmitter/EmailNotificationTransmitterTest.kt b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/service/transmitter/EmailNotificationTransmitterTest.kt similarity index 80% rename from src/test/java/org/radarbase/appserver/service/transmitter/EmailNotificationTransmitterTest.kt rename to appserver-legacy/src/test/kotlin/org/radarbase/appserver/service/transmitter/EmailNotificationTransmitterTest.kt index 20d314708..2ff43e10e 100644 --- a/src/test/java/org/radarbase/appserver/service/transmitter/EmailNotificationTransmitterTest.kt +++ b/appserver-legacy/src/test/kotlin/org/radarbase/appserver/service/transmitter/EmailNotificationTransmitterTest.kt @@ -4,7 +4,12 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith -import org.mockito.Mockito.* +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.doThrow +import org.mockito.Mockito.mock +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.radarbase.appserver.entity.Notification import org.radarbase.appserver.entity.User import org.radarbase.appserver.exception.EmailMessageTransmitException @@ -19,7 +24,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension @ExtendWith(SpringExtension::class) @SpringBootTest( classes = [EmailNotificationTransmitter::class], - properties = ["radar.notification.email.enabled=true"] + properties = ["radar.notification.email.enabled=true"], ) class EmailNotificationTransmitterTest { @@ -34,7 +39,7 @@ class EmailNotificationTransmitterTest { val validNotification = buildNotification() assertDoesNotThrow("Valid Notification should not throw an exception") { emailNotificationTransmitter.send( - validNotification + validNotification, ) } verify(javaMailSender, times(1)).send(any()) @@ -44,21 +49,17 @@ class EmailNotificationTransmitterTest { fun testExceptionNotThrownForMissingEmail() { val invalidNotification = buildNotification() `when`(invalidNotification.user?.emailAddress).thenReturn(null) - assertDoesNotThrow("Notification with User w/o an email address should not throw an exception") - { emailNotificationTransmitter.send(invalidNotification) } + assertDoesNotThrow("Notification with User w/o an email address should not throw an exception") { emailNotificationTransmitter.send(invalidNotification) } `when`(invalidNotification.user?.emailAddress).thenReturn("") - assertDoesNotThrow("Notification with User w/o an email address should not throw an exception") - { emailNotificationTransmitter.send(invalidNotification) } + assertDoesNotThrow("Notification with User w/o an email address should not throw an exception") { emailNotificationTransmitter.send(invalidNotification) } } @Test fun testExceptionThrownWithEmailFailure() { doThrow(mock(MailException::class.java)).`when`(javaMailSender).send(any()) val validNotification = buildNotification() - assertThrows("Problems during sending of email notifications should throw an exception") - { emailNotificationTransmitter.send(validNotification) } - + assertThrows("Problems during sending of email notifications should throw an exception") { emailNotificationTransmitter.send(validNotification) } } private fun buildNotification(): Notification { diff --git a/src/test/resources/application.properties b/appserver-legacy/src/test/resources/application.properties similarity index 100% rename from src/test/resources/application.properties rename to appserver-legacy/src/test/resources/application.properties diff --git a/build.gradle b/build.gradle deleted file mode 100644 index a388bd8ea..000000000 --- a/build.gradle +++ /dev/null @@ -1,304 +0,0 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.dsl.KotlinVersion -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -plugins { - id 'pmd' - id 'java' - id 'eclipse' - id 'idea' - id 'scala' - id 'checkstyle' - id 'io.gatling.gradle' version '3.9.2.1' - id 'com.github.johnrengelman.shadow' version '8.1.0' - id 'org.springframework.boot' version '3.3.3' - id 'org.openjfx.javafxplugin' version '0.0.13' - id 'io.spring.dependency-management' version '1.1.6' - id 'com.github.ben-manes.versions' version "0.46.0" - id 'org.jetbrains.kotlin.jvm' version '1.9.25' - id "org.jetbrains.kotlin.kapt" version "1.9.25" - id "org.jetbrains.kotlin.plugin.allopen" version "1.9.25" - id "org.jetbrains.kotlin.plugin.spring" version "1.9.25" - id "org.jetbrains.kotlin.plugin.jpa" version "1.9.25" - id "org.jetbrains.kotlin.plugin.noarg" version "1.9.25" - id 'io.sentry.jvm.gradle' version '4.11.0' -} - -group = 'org.radarbase' -version = '2.4.3' - -java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(17)) - } -} - -kotlin { - jvmToolchain(17) -} - -kapt { - keepJavacAnnotationProcessors = true -} - -idea { - module { - downloadJavadoc = true - downloadSources = true - } -} - -repositories { - mavenCentral() - maven { url = "https://oss.sonatype.org/content/repositories/snapshots" } -} - -springBoot { - mainClass.set('org.radarbase.appserver.AppserverApplicationKt') -} - -bootJar { - mainClass = 'org.radarbase.appserver.AppserverApplicationKt' - duplicatesStrategy = DuplicatesStrategy.INCLUDE -} - -jar { - duplicatesStrategy = DuplicatesStrategy.INCLUDE -} - -ext { - springBootVersion = '3.3.3' - springVersion = '6.0.6' - springOauth2Version = "2.5.2.RELEASE" - springOauth2AutoconfigureVersion = "2.6.8" - springDocVersion = '2.2.0' - lombokVersion = '1.18.26' - junit5Version = '5.9.2' - radarSpringAuthVersion = '1.2.1' - springSecurityVersion = '6.0.5' - hibernateValidatorVersion = '8.0.0.Final' - minioVersion = '8.5.10' - kotlinVersion = '1.9.25' - jacksonKotlinVersion = '2.15.4' - dateTimeVersion = '0.6.1' - ktorVersion = '2.3.0' - coroutinesVersion = '1.10.1' - mockitoKotlinVersion = '3.2.0' -} - -sourceSets { - main { - kotlin { - srcDirs += 'src/main/java' - } - } - integrationTest { - java { - compileClasspath += main.output + test.output + test.compileClasspath - runtimeClasspath += main.output + test.output + test.runtimeClasspath - srcDir file('src/integrationTest/java') - } - resources.srcDir file('src/integrationTest/resources') - } -} - -dependencies { - implementation('org.springframework.boot:spring-boot-starter-data-jpa') - implementation('org.springframework.boot:spring-boot-starter-web') - implementation('org.springframework.boot:spring-boot-starter-quartz') - implementation('org.springframework.boot:spring-boot-starter-security') - implementation('org.springframework.boot:spring-boot-starter-actuator') - implementation('org.springframework.boot:spring-boot-starter-mail') - implementation group: "org.springframework.security", name: "spring-security-config", version: springSecurityVersion - implementation('org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:' + springOauth2AutoconfigureVersion) - implementation('org.springframework.security.oauth:spring-security-oauth2:' + springOauth2Version) - runtimeOnly("org.hibernate.validator:hibernate-validator:$hibernateValidatorVersion") - implementation("io.minio:minio:$minioVersion") { - exclude group: 'org.jetbrains.kotlin' - } - - // Open API spec - implementation(group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-ui', version: springDocVersion) - - //runtimeOnly('org.springframework.boot:spring-boot-devtools') - runtimeOnly('org.hsqldb:hsqldb') - runtimeOnly('org.liquibase:liquibase-core:4.20.0') - runtimeOnly(group: 'org.postgresql', name: 'postgresql', version: '42.5.5') - - - annotationProcessor group: 'org.projectlombok', name: 'lombok', version: lombokVersion - implementation group: 'org.projectlombok', name: 'lombok', version: lombokVersion - - kapt("org.springframework:spring-context-indexer:$springVersion") - annotationProcessor "org.springframework:spring-context-indexer:$springVersion" - - // FCM Admin SDK - implementation('com.google.firebase:firebase-admin:9.3.0') { - // Possibly remove these constraints when a newer version of firebase-adkon is available. - constraints { - implementation('com.google.protobuf:protobuf-java:3.25.5') { - because 'Provided version of protobuf has security vulnerabilities' - } - implementation('com.google.protobuf:protobuf-java-util:3.25.5') { - because 'Provided version of protobuf has security vulnerabilities' - } - } - } - - // AOP - runtimeOnly group: 'org.springframework', name: 'spring-aop', version: springVersion - implementation(group: 'org.radarbase', name: 'radar-spring-auth', version: radarSpringAuthVersion) - - //Kotlin - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" - implementation "com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonKotlinVersion" - implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.25") - kapt("org.springframework.boot:spring-boot-configuration-processor") - implementation("io.ktor:ktor-client-core:$ktorVersion") - implementation "io.ktor:ktor-client-cio:$ktorVersion" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" - testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion" - - testImplementation group: 'io.gatling.highcharts', name: 'gatling-charts-highcharts', version: '3.9.2' - - implementation('org.liquibase.ext:liquibase-hibernate6:4.20.0') - - testImplementation('org.springframework.boot:spring-boot-starter-test') { - exclude group: 'org.junit', module: 'junit' - } - - testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter', version: junit5Version - testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: junit5Version - testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: junit5Version - testImplementation group: 'org.junit.platform', name: 'junit-platform-commons', version: '1.8.2' - testImplementation group: 'org.junit.platform', name: 'junit-platform-launcher', version: '1.8.2' - testImplementation group: 'org.junit.platform', name: 'junit-platform-engine', version: '1.8.2' - - gatlingImplementation('com.fasterxml.jackson.datatype:jackson-datatype-jsr310') -} - -noArg { - annotation("org.radarbase.appserver.util.GenerateZeroArgs") -} - -allOpen { - annotation("org.radarbase.appserver.util.OpenClass") - annotation("jakarta.persistence.MappedSuperclass") - annotation("jakarta.persistence.Entity") - annotation("jakarta.persistence.Embeddable") -} - -javafx { - version = "19" - modules = [ 'javafx.controls' ] -} - -checkstyle { - configDirectory.set(file("config/checkstyle")) - toolVersion = "10.8.0" - showViolations = false - ignoreFailures = true - sourceSets = [it.sourceSets.main] -} - -javadoc { - options.addBooleanOption('html5', true) - destinationDir = new File("${project.rootDir}/src/main/resources/static/java-docs".toString()) -} - -wrapper { - gradleVersion '8.5' -} - -test { - useJUnitPlatform() { - excludeEngines 'junit-vintage' - } -} - -tasks.withType(KotlinCompile).configureEach { - compilerOptions { - jvmTarget = JvmTarget.JVM_17 - apiVersion = KotlinVersion.KOTLIN_1_9 - languageVersion = KotlinVersion.KOTLIN_1_9 - } -} - -tasks.register('unpack', Copy) { - duplicatesStrategy = DuplicatesStrategy.INCLUDE - dependsOn bootJar - from(zipTree(tasks.bootJar.outputs.files.singleFile)) - into("build/dependency") -} - -tasks.register('loadTest', JavaExec) { - dependsOn testClasses - description = "Load Test With Gatling" - group = "Load Test" - classpath = sourceSets.main.runtimeClasspath - jvmArgs = [ - "-Dgatling.core.directory.binaries=${sourceSets.main.output.classesDirs.toString()}" - ] - mainClass = "io.gatling.app.Gatling" - args = [ - "--simulation", "org.radarbase.appserver.ApiGatlingSimulationTest", - "--results-folder", "${buildDir}/gatling-results", - "--binaries-folder", sourceSets.main.output.classesDirs.toString(), - "--bodies-folder", sourceSets.main.resources.srcDirs.toList().first().toString() + "/gatling/bodies", - ] -} - -tasks.register('integrationTest', Test) { - testClassesDirs = sourceSets.integrationTest.output.classesDirs - classpath = sourceSets.integrationTest.runtimeClasspath - useJUnitPlatform() { - excludeEngines 'junit-vintage' - } - environment "RADAR_IS_CONFIG_LOCATION", "src/integrationTest/resources/radar-is.yml" - - shouldRunAfter test -} - -tasks.register('downloadDependencies') { - description = "Pre-downloads dependencies" - configurations.compileClasspath.files - configurations.runtimeClasspath.files -} - -tasks.register('copyDependencies', Copy) { - from configurations.runtimeClasspath.files - into "$buildDir/third-party/" -} - -rootProject.tasks.named("processIntegrationTestResources") { - duplicatesStrategy = 'include' -} - -check.dependsOn integrationTest - -test { - testLogging { - events "failed" - exceptionFormat "full" - - error { - exceptionFormat "full" - } - } -} - -def isNonStable = { String version -> - def stableKeyword = ["RELEASE", "FINAL", "GA"].any { version.toUpperCase().contains(it) } - def regex = /^[0-9,.v-]+(-r)?$/ - return !stableKeyword && !(version ==~ regex) -} - -tasks.named("dependencyUpdates").configure { - rejectVersionIf { - isNonStable(it.candidate.version) - } -} - -pmd { - sourceSets = [sourceSets.main] -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 000000000..4d107834a --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + idea + id("org.radarbase.appserver-conventions") + id("org.radarbase.radar-dependency-management") version Versions.radarCommonsVersion apply false + id("org.radarbase.radar-kotlin") version Versions.radarCommonsVersion apply false + id("org.jetbrains.kotlin.plugin.spring") version Versions.kotlinVersion + id("org.jetbrains.kotlin.plugin.jpa") version Versions.kotlinVersion + kotlin("plugin.allopen") version Versions.kotlinVersion + kotlin("plugin.noarg") version Versions.kotlinVersion +// id("com.avast.gradle.docker-compose") version Versions.dockerCompose apply false +} + +appserverProject { + version.set(Versions.project) + gradleWrapper.set(Versions.wrapper) +} + +idea { + module { + isDownloadJavadoc = true + isDownloadSources = true + } +} + diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 000000000..eb6563cf2 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,38 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + kotlin("jvm") version "1.9.10" + `kotlin-dsl` + `java-gradle-plugin` +} + +repositories { + mavenCentral() +} + +java.toolchain.languageVersion.set(JavaLanguageVersion.of(17)) + +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9) + languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9) + } +} + +gradlePlugin { + plugins { + create("appserverConvention") { + id = "org.radarbase.appserver-conventions" + implementationClass = "org.radarbase.appserver.convention.AppserverConventionPlugin" + displayName = "RADAR-AppServer conventions" + description = "Common conventions for RADAR-AppServer " + } + create("customSourceSetConvention") { + id = "org.radarbase.appserver.int-test-source-sets" + implementationClass = "org.radarbase.appserver.convention.IntegrationTestSourceSetPlugin" + displayName = "RADAR-AppServer custom source sets" + description = "Custom source sets for RADAR-AppServer" + } + } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt new file mode 100644 index 000000000..1477f02a2 --- /dev/null +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -0,0 +1,34 @@ +@Suppress("ConstPropertyName") +object Versions { + const val gatlingVersion = "3.9.2.1" + const val shadowVersion = "8.1.1" + const val springBootVersion = "3.3.3" + const val springDependencyManagementVersion = "1.1.6" + const val benMenesVersion = "0.46.0" + const val kotlinVersion = "1.9.25" + const val sentryVersion = "4.11.0" + + const val springSecurityVersion = "6.0.5" + const val springOauth2AutoconfigureVersion = "2.6.8" + const val springOauth2Version = "2.5.2.RELEASE" + const val hibernateValidatorVersion = "8.0.0.Final" + const val springDocVersion = "2.2.0" + const val lombokVersion = "1.18.26" + const val springVersion = "6.0.6" + const val radarSpringAuthVersion = "1.2.1" + const val radarJerseyVersion = "0.12.2" + const val jacksonKotlinVersion = "2.15.4" + const val ktorVersion = "2.3.13" + const val coroutinesVersion = "1.10.1" + const val mockitoKotlinVersion = "3.2.0" + const val minioVersion = "8.5.10" + const val junit5Version = "5.9.2" + const val log4j2 = "2.23.1" + const val radarCommonsVersion = "1.2.2" + const val h2Version = "2.2.224" + const val postgresqlVersion = "42.7.5" + + const val project = "2.4.3" + const val wrapper = "8.5" + const val java = 17 +} diff --git a/buildSrc/src/main/kotlin/org/radarbase/appserver/convention/AppserverConventionPlugin.kt b/buildSrc/src/main/kotlin/org/radarbase/appserver/convention/AppserverConventionPlugin.kt new file mode 100644 index 000000000..290eb7a06 --- /dev/null +++ b/buildSrc/src/main/kotlin/org/radarbase/appserver/convention/AppserverConventionPlugin.kt @@ -0,0 +1,41 @@ +package org.radarbase.appserver.convention + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.provider.Property +import org.gradle.api.tasks.wrapper.Wrapper +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.named + +interface AppserverProjectExtension { + val group: Property + val version: Property + val gradleWrapper: Property +} + +@Suppress("unused") +fun Project.appserverProject(config: AppserverProjectExtension.() -> Unit) { + configure(config) +} + +@Suppress("unused") +class AppserverConventionPlugin : Plugin { + override fun apply(project: Project) = project.run { + val extension = extensions.create("appserverProject", AppserverProjectExtension::class.java).apply { + group.convention("org.radarbase") + } + + allprojects { + afterEvaluate { + version = extension.version.get() + group = extension.group.get() + } + } + + afterEvaluate { + tasks.named("wrapper") { + gradleVersion = extension.gradleWrapper.get() + } + } + } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/org/radarbase/appserver/convention/IntegrationTestSourceSetPlugin.kt b/buildSrc/src/main/kotlin/org/radarbase/appserver/convention/IntegrationTestSourceSetPlugin.kt new file mode 100644 index 000000000..48f2484ab --- /dev/null +++ b/buildSrc/src/main/kotlin/org/radarbase/appserver/convention/IntegrationTestSourceSetPlugin.kt @@ -0,0 +1,73 @@ +package org.radarbase.appserver.convention + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.file.DuplicatesStrategy +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Copy +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.testing.Test +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.get +import org.gradle.kotlin.dsl.getByType +import org.gradle.kotlin.dsl.named +import org.gradle.kotlin.dsl.register +import java.util.Locale + +interface IntegrationTestExtension { + val sourceSetName: Property + val duplicatesStrategy: Property + val hookIntoCheck: Property +} + +fun Project.integrationTestConfig(config: IntegrationTestExtension.() -> Unit) = config.run { + configure(config) +} + +class IntegrationTestSourceSetPlugin : Plugin { + override fun apply(target: Project) = target.run { + val extension = extensions.create("integrationTestConfig").apply { + sourceSetName.convention("integrationTest") + hookIntoCheck.convention(true) + } + + afterEvaluate { + val ssName = extension.sourceSetName.get() + val sourceSets = extensions.getByType() + + sourceSets.create(ssName) { + compileClasspath += sourceSets["main"].output + runtimeClasspath += sourceSets["main"].output + } + + val integrationTestImplementation = configurations.getByName("${ssName}Implementation") + integrationTestImplementation.extendsFrom(configurations.getByName("testImplementation")) + + configurations.getByName("${ssName}RuntimeOnly") + .extendsFrom(configurations.getByName("testRuntimeOnly")) + + val integrationTestTask = tasks.register(ssName) { + description = "Runs ${ssName} tests." + group = "verification" + + testClassesDirs = sourceSets[ssName].output.classesDirs + classpath = sourceSets[ssName].runtimeClasspath + shouldRunAfter("test") + + outputs.upToDateWhen { false } + useJUnitPlatform { excludeEngines("junit-vintage") } + testLogging { events("passed") } + } + + if (extension.hookIntoCheck.get()) { + tasks.named("check") { dependsOn(integrationTestTask) } + } + + tasks.named("process${ssName.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }}Resources") { + duplicatesStrategy = extension.duplicatesStrategy.get() + } + } + + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 62ca6a700..000000000 --- a/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'appserver' diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 000000000..8d0d3d86f --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,21 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + +@Suppress("UnstableApiUsage") +dependencyResolutionManagement { + repositories { + mavenCentral() + maven { + url = uri("https://oss.sonatype.org/content/repositories/snapshots") + } + } +} + +rootProject.name = "radar-appserver" + +include("appserver-legacy") +include("appserver-jersey")