Skip to content

Commit

Permalink
feat(backend): implement endpoint to change data use terms of sequenc…
Browse files Browse the repository at this point in the history
…e entries #763
  • Loading branch information
TobiasKampmann authored and fengelniederhammer committed Jan 24, 2024
1 parent 5ef6d0c commit 651be11
Show file tree
Hide file tree
Showing 25 changed files with 540 additions and 220 deletions.
72 changes: 38 additions & 34 deletions backend/src/main/kotlin/org/loculus/backend/api/DataUseTerms.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.loculus.backend.api

import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.annotation.JsonPropertyOrder
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
Expand All @@ -9,43 +10,60 @@ import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import kotlinx.datetime.Clock
import kotlinx.datetime.DateTimeUnit.Companion.YEAR
import io.swagger.v3.oas.annotations.media.Schema
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.plus
import kotlinx.datetime.toLocalDateTime
import mu.KotlinLogging
import org.loculus.backend.config.logger
import org.loculus.backend.controller.BadRequestException
import org.loculus.backend.utils.Accession

enum class DataUseTermsType {
@JsonProperty("OPEN")
OPEN,

@JsonProperty("RESTRICTED")
RESTRICTED,

;

companion object {
private val stringToEnumMap: Map<String, DataUseTermsType> = entries.associateBy { it.name }

fun fromString(dataUseTermsTypeString: String): DataUseTermsType {
return stringToEnumMap[dataUseTermsTypeString]
?: throw IllegalArgumentException("Unknown DataUseTermsType: $dataUseTermsTypeString")
}
}
}

val logger = KotlinLogging.logger { }
private val logger = KotlinLogging.logger { }

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes(
JsonSubTypes.Type(value = DataUseTerms.Open::class, name = "OPEN"),
JsonSubTypes.Type(value = DataUseTerms.Restricted::class, name = "RESTRICTED"),
)
@JsonPropertyOrder(value = ["type", "restrictedUntil", "changeDateTime"])
@JsonPropertyOrder(value = ["type", "restrictedUntil"])
sealed interface DataUseTerms {
val type: DataUseTermsType

@JsonTypeName("OPEN")
data class Open(private val dummy: String = "") :
DataUseTerms {
@Schema(description = "The sequence entry is open access. No restrictions apply.")
data object Open : DataUseTerms {
@JsonIgnore
override val type = DataUseTermsType.OPEN
}

@JsonTypeName("RESTRICTED")
@Schema(description = "The sequence entry is restricted access.")
data class Restricted(
@JsonSerialize(using = LocalDateSerializer::class)
@Schema(
description = "The date (YYYY-MM-DD) until which the sequence entry is restricted.",
type = "string",
format = "date",
example = "2021-01-01",
)
val restrictedUntil: LocalDate,
) : DataUseTerms {
@JsonIgnore
Expand All @@ -56,12 +74,8 @@ sealed interface DataUseTerms {
fun fromParameters(type: DataUseTermsType, restrictedUntilString: String?): DataUseTerms {
logger.info { "Creating DataUseTerms from parameters: type=$type, restrictedUntil=$restrictedUntilString" }
return when (type) {
DataUseTermsType.OPEN -> Open()
DataUseTermsType.RESTRICTED -> {
val restrictedUntil = parseRestrictedUntil(restrictedUntilString)
validateRestrictedUntil(restrictedUntil)
Restricted(restrictedUntil)
}
DataUseTermsType.OPEN -> Open
DataUseTermsType.RESTRICTED -> Restricted(parseRestrictedUntil(restrictedUntilString))
}
}

Expand All @@ -77,32 +91,22 @@ sealed interface DataUseTerms {
)
}
}

private fun validateRestrictedUntil(restrictedUntil: LocalDate) {
val now = Clock.System.now().toLocalDateTime(TimeZone.UTC).date
val oneYearFromNow = now.plus(1, YEAR)

if (restrictedUntil < now) {
throw BadRequestException(
"The date 'restrictedUntil' must be in the future, up to a maximum of 1 year from now.",
)
}
if (restrictedUntil > oneYearFromNow) {
throw BadRequestException(
"The date 'restrictedUntil' must not exceed 1 year from today.",
)
}
}
}
}

class LocalDateSerializer : StdSerializer<LocalDate>(LocalDate::class.java) {
data class DataUseTermsChangeRequest(
@Schema(description = "A list of accessions of the dataset to set the data use terms for")
val accessions: List<Accession>,
val newDataUseTerms: DataUseTerms,
)

private class LocalDateSerializer : StdSerializer<LocalDate>(LocalDate::class.java) {
override fun serialize(value: LocalDate, gen: JsonGenerator, provider: SerializerProvider) {
gen.writeString(value.toString())
}
}

class LocalDateTimeSerializer : StdSerializer<LocalDateTime>(LocalDateTime::class.java) {
private class LocalDateTimeSerializer : StdSerializer<LocalDateTime>(LocalDateTime::class.java) {
override fun serialize(value: LocalDateTime, gen: JsonGenerator, provider: SerializerProvider) {
gen.writeString(value.toString())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import io.swagger.v3.oas.annotations.media.Schema
import org.loculus.backend.utils.Accession
import org.loculus.backend.utils.Version

data class Accessions(
val accessions: List<Accession>,
)

interface AccessionVersionInterface {
val accession: Accession
val version: Version
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ object BackendSpringProperty {
const val BACKEND_CONFIG_PATH = "backend.config.path"
}

val logger = mu.KotlinLogging.logger {}
private val logger = mu.KotlinLogging.logger {}

@Configuration
@ImportAutoConfiguration(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@ package org.loculus.backend.controller
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import org.loculus.backend.api.DataUseTerms
import org.loculus.backend.api.DataUseTermsChangeRequest
import org.loculus.backend.service.datauseterms.DataUseTermsDatabaseService
import org.loculus.backend.utils.Accession
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
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.ResponseStatus
import org.springframework.web.bind.annotation.RestController

Expand All @@ -20,20 +18,17 @@ class DataUseTermsController(
private val dataUseTermsDatabaseService: DataUseTermsDatabaseService,
) {

@Operation(description = "Set new data use terms. Until now, just testing purposes")
@Operation(
description = "Change the data use terms of the given accessions. Only a change to more open terms is allowed.",
)
@ResponseStatus(HttpStatus.NO_CONTENT)
@PutMapping("/data-use-terms", produces = [MediaType.APPLICATION_JSON_VALUE])
fun setNewDataUseTerms(
@UsernameFromJwt username: String,
@Parameter(
description = "The accession of the dataset to set the data use terms for",
) @RequestParam accession: Accession,
@Parameter(
description = "The new data use terms",
) @RequestBody newDataUseTerms: DataUseTerms,
@Parameter @RequestBody request: DataUseTermsChangeRequest,
) = dataUseTermsDatabaseService.setNewDataUseTerms(
listOf(accession),
username,
DataUseTerms.Open(),
request.accessions,
request.newDataUseTerms,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import jakarta.validation.Valid
import jakarta.validation.constraints.Max
import mu.KotlinLogging
import org.loculus.backend.api.AccessionVersion
import org.loculus.backend.api.Accessions
import org.loculus.backend.api.DataUseTerms
import org.loculus.backend.api.DataUseTermsType
import org.loculus.backend.api.Organism
Expand All @@ -23,7 +24,7 @@ import org.loculus.backend.api.UnprocessedData
import org.loculus.backend.model.ReleasedDataModel
import org.loculus.backend.model.SubmissionParams
import org.loculus.backend.model.SubmitModel
import org.loculus.backend.service.submission.DatabaseService
import org.loculus.backend.service.submission.SubmissionDatabaseService
import org.loculus.backend.utils.Accession
import org.loculus.backend.utils.IteratorStreamer
import org.springframework.http.HttpHeaders
Expand Down Expand Up @@ -55,7 +56,7 @@ private val log = KotlinLogging.logger { }
class SubmissionController(
private val submitModel: SubmitModel,
private val releasedDataModel: ReleasedDataModel,
private val databaseService: DatabaseService,
private val submissionDatabaseService: SubmissionDatabaseService,
private val iteratorStreamer: IteratorStreamer,
) {

Expand Down Expand Up @@ -136,7 +137,8 @@ class SubmissionController(
val headers = HttpHeaders()
headers.contentType = MediaType.parseMediaType(MediaType.APPLICATION_NDJSON_VALUE)

val streamBody = stream { databaseService.streamUnprocessedSubmissions(numberOfSequenceEntries, organism) }
val streamBody =
stream { submissionDatabaseService.streamUnprocessedSubmissions(numberOfSequenceEntries, organism) }
return ResponseEntity(streamBody, headers, HttpStatus.OK)
}

Expand All @@ -159,7 +161,7 @@ class SubmissionController(
@PathVariable @Valid
organism: Organism,
request: HttpServletRequest,
) = databaseService.updateProcessedData(request.inputStream, organism)
) = submissionDatabaseService.updateProcessedData(request.inputStream, organism)

@Operation(description = GET_RELEASED_DATA_DESCRIPTION)
@ResponseStatus(HttpStatus.OK)
Expand Down Expand Up @@ -203,7 +205,7 @@ class SubmissionController(
val headers = HttpHeaders()
headers.contentType = MediaType.parseMediaType(MediaType.APPLICATION_NDJSON_VALUE)

val entries = databaseService.streamDataToEdit(username, groupName, numberOfSequenceEntries, organism)
val entries = submissionDatabaseService.streamDataToEdit(username, groupName, numberOfSequenceEntries, organism)
val streamBody = stream { entries }

return ResponseEntity(streamBody, headers, HttpStatus.OK)
Expand All @@ -217,7 +219,7 @@ class SubmissionController(
@PathVariable accession: Accession,
@PathVariable version: Long,
@UsernameFromJwt username: String,
): SequenceEntryVersionToEdit = databaseService.getSequenceEntryVersionToEdit(
): SequenceEntryVersionToEdit = submissionDatabaseService.getSequenceEntryVersionToEdit(
username,
AccessionVersion(accession, version),
organism,
Expand All @@ -231,7 +233,7 @@ class SubmissionController(
organism: Organism,
@UsernameFromJwt username: String,
@RequestBody accessionVersion: UnprocessedData,
) = databaseService.submitEditedData(username, accessionVersion, organism)
) = submissionDatabaseService.submitEditedData(username, accessionVersion, organism)

@Operation(description = GET_SEQUENCES_OF_USER_DESCRIPTION)
@GetMapping("/get-sequences-of-user", produces = [MediaType.APPLICATION_JSON_VALUE])
Expand All @@ -240,7 +242,7 @@ class SubmissionController(
organism: Organism,
@UsernameFromJwt username: String,
): List<SequenceEntryStatus> {
return databaseService.getActiveSequencesSubmittedBy(username, organism)
return submissionDatabaseService.getActiveSequencesSubmittedBy(username, organism)
}

@Operation(description = APPROVE_PROCESSED_DATA_DESCRIPTION)
Expand All @@ -252,7 +254,7 @@ class SubmissionController(
@UsernameFromJwt username: String,
@RequestBody body: AccessionVersions,
) {
databaseService.approveProcessedData(username, body.accessionVersions, organism)
submissionDatabaseService.approveProcessedData(username, body.accessionVersions, organism)
}

@Operation(description = REVOKE_DESCRIPTION)
Expand All @@ -262,7 +264,7 @@ class SubmissionController(
organism: Organism,
@RequestBody body: Accessions,
@UsernameFromJwt username: String,
): List<SequenceEntryStatus> = databaseService.revoke(body.accessions, username, organism)
): List<SequenceEntryStatus> = submissionDatabaseService.revoke(body.accessions, username, organism)

@Operation(description = CONFIRM_REVOCATION_DESCRIPTION)
@ResponseStatus(HttpStatus.NO_CONTENT)
Expand All @@ -272,7 +274,7 @@ class SubmissionController(
organism: Organism,
@UsernameFromJwt username: String,
@RequestBody body: AccessionVersions,
) = databaseService.confirmRevocation(body.accessionVersions, username, organism)
) = submissionDatabaseService.confirmRevocation(body.accessionVersions, username, organism)

@Operation(description = DELETE_SEQUENCES_DESCRIPTION)
@ResponseStatus(HttpStatus.NO_CONTENT)
Expand All @@ -284,7 +286,7 @@ class SubmissionController(
organism: Organism,
@UsernameFromJwt username: String,
@RequestBody body: AccessionVersions,
) = databaseService.deleteSequenceEntryVersions(body.accessionVersions, username, organism)
) = submissionDatabaseService.deleteSequenceEntryVersions(body.accessionVersions, username, organism)

private fun <T> stream(sequenceProvider: () -> Sequence<T>) = StreamingResponseBody { outputStream ->
try {
Expand All @@ -297,10 +299,6 @@ class SubmissionController(
}
}

data class Accessions(
val accessions: List<Accession>,
)

data class AccessionVersions(
val accessionVersions: List<AccessionVersion>,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import mu.KotlinLogging
import org.loculus.backend.api.Organism
import org.loculus.backend.api.ProcessedData
import org.loculus.backend.api.SiloVersionStatus
import org.loculus.backend.service.submission.DatabaseService
import org.loculus.backend.service.submission.RawProcessedData
import org.loculus.backend.service.submission.SubmissionDatabaseService
import org.loculus.backend.utils.Accession
import org.loculus.backend.utils.Version
import org.springframework.stereotype.Service
Expand All @@ -16,15 +16,15 @@ import org.springframework.transaction.annotation.Transactional
private val log = KotlinLogging.logger { }

@Service
class ReleasedDataModel(private val databaseService: DatabaseService) {
class ReleasedDataModel(private val submissionDatabaseService: SubmissionDatabaseService) {
@Transactional(readOnly = true)
fun getReleasedData(organism: Organism): Sequence<ProcessedData> {
log.info { "fetching released submissions" }

val latestVersions = databaseService.getLatestVersions(organism)
val latestRevocationVersions = databaseService.getLatestRevocationVersions(organism)
val latestVersions = submissionDatabaseService.getLatestVersions(organism)
val latestRevocationVersions = submissionDatabaseService.getLatestRevocationVersions(organism)

return databaseService.streamReleasedSubmissions(organism)
return submissionDatabaseService.streamReleasedSubmissions(organism)
.map { computeAdditionalMetadataFields(it, latestVersions, latestRevocationVersions) }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import org.loculus.backend.api.SubmissionIdMapping
import org.loculus.backend.controller.BadRequestException
import org.loculus.backend.controller.DuplicateKeyException
import org.loculus.backend.controller.UnprocessableEntityException
import org.loculus.backend.service.datauseterms.DataUseTermsPreconditionValidator
import org.loculus.backend.service.groupmanagement.GroupManagementPreconditionValidator
import org.loculus.backend.service.submission.CompressionAlgorithm
import org.loculus.backend.service.submission.UploadDatabaseService
Expand Down Expand Up @@ -70,6 +71,7 @@ enum class UploadType {
class SubmitModel(
private val uploadDatabaseService: UploadDatabaseService,
private val groupManagementPreconditionValidator: GroupManagementPreconditionValidator,
private val dataUseTermsPreconditionValidator: DataUseTermsPreconditionValidator,
) {

companion object AcceptedFileTypes {
Expand Down Expand Up @@ -130,6 +132,7 @@ class SubmitModel(
submissionParams.groupName,
submissionParams.username,
)
dataUseTermsPreconditionValidator.checkThatRestrictedUntilIsAllowed(submissionParams.dataUseTerms)
}

val metadataTempFileToDelete = MaybeFile()
Expand Down
Loading

0 comments on commit 651be11

Please sign in to comment.