Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Structure definition export #157

Merged
merged 6 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ class JobEndpoint(jobRepository: IJobRepository, mappingRepository: IMappingRepo
* */
private def getExecutions(projectId: String, id: String): Route = {
get {
parameterMap { queryParams => // page is supported for now (e.g. page=1)
parameterMap { queryParams => // page and some filter options available. (page, dateBefore, dateAfter, errorStatuses, rowPerPage)
onComplete(executionService.getExecutions(projectId, id, queryParams)) {
case util.Success(response) =>
val headers = List(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import com.typesafe.scalalogging.LazyLogging
import io.tofhir.common.model.SchemaDefinition
import io.tofhir.common.model.{SchemaDefinition, SimpleStructureDefinition}
import io.tofhir.engine.Execution.actorSystem.dispatcher
import io.tofhir.server.endpoint.SchemaDefinitionEndpoint.{SEGMENT_INFER, SEGMENT_REDCAP, SEGMENT_SCHEMAS}
import io.tofhir.server.model.Json4sSupport._
Expand All @@ -15,6 +15,7 @@ import io.tofhir.engine.util.FhirMappingJobFormatter.formats
import io.tofhir.server.common.model.{BadRequest, ResourceNotFound, ToFhirRestCall}
import io.tofhir.server.endpoint.MappingContextEndpoint.ATTACHMENT
import io.tofhir.server.service.mapping.IMappingRepository
import io.onfhir.api.Resource

class SchemaDefinitionEndpoint(schemaRepository: ISchemaRepository, mappingRepository: IMappingRepository) extends LazyLogging {

Expand All @@ -27,7 +28,7 @@ class SchemaDefinitionEndpoint(schemaRepository: ISchemaRepository, mappingRepos
parameterMap { queryParams =>
queryParams.get("url") match {
case Some(url) => getSchemaByUrl(projectId, url)
case None => getAllSchemas(request) ~ createSchema(projectId) // Operations on all schemas
case None => getAllSchemas(request) ~ createSchema(projectId, queryParams.getOrElse("format", SchemaFormats.SIMPLE_STRUCTURE_DEFINITION)) // Operations on all schemas
}
}
} ~ pathPrefix(SEGMENT_INFER) { // infer a schema
Expand All @@ -46,12 +47,28 @@ class SchemaDefinitionEndpoint(schemaRepository: ISchemaRepository, mappingRepos
}
}

private def createSchema(projectId: String): Route = {
/**
* Create a new schema with the given body
* @param projectId Id of the project in which the schemas will be created
* @param format format of the schema in the request, there are two options StructureDefinition and SimpleStructureDefinition
* @return the SchemaDefinition of the created schema
*/
private def createSchema(projectId: String, format: String): Route = {
post { // Create a new schema definition
entity(as[SchemaDefinition]) { schemaDefinition =>
complete {
service.createSchema(projectId, schemaDefinition) map { createdDefinition =>
StatusCodes.Created -> createdDefinition
// If the schema is in the form of StructureDefinition, convert into SimpleStructureDefinition and save
if (format == SchemaFormats.STRUCTURE_DEFINITION) {
entity(as[Resource]) { schemaStructureDefinition =>
complete {
service.createSchemaFromStructureDefinition(projectId, schemaStructureDefinition)
}
}
}
else{
entity(as[SchemaDefinition]) { schemaDefinition =>
complete {
service.createSchema(projectId, schemaDefinition) map { createdDefinition =>
StatusCodes.Created -> createdDefinition
}
}
}
}
Expand All @@ -60,11 +77,27 @@ class SchemaDefinitionEndpoint(schemaRepository: ISchemaRepository, mappingRepos

private def getSchema(projectId: String, id: String): Route = {
get {
complete {
service.getSchema(projectId, id) map {
case Some(schemaDefinition) => StatusCodes.OK -> schemaDefinition
case None => StatusCodes.NotFound -> {
throw ResourceNotFound("Schema not found", s"Schema definition with name $id not found")
parameterMap { queryParams =>
complete {
// Requested format of the schema: "StructureDefinition" or "SimpleStructureDefinition"
val format: String = queryParams.getOrElse("format", SchemaFormats.SIMPLE_STRUCTURE_DEFINITION)
// Send structure definition for the user to export
if(format == SchemaFormats.STRUCTURE_DEFINITION){
service.getSchemaAsStructureDefinition(projectId, id) map {
case Some(schemaStructureDefinition) => StatusCodes.OK -> schemaStructureDefinition
case None => {
throw ResourceNotFound("Schema not found", s"Schema definition with name $id not found")
}
}
}
// Send simple structure definition for general use in frontend
else {
service.getSchema(projectId, id) map {
case Some(schemaSimpleStructureDefinition) => StatusCodes.OK -> schemaSimpleStructureDefinition
case None => StatusCodes.NotFound -> {
throw ResourceNotFound("Schema not found", s"Schema definition with name $id not found")
}
}
}
}
}
Expand Down Expand Up @@ -147,3 +180,11 @@ object SchemaDefinitionEndpoint {
val SEGMENT_REDCAP = "redcap"
}

/**
* The schema formats available for POST and GET schema methods
*/
object SchemaFormats{
val STRUCTURE_DEFINITION = "StructureDefinition"
val SIMPLE_STRUCTURE_DEFINITION = "SimpleStructureDefinition"
}

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import akka.stream.scaladsl.Source
import akka.util.ByteString
import io.onfhir.api.Resource
import io.tofhir.engine.util.{CsvUtil, RedCapUtil}
import io.tofhir.server.common.model.{BadRequest, ResourceNotFound}

Expand Down Expand Up @@ -143,4 +144,24 @@ class SchemaDefinitionService(schemaRepository: ISchemaRepository, mappingReposi
Future.sequence(definitions.map(definition => schemaRepository.saveSchema(projectId, definition)))
})
}

/**
* Get structure definition resource of the schema
* @param projectId project containing the schema definition
* @param id id of the requested schema
* @return Structure definition of the schema converted into StructureDefinition Resource
*/
def getSchemaAsStructureDefinition(projectId: String, id: String): Future[Option[Resource]] = {
schemaRepository.getSchemaAsStructureDefinition(projectId, id)
}

/**
* Save the schema by using its StructureDefinition
* @param projectId Identifier of the project in which the schema will be created
* @param structureDefinitionResource schema definition in the form of Structure Definition resource
* @return the SchemaDefinition of the created schema
*/
def createSchemaFromStructureDefinition(projectId: String, structureDefinitionResource: Resource): Future[SchemaDefinition] = {
schemaRepository.saveSchemaByStructureDefinition(projectId, structureDefinitionResource)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.tofhir.server.service.schema

import io.onfhir.api.Resource
import io.tofhir.common.model.SchemaDefinition
import io.tofhir.engine.mapping.IFhirSchemaLoader

Expand Down Expand Up @@ -70,4 +71,23 @@ trait ISchemaRepository extends IFhirSchemaLoader {
*/
def deleteProjectSchemas(projectId: String): Unit

/**
* Retrieve the Structure Definition of the schema identified by its id.
*
* @param projectId Project containing the schema definition
* @param id Identifier of the schema definition
* @return Structure definition of the schema converted into StructureDefinition Resource
*/
def getSchemaAsStructureDefinition(projectId: String, id: String): Future[Option[Resource]]


/**
* Save the schema by using its Structure Definition
*
* @param projectId Project containing the schema definition
* @param structureDefinitionResource The resource of the structure definition
* @return the SchemaDefinition of the created schema
*/
def saveSchemaByStructureDefinition(projectId: String, structureDefinitionResource: Resource): Future[SchemaDefinition]

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import org.json4s.{Extraction, JBool, JObject}

import java.io.{File, FileWriter}
import java.nio.charset.StandardCharsets
import java.util.UUID
import scala.collection.mutable
import scala.concurrent.Future
import scala.io.Source
Expand Down Expand Up @@ -117,31 +118,10 @@ class SchemaFolderRepository(schemaRepositoryFolderPath: String, projectFolderRe
throw BadRequest("Schema definition is not valid.", s"Schema definition cannot be validated: ${schemaDefinition.url}", Some(e))
}

if (schemaDefinitions.contains(projectId) && schemaDefinitions(projectId).contains(schemaDefinition.id)) {
throw AlreadyExists("Schema already exists.", s"A schema definition with id ${schemaDefinition.id} already exists in the schema repository at ${FileUtils.getPath(schemaRepositoryFolderPath).toAbsolutePath.toString}")
}

// Check if the url already exists
val schemaUrls: Map[String, String] = schemaDefinitions.values.flatMap(_.values).map(schema => schema.url -> schema.name).toMap
if (schemaUrls.contains(schemaDefinition.url)) {
throw AlreadyExists("Schema already exists.", s"A schema definition with url ${schemaDefinition.url} already exists. Check the schema '${schemaUrls(schemaDefinition.url)}'")
}

// Write to the repository as a new file
getFileForSchema(projectId, schemaDefinition.id).map(newFile => {
val fw = new FileWriter(newFile)
fw.write(structureDefinitionResource.toPrettyJson)
fw.close()
checkIfSchemaIsUnique(projectId, schemaDefinition.id, schemaDefinition.url)

// Update the project with the schema
projectFolderRepository.addSchema(projectId, schemaDefinition)

// Update the caches with the new schema
baseFhirConfig.profileRestrictions += schemaDefinition.url -> fhirFoundationResourceParser.parseStructureDefinition(structureDefinitionResource, includeElementMetadata = true)
schemaDefinitions.getOrElseUpdate(projectId, mutable.Map.empty).put(schemaDefinition.id, schemaDefinition)

schemaDefinition
})
// Write to the repository as a new file and update caches
writeSchemaAndUpdateCaches(projectId, structureDefinitionResource, schemaDefinition)
}

/**
Expand Down Expand Up @@ -354,5 +334,100 @@ class SchemaFolderRepository(schemaRepositoryFolderPath: String, projectFolderRe
}
baseFhirConfig
}

/**
* Retrieve the Structure Definition of the schema identified by its id.
*
* @param projectId Project containing the schema definition
* @param id Identifier of the schema definition
* @return Structure definition of the schema converted into StructureDefinition Resource
*/
override def getSchemaAsStructureDefinition(projectId: String, id: String): Future[Option[Resource]] = {
getSchema(projectId, id).map {
case Some(schemaStructureDefinition) =>
Some(SchemaUtil.convertToStructureDefinitionResource(schemaStructureDefinition))
case None =>
None
}
}

/**
* Save a schema by using directly its structure definition resource
* Throws:
* InitializationException – If there is a problem in given profile or value set definitions of the schema structure definition
*
* @param projectId Identifier of the project in which the schema will be created
* @param structureDefinitionResource Structure definition resource of the schema
* @return the SchemaDefinition of the created schema
*/
override def saveSchemaByStructureDefinition(projectId: String, structureDefinitionResource: Resource): Future[SchemaDefinition] = {
// Validate the resource
try {
fhirConfigurator.validateGivenInfrastructureResources(baseFhirConfig, api.FHIR_FOUNDATION_RESOURCES.FHIR_STRUCTURE_DEFINITION, Seq(structureDefinitionResource))
} catch {
case e: Exception =>
throw BadRequest("Schema resource is not valid.", s"Schema resource cannot be validated.", Some(e))
}

// Create structureDefinition from the resource
val structureDefinition: ProfileRestrictions = fhirFoundationResourceParser.parseStructureDefinition(structureDefinitionResource, includeElementMetadata = true)
// Generate an Id if id is missing
val schemaId = structureDefinition.id.getOrElse(UUID.randomUUID().toString)

checkIfSchemaIsUnique(projectId, schemaId, structureDefinition.url)

// To use convertToSchemaDefinition, profileRestrictions sequence must include the structure definition. Add it before conversion
baseFhirConfig.profileRestrictions += structureDefinition.url -> structureDefinition
val schemaDefinition = convertToSchemaDefinition(structureDefinition, simpleStructureDefinitionService)
// Remove structure definition from the cache and add it after file writing is done to ensure files and cache are the same
baseFhirConfig.profileRestrictions -= structureDefinition.url

// Write to the repository as a new file and update caches
writeSchemaAndUpdateCaches(projectId, structureDefinitionResource, schemaDefinition)
}

/**
* Checks if ID of the schema is unique in the project and url is unique in the program
* Throws:
* {@link AlreadyExists} with the code 409 if the schema id is not unique in the project or the schema url is not unique in the program
*
* @param projectId Identifier of the project to check schema ID's in it
* @param schemaId Identifier of the schema
* @param schemaUrl Url of the schema
*/
private def checkIfSchemaIsUnique(projectId: String, schemaId: String, schemaUrl: String): Unit = {
if (schemaDefinitions.contains(projectId) && schemaDefinitions(projectId).contains(schemaId)) {
throw AlreadyExists("Schema already exists.", s"A schema definition with id ${schemaId} already exists in the schema repository at ${FileUtils.getPath(schemaRepositoryFolderPath).toAbsolutePath.toString}")
}

val schemaUrls: Map[String, String] = schemaDefinitions.values.flatMap(_.values).map(schema => schema.url -> schema.name).toMap
if (schemaUrls.contains(schemaUrl)) {
throw AlreadyExists("Schema already exists.", s"A schema definition with url ${schemaUrl} already exists. Check the schema '${schemaUrls(schemaUrl)}'")
}
}

/**
* Write the schema file and update the caches accordingly
* @param projectId Id of the project that will include the schema
* @param structureDefinitionResource Schema resource that will be written
* @param schemaDefinition Definition of the schema
* @return
*/
private def writeSchemaAndUpdateCaches(projectId: String, structureDefinitionResource: Resource, schemaDefinition: SchemaDefinition ): Future[SchemaDefinition] = {
getFileForSchema(projectId, schemaDefinition.id).map(newFile => {
val fw = new FileWriter(newFile)
fw.write(structureDefinitionResource.toPrettyJson)
fw.close()

// Update the project with the schema
projectFolderRepository.addSchema(projectId, schemaDefinition)

// Update the caches with the new schema
baseFhirConfig.profileRestrictions += schemaDefinition.url -> fhirFoundationResourceParser.parseStructureDefinition(structureDefinitionResource, includeElementMetadata = true)
schemaDefinitions.getOrElseUpdate(projectId, mutable.Map.empty).put(schemaDefinition.id, schemaDefinition)

schemaDefinition
})
}
}

Loading
Loading