Skip to content

Commit

Permalink
Merge pull request #218 from srdc/prefix-management
Browse files Browse the repository at this point in the history
Prefix management
  • Loading branch information
YemreGurses authored Sep 2, 2024
2 parents a9ac332 + 21a7fbf commit eadc839
Show file tree
Hide file tree
Showing 12 changed files with 77 additions and 63 deletions.
20 changes: 8 additions & 12 deletions tofhir-server/src/main/scala/io/tofhir/server/model/Project.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,23 @@ import io.tofhir.common.model.SchemaDefinition
*
* @param id Unique identifier for the project
* @param name Project name
* @param url Project url
* @param description Description of the project
* @param schemaUrlPrefix Prefix (beginning) of the URLs to be used while creating schema definitions within this project.
* @param mappingUrlPrefix Prefix (beginning) of the URLs to be used while creating mapping definitions within this project.
* @param schemas Schemas defined in this project
* @param mappingContexts Identifiers of the mapping contexts defined in this project
* @param mappingJobs Mapping jobs defined in this project
*/
case class Project(id: String = UUID.randomUUID().toString,
name: String,
url: String,
description: Option[String] = None,
schemaUrlPrefix: Option[String] = None,
mappingUrlPrefix: Option[String] = None,
schemas: Seq[SchemaDefinition] = Seq.empty,
mappings: Seq[FhirMapping] = Seq.empty,
mappingContexts: Seq[String] = Seq.empty,
mappingJobs: Seq[FhirMappingJob] = Seq.empty
) {
/**
* Validates the fields of a project.
*
* @throws IllegalArgumentException when the project id is not a valid UUID
* */
def validate(): Unit = {
// throws IllegalArgumentException if the id is not a valid UUID
UUID.fromString(id)
}

/**
* Extracts the project metadata to be written to the metadata file.
Expand All @@ -47,8 +40,9 @@ case class Project(id: String = UUID.randomUUID().toString,
List(
"id" -> JString(this.id),
"name" -> JString(this.name),
"url" -> JString(this.url),
"description" -> JString(this.description.getOrElse("")),
"schemaUrlPrefix" -> JString(this.schemaUrlPrefix.getOrElse("")),
"mappingUrlPrefix" -> JString(this.mappingUrlPrefix.getOrElse("")),
"schemas" -> JArray(
List(
this.schemas.map(_.getMetadata()): _*
Expand Down Expand Up @@ -79,4 +73,6 @@ case class Project(id: String = UUID.randomUUID().toString,
* */
object ProjectEditableFields {
val DESCRIPTION = "description"
val SCHEMA_URL_PREFIX = "schemaUrlPrefix"
val MAPPING_URL_PREFIX = "mappingUrlPrefix"
}
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ class JobFolderRepository(jobRepositoryFolderPath: String, projectFolderReposito
private def initMap(jobRepositoryFolderPath: String): mutable.Map[String, mutable.Map[String, FhirMappingJob]] = {
val map = mutable.Map.empty[String, mutable.Map[String, FhirMappingJob]]
val jobRepositoryFolder = FileUtils.getPath(jobRepositoryFolderPath).toFile
logger.info(s"Initializing the Mapping Repository from path ${jobRepositoryFolder.getAbsolutePath}.")
if (!jobRepositoryFolder.exists()) {
jobRepositoryFolder.mkdirs()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,12 @@ class ProjectMappingFolderRepository(mappingRepositoryFolderPath: String, projec
System.exit(1)
}
}
map.put(projectDirectory.getName, fhirMappingMap)
if(fhirMappingMap.isEmpty) {
// No processable schema files under projectDirectory
logger.warn(s"There are no processable mapping files under ${projectDirectory.getAbsolutePath}. Skipping ${projectDirectory.getName}.")
} else {
map.put(projectDirectory.getName, fhirMappingMap)
}
}
map
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ trait IProjectRepository {
def getProject(id: String): Future[Option[Project]]

/**
* Update the some fields of project in the repository.
* Update some fields of project in the repository.
*
* @param id id of the project
* @param id id of the project
* @param patch patch to be applied to the project
* @return
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class ProjectFolderRepository(config: ToFhirEngineConfig) extends IProjectReposi
}

/**
* Update the some fields of project in the repository.
* Update some fields of the project in the repository.
*
* @param id id of the project
* @param patch patch to be applied to the project
Expand All @@ -100,8 +100,10 @@ class ProjectFolderRepository(config: ToFhirEngineConfig) extends IProjectReposi
throw ResourceNotFound("Project does not exist.", s"Project $id not found")

// update the editable fields of project with new values
val newDescription = (patch \ ProjectEditableFields.DESCRIPTION).extract[String]
val updatedProject = projects(id).copy(description = Some(newDescription))
val newDescription = (patch \ ProjectEditableFields.DESCRIPTION).extractOpt[String]
val newSchemaUrlPrefix = (patch \ ProjectEditableFields.SCHEMA_URL_PREFIX).extractOpt[String]
val newMappingUrlPrefix = (patch \ ProjectEditableFields.MAPPING_URL_PREFIX).extractOpt[String]
val updatedProject = projects(id).copy(description = newDescription, schemaUrlPrefix = newSchemaUrlPrefix, mappingUrlPrefix = newMappingUrlPrefix)
projects.put(id, updatedProject)

// update the projects metadata
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,12 @@ class SchemaFolderRepository(schemaRepositoryFolderPath: String, projectFolderRe
System.exit(1)
}
}
schemaDefinitionMap.put(projectFolder.getName, projectSchemas)
if(projectSchemas.isEmpty) {
// No processable schema files under projectFolder
logger.warn(s"There are no processable schema files under ${projectFolder.getAbsolutePath}. Skipping ${projectFolder.getName}.")
} else {
schemaDefinitionMap.put(projectFolder.getName, projectSchemas)
}
})
schemaDefinitionMap
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,9 @@ class ProjectService(projectRepository: IProjectRepository,
* Save project to the repository.
*
* @param project project to be saved
* @throws BadRequest when the given project is not valid
* @return the created project
*/
def createProject(project: Project): Future[Project] = {
try {
project.validate()
} catch {
case _: IllegalArgumentException => throw BadRequest("Invalid project content !", s"The given id ${project.id} is not a valid UUID.")
}
projectRepository.createProject(project)
}

Expand All @@ -56,7 +50,7 @@ class ProjectService(projectRepository: IProjectRepository,
}

/**
* Update the some fields of a project in the repository.
* Update some fields of a project in the repository.
*
* @param id id of the project
* @param project patch to be applied to the project
Expand All @@ -77,9 +71,9 @@ class ProjectService(projectRepository: IProjectRepository,
// first delete the project from repository
projectRepository.removeProject(id)
// if project deletion is failed throw the error
.recover {case e: Throwable => throw e}
.recover { case e: Throwable => throw e }
// else delete jobs, mappings, mapping contexts and schemas as well
.map(_=> {
.map(_ => {
jobRepository.deleteProjectJobs(id)
mappingRepository.deleteProjectMappings(id)
mappingContextRepository.deleteProjectMappingContexts(id)
Expand All @@ -94,10 +88,20 @@ class ProjectService(projectRepository: IProjectRepository,
* @throws BadRequest when the given patch is invalid
*/
private def validateProjectPatch(patch: JObject): Unit = {
if (patch.obj.isEmpty || !patch.obj.forall {
case (ProjectEditableFields.DESCRIPTION, JString(_)) => true
case _ => false
})
// Define the allowed fields set from ProjectEditableFields
val allowedFields = Set(
ProjectEditableFields.DESCRIPTION,
ProjectEditableFields.SCHEMA_URL_PREFIX,
ProjectEditableFields.MAPPING_URL_PREFIX
)

// Extract the keys from the patch
val patchKeys = patch.obj.map(_._1).toSet

// Check for invalid fields
val invalidFields = patchKeys.diff(allowedFields)
if (invalidFields.nonEmpty) {
throw BadRequest("Invalid Patch!", "Invalid project patch!")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ class FolderDBInitializer(schemaFolderRepository: SchemaFolderRepository,
val parsedProjects = if (file.exists()) {
val projects: JArray = FileOperations.readFileIntoJson(file).asInstanceOf[JArray]
val projectMap: Map[String, Project] = projects.arr.map(p => {
val project: Project = initProjectFromMetadata(p.asInstanceOf[JObject])
project.id -> project
})
val project: Project = initProjectFromMetadata(p.asInstanceOf[JObject])
project.id -> project
})
.toMap
collection.mutable.Map(projectMap.toSeq: _*)
} else {
Expand All @@ -67,8 +67,9 @@ class FolderDBInitializer(schemaFolderRepository: SchemaFolderRepository,
private def initProjectFromMetadata(projectMetadata: JObject): Project = {
val id: String = (projectMetadata \ "id").extract[String]
val name: String = (projectMetadata \ "name").extract[String]
val url: String = (projectMetadata \ "url").extract[String]
val description: Option[String] = (projectMetadata \ "description").extractOpt[String]
val schemaUrlPrefix: Option[String] = (projectMetadata \ "schemaUrlPrefix").extractOpt[String]
val mappingUrlPrefix: Option[String] = (projectMetadata \ "mappingUrlPrefix").extractOpt[String]
val mappingContexts: Seq[String] = (projectMetadata \ "mappingContexts").extract[Seq[String]]
// resolve schemas via the schema repository
val schemaFutures: Future[Seq[Option[SchemaDefinition]]] = Future.sequence(
Expand Down Expand Up @@ -109,7 +110,16 @@ class FolderDBInitializer(schemaFolderRepository: SchemaFolderRepository,
)
val jobs: Seq[FhirMappingJob] = Await.result[Seq[Option[FhirMappingJob]]](mappingJobFutures, 2 seconds).map(_.get)

Project(id, name, url, description, schemas, mappings, mappingContexts, jobs)
Project(id, name, description, schemaUrlPrefix, mappingUrlPrefix, schemas, mappings, mappingContexts, jobs)
}

private def dropLastPart(url: String): String = {
// Remove trailing slash if it exists, to handle cases where URL ends with "/"
val cleanedUrl = if (url.endsWith("/")) url.dropRight(1) else url
// Find the last slash and drop everything after it
val lastSlashIndex = cleanedUrl.lastIndexOf('/')
if (lastSlashIndex != -1) cleanedUrl.substring(0, lastSlashIndex + 1)
else url // Return original URL if no slashes found
}

/**
Expand All @@ -125,17 +135,19 @@ class FolderDBInitializer(schemaFolderRepository: SchemaFolderRepository,
val schemas: mutable.Map[String, mutable.Map[String, SchemaDefinition]] = schemaFolderRepository.getCachedSchemas()
schemas.foreach(projectIdAndSchemas => {
val projectId: String = projectIdAndSchemas._1
val schemaUrl: String = projectIdAndSchemas._2.head._2.url
// If there is no project create a new one. Use id as name as well
val project: Project = projects.getOrElse(projectId, Project(projectId, projectId, convertToUrl(projectId), None))
val project: Project = projects.getOrElse(projectId, Project(id = projectId, name = projectId, schemaUrlPrefix = Some(dropLastPart(schemaUrl))))
projects.put(projectId, project.copy(schemas = projectIdAndSchemas._2.values.toSeq))
})

// Parse mappings
val mappings: mutable.Map[String, mutable.Map[String, FhirMapping]] = mappingFolderRepository.getCachedMappings()
mappings.foreach(projectIdAndMappings => {
val projectId: String = projectIdAndMappings._1
val mappingUrl: String = projectIdAndMappings._2.head._2.url
// If there is no project create a new one. Use id as name as well
val project: Project = projects.getOrElse(projectId, Project(projectId, projectId, convertToUrl(projectId), None))
val project: Project = projects.getOrElse(projectId, Project(id = projectId, name = projectId, mappingUrlPrefix = Some(dropLastPart(mappingUrl))))
projects.put(projectId, project.copy(mappings = projectIdAndMappings._2.values.toSeq))
})

Expand All @@ -144,7 +156,7 @@ class FolderDBInitializer(schemaFolderRepository: SchemaFolderRepository,
jobs.foreach(projectIdAndMappingsJobs => {
val projectId: String = projectIdAndMappingsJobs._1
// If there is no project create a new one. Use id as name as well
val project: Project = projects.getOrElse(projectId, Project(projectId, projectId, convertToUrl(projectId), None))
val project: Project = projects.getOrElse(projectId, Project(id = projectId, name = projectId))
projects.put(projectId, project.copy(mappingJobs = projectIdAndMappingsJobs._2.values.toSeq))
})

Expand All @@ -153,21 +165,11 @@ class FolderDBInitializer(schemaFolderRepository: SchemaFolderRepository,
mappingContexts.foreach(mappingContexts => {
val projectId: String = mappingContexts._1
// If there is no project create a new one. Use id as name as well
val project: Project = projects.getOrElse(projectId, Project(projectId, projectId, convertToUrl(projectId), None))
val project: Project = projects.getOrElse(projectId, Project(id = projectId, name = projectId))
projects.put(projectId, project.copy(mappingContexts = mappingContexts._2))
})

projects
}

/**
* Converts a project id to a url in a specific format.
* @param inputString
* @return
*/
private def convertToUrl(inputString: String): String = {
val transformedString = inputString.replaceAll("\\s", "").toLowerCase
val url = s"https://www.$inputString.com"
url
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ trait BaseEndpointTest extends AnyWordSpec with Matchers with ScalatestRouteTest
* Creates a test project whose identifier is stored in [[projectId]].
* */
def createProject(id: Option[String] = None): Unit = {
val project1: Project = Project(id = id.getOrElse(UUID.randomUUID().toString), name = "example", url = "https://www.example.com", description = Some("example project"))
val project1: Project = Project(id = id.getOrElse(UUID.randomUUID().toString), name = "example", description = Some("example project"))
// create a project
Post(s"/${webServerConfig.baseUri}/${ProjectEndpoint.SEGMENT_PROJECTS}", HttpEntity(ContentTypes.`application/json`, writePretty(project1))) ~> route ~> check {
status shouldEqual StatusCodes.Created
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,8 +370,8 @@ class MappingExecutionEndpointTest extends BaseEndpointTest with OnFhirTestConta
// run the job
Post(s"/${webServerConfig.baseUri}/${ProjectEndpoint.SEGMENT_PROJECTS}/$projectId/${JobEndpoint.SEGMENT_JOB}/$jobId/${JobEndpoint.SEGMENT_RUN}", HttpEntity(ContentTypes.`application/json`, "")) ~> route ~> check {
status shouldEqual StatusCodes.OK
// Mappings run asynchronously. Wait at most 30 seconds for mappings to complete.
val success = waitForCondition(30) {
// Mappings run asynchronously. Wait at most 45 seconds for mappings to complete.
val success = waitForCondition(45) {
fsSinkFolder.listFiles.exists(_.getName.contains("results.csv")) && {
// read the csv file created in the file system
val csvFile: File = fsSinkFolder.listFiles.find(_.getName.contains("results.csv"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import io.tofhir.common.model.Json4sSupport.formats
import io.tofhir.common.model.SchemaDefinition
import io.tofhir.engine.util.FileUtils
import io.tofhir.server.BaseEndpointTest
import io.tofhir.server.endpoint.{ProjectEndpoint, SchemaDefinitionEndpoint}
import io.tofhir.server.model.{Project, ProjectEditableFields}
import io.tofhir.server.util.TestUtil
import org.json4s.JArray
Expand All @@ -14,9 +13,9 @@ import org.json4s.jackson.Serialization.writePretty

class ProjectEndpointTest extends BaseEndpointTest {
// first project to be created
val project1: Project = Project(name = "example", url = "https://www.example.com", description = Some("example project"))
val project1: Project = Project(name = "example", description = Some("example project"), schemaUrlPrefix = Some("https://example.com/StructureDefinition/"), mappingUrlPrefix = Some("https://example.com/mappings/"))
// second project to be created
val project2: Project = Project(name = "second example", url = "https://www.secondexample.com", description = Some("second example project"))
val project2: Project = Project(name = "second example", description = Some("second example project"))
// patch to be applied to the existing project
val projectPatch: (String, String) = ProjectEditableFields.DESCRIPTION -> "updated description"
// schema definition
Expand Down Expand Up @@ -66,7 +65,8 @@ class ProjectEndpointTest extends BaseEndpointTest {
val project: Project = JsonMethods.parse(responseAs[String]).extract[Project]
project.id shouldEqual project1.id
project.name shouldEqual project1.name
project.url shouldEqual project1.url
project.schemaUrlPrefix shouldEqual project1.schemaUrlPrefix
project.mappingUrlPrefix shouldEqual project1.mappingUrlPrefix
}
// get a project with invalid id
Get(s"/${webServerConfig.baseUri}/${ProjectEndpoint.SEGMENT_PROJECTS}/123123") ~> route ~> check {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ package io.tofhir.server.endpoint
import akka.http.scaladsl.model.{ContentTypes, StatusCodes}
import io.tofhir.engine.util.FhirMappingJobFormatter.formats
import io.tofhir.server.BaseEndpointTest
import io.tofhir.server.endpoint.ProjectEndpoint
import io.tofhir.server.model.Project
import org.json4s.jackson.Serialization.writePretty


class ToFhirRejectionHandlerTest extends BaseEndpointTest {
val project1: Project = Project(name = "example", url = "https://www.example.com", description = Some("example project"))
val project1: Project = Project(name = "example", description = Some("example project"))

"ToFhirRejectionHandler" should {

Expand All @@ -23,14 +22,14 @@ class ToFhirRejectionHandlerTest extends BaseEndpointTest {
}

"create an HTTP response with bad request status for content not complying with the data model" in {
// Send a POST request with a project without URL
val projectWithoutUrl = Map("name" -> "example3",
// Send a POST request with a project without name
val projectWithoutUrl = Map("url" -> "http://example3.com/example-project",
"description" -> Some("example project"))
Post(s"/${webServerConfig.baseUri}/${ProjectEndpoint.SEGMENT_PROJECTS}", akka.http.scaladsl.model.HttpEntity.apply(ContentTypes.`application/json`, writePretty(projectWithoutUrl))) ~> route ~> check {
status shouldEqual StatusCodes.BadRequest
val response = responseAs[String]
response should include("Type: https://tofhir.io/errors/BadRequest")
response should include("Detail: No usable value for url")
response should include("Detail: No usable value for name")
}
}

Expand Down

0 comments on commit eadc839

Please sign in to comment.