From bd1c0386a8e636ee9bbc77d00f7c0956f32f2c01 Mon Sep 17 00:00:00 2001 From: Dogukan Cavdaroglu Date: Fri, 15 Nov 2024 17:51:41 +0300 Subject: [PATCH 1/2] :sparkles: Integrate onFHIR definitions microservices --- pom.xml | 18 + tofhir-common/pom.xml | 5 + .../tofhir/common/model/Json4sSupport.scala | 79 --- .../io/tofhir/common/model/JsonFormats.scala | 9 - .../common/model/SchemaDefinition.scala | 58 --- .../model/SimpleStructureDefinition.scala | 107 ----- .../io/tofhir/common/util/HashUtil.scala | 27 -- .../common/util/JavaDateTimeSerializers.scala | 25 - .../io/tofhir/common/util/SchemaUtil.scala | 2 +- .../cli/command/ExtractRedCapSchemas.scala | 2 +- .../data/write/FhirRepositoryWriter.scala | 2 +- .../engine/mapping/FhirMappingService.scala | 2 +- .../engine/mapping/MappingTaskExecutor.scala | 2 +- .../mapping/schema/SchemaConverter.scala | 2 +- .../mapping/FhirMappingFolderRepository.scala | 2 +- .../engine/util/redcap/RedCapUtil.scala | 2 +- .../KafkaSourceIntegrationTest.scala | 2 +- .../scala/io/tofhir/test/SchedulingTest.scala | 2 +- .../src/test/scala/RxNormApiClientTest.scala | 2 +- tofhir-server-common/pom.xml | 5 + .../common/interceptor/IErrorHandler.scala | 14 +- tofhir-server/pom.xml | 10 + .../src/main/resources/application.conf | 2 + .../scala/io/tofhir/server/ToFhirServer.scala | 2 +- .../server/endpoint/CodeSystemEndpoint.scala | 2 +- .../server/endpoint/ConceptMapEndpoint.scala | 2 +- .../endpoint/FhirDefinitionsEndpoint.scala | 124 ----- .../endpoint/FhirPathFunctionsEndpoint.scala | 40 -- .../FileSystemTreeStructureEndpoint.scala | 2 +- .../tofhir/server/endpoint/JobEndpoint.scala | 2 +- .../endpoint/MappingContextEndpoint.scala | 2 +- .../server/endpoint/MappingEndpoint.scala | 2 +- .../server/endpoint/MetadataEndpoint.scala | 4 +- .../server/endpoint/ProjectEndpoint.scala | 2 +- .../server/endpoint/RedCapEndpoint.scala | 2 +- .../endpoint/SchemaDefinitionEndpoint.scala | 4 +- .../TerminologyServiceManagerEndpoint.scala | 2 +- .../endpoint/ToFhirServerEndpoint.scala | 10 +- .../server/fhir/FhirDefinitionsConfig.scala | 38 -- .../fhir/FhirEndpointResourceReader.scala | 106 ---- .../scala/io/tofhir/server/fhir/package.scala | 10 - .../io/tofhir/server/model/CountingMap.scala | 45 -- .../io/tofhir/server/model/ProfileInfo.scala | 3 - .../io/tofhir/server/model/Project.scala | 2 +- .../ProjectMappingFolderRepository.scala | 2 +- .../project/ProjectFolderRepository.scala | 4 +- .../repository/schema/ISchemaRepository.scala | 2 +- .../schema/SchemaFolderRepository.scala | 7 +- .../codesystem/CodeSystemRepository.scala | 2 +- .../conceptmap/ConceptMapRepository.scala | 2 +- .../server/service/MetadataService.scala | 2 +- .../service/SchemaDefinitionService.scala | 2 +- .../service/db/FolderDBInitializer.scala | 4 +- .../service/fhir/FhirDefinitionsService.scala | 179 ------- .../fhir/FhirPathFunctionsService.scala | 45 -- .../SimpleStructureDefinitionService.scala | 452 ------------------ .../fhir/base/FhirBaseProfilesService.scala | 89 ---- .../fhir/base/R4FhirBaseProfilesService.scala | 13 - .../fhir/base/R5FhirBaseProfilesService.scala | 13 - .../tofhir/server/util/GroupByOrdered.scala | 40 -- .../src/test/resources/application.conf | 2 + .../io/tofhir/server/BaseEndpointTest.scala | 4 +- .../FhirDefinitionsEndpointTest.scala | 7 +- .../FileSystemTreeStructureEndpointTest.scala | 2 +- .../endpoint/MappingContextEndpointTest.scala | 2 +- .../MappingExecutionEndpointTest.scala | 2 +- .../endpoint/MetadataEndpointTest.scala | 2 +- .../server/endpoint/ProjectEndpointTest.scala | 4 +- .../server/endpoint/SchemaEndpointTest.scala | 2 +- 69 files changed, 114 insertions(+), 1558 deletions(-) delete mode 100644 tofhir-common/src/main/scala/io/tofhir/common/model/Json4sSupport.scala delete mode 100644 tofhir-common/src/main/scala/io/tofhir/common/model/JsonFormats.scala delete mode 100644 tofhir-common/src/main/scala/io/tofhir/common/model/SchemaDefinition.scala delete mode 100644 tofhir-common/src/main/scala/io/tofhir/common/model/SimpleStructureDefinition.scala delete mode 100644 tofhir-common/src/main/scala/io/tofhir/common/util/HashUtil.scala delete mode 100644 tofhir-common/src/main/scala/io/tofhir/common/util/JavaDateTimeSerializers.scala delete mode 100644 tofhir-server/src/main/scala/io/tofhir/server/endpoint/FhirDefinitionsEndpoint.scala delete mode 100644 tofhir-server/src/main/scala/io/tofhir/server/endpoint/FhirPathFunctionsEndpoint.scala delete mode 100644 tofhir-server/src/main/scala/io/tofhir/server/fhir/FhirDefinitionsConfig.scala delete mode 100644 tofhir-server/src/main/scala/io/tofhir/server/fhir/FhirEndpointResourceReader.scala delete mode 100644 tofhir-server/src/main/scala/io/tofhir/server/fhir/package.scala delete mode 100644 tofhir-server/src/main/scala/io/tofhir/server/model/CountingMap.scala delete mode 100644 tofhir-server/src/main/scala/io/tofhir/server/model/ProfileInfo.scala delete mode 100644 tofhir-server/src/main/scala/io/tofhir/server/service/fhir/FhirDefinitionsService.scala delete mode 100644 tofhir-server/src/main/scala/io/tofhir/server/service/fhir/FhirPathFunctionsService.scala delete mode 100644 tofhir-server/src/main/scala/io/tofhir/server/service/fhir/SimpleStructureDefinitionService.scala delete mode 100644 tofhir-server/src/main/scala/io/tofhir/server/service/fhir/base/FhirBaseProfilesService.scala delete mode 100644 tofhir-server/src/main/scala/io/tofhir/server/service/fhir/base/R4FhirBaseProfilesService.scala delete mode 100644 tofhir-server/src/main/scala/io/tofhir/server/service/fhir/base/R5FhirBaseProfilesService.scala delete mode 100644 tofhir-server/src/main/scala/io/tofhir/server/util/GroupByOrdered.scala diff --git a/pom.xml b/pom.xml index c9bf415d3..00b36363a 100644 --- a/pom.xml +++ b/pom.xml @@ -118,6 +118,7 @@ 10.5.3 0.10.2 5.8 + 1.0-SNAPSHOT @@ -480,6 +481,23 @@ ${project.version} + + + io.onfhir + onfhir-fhirpath-functions + ${onfhir-definitions-microservices.version} + + + io.onfhir + onfhir-resource-definitions + ${onfhir-definitions-microservices.version} + + + io.onfhir + onfhir-definition-commons + ${onfhir-definitions-microservices.version} + + com.typesafe.akka diff --git a/tofhir-common/pom.xml b/tofhir-common/pom.xml index b7856960c..3b0d8e2c7 100644 --- a/tofhir-common/pom.xml +++ b/tofhir-common/pom.xml @@ -64,5 +64,10 @@ io.onfhir onfhir-path_${scala.binary.version} + + + io.onfhir + onfhir-definition-commons + diff --git a/tofhir-common/src/main/scala/io/tofhir/common/model/Json4sSupport.scala b/tofhir-common/src/main/scala/io/tofhir/common/model/Json4sSupport.scala deleted file mode 100644 index 3e1f4ca84..000000000 --- a/tofhir-common/src/main/scala/io/tofhir/common/model/Json4sSupport.scala +++ /dev/null @@ -1,79 +0,0 @@ -package io.tofhir.common.model - -import akka.http.scaladsl.marshalling.{Marshaller, ToEntityMarshaller} -import akka.http.scaladsl.model.MediaTypes.`application/json` -import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller} -import akka.util.ByteString -import org.json4s.{Formats, MappingException, Serialization, jackson} - -import java.lang.reflect.InvocationTargetException - -/** - * Automatic to and from JSON marshalling/unmarshalling using an in-scope *Json4s* protocol. - * - * Pretty printing is enabled if an implicit [[Json4sSupport.ShouldWritePretty.True]] is in scope. - */ -object Json4sSupport extends Json4sSupport { - - implicit lazy val formats: Formats = JsonFormats.getFormats - implicit lazy val serialization: Serialization = jackson.Serialization - - sealed abstract class ShouldWritePretty - - object ShouldWritePretty { - object True extends ShouldWritePretty - - object False extends ShouldWritePretty - } - -} - -/** - * Automatic to and from JSON marshalling/unmarshalling using an in-scope *Json4s* protocol. - * - * Pretty printing is enabled if an implicit [[Json4sSupport.ShouldWritePretty.True]] is in scope. - */ -trait Json4sSupport { - - import Json4sSupport._ - - private val jsonStringUnmarshaller = - Unmarshaller.byteStringUnmarshaller - .forContentTypes(`application/json`) - .mapWithCharset { - case (ByteString.empty, _) => throw Unmarshaller.NoContentException - case (data, charset) => data.decodeString(charset.nioCharset.name) - } - - private val jsonStringMarshaller = Marshaller.stringMarshaller(`application/json`) - - /** - * HTTP entity => `A` - * - * @tparam A type to decode - * @return unmarshaller for `A` - */ - implicit def unmarshaller[A: Manifest](implicit serialization: Serialization, formats: Formats): FromEntityUnmarshaller[A] = - jsonStringUnmarshaller - .map(s => serialization.read(s)) - .recover { _ => - _ => { - case MappingException(_, ite: InvocationTargetException) => throw ite.getCause - } - } - - /** - * `A` => HTTP entity - * - * @tparam A type to encode, must be upper bounded by `AnyRef` - * @return marshaller for any `A` value - */ - implicit def marshaller[A <: AnyRef](implicit serialization: Serialization, formats: Formats, - shouldWritePretty: ShouldWritePretty = ShouldWritePretty.False): ToEntityMarshaller[A] = - shouldWritePretty match { - case ShouldWritePretty.False => - jsonStringMarshaller.compose(serialization.write[A]) - case ShouldWritePretty.True => - jsonStringMarshaller.compose(serialization.writePretty[A]) - } -} diff --git a/tofhir-common/src/main/scala/io/tofhir/common/model/JsonFormats.scala b/tofhir-common/src/main/scala/io/tofhir/common/model/JsonFormats.scala deleted file mode 100644 index 1d09a2396..000000000 --- a/tofhir-common/src/main/scala/io/tofhir/common/model/JsonFormats.scala +++ /dev/null @@ -1,9 +0,0 @@ -package io.tofhir.common.model - -import io.tofhir.common.util.JavaDateTimeSerializers -import org.json4s.jackson.Serialization -import org.json4s.{Formats, NoTypeHints} - -object JsonFormats { - def getFormats: Formats = Serialization.formats(NoTypeHints) + JavaDateTimeSerializers.LocalDateTimeSerializer -} diff --git a/tofhir-common/src/main/scala/io/tofhir/common/model/SchemaDefinition.scala b/tofhir-common/src/main/scala/io/tofhir/common/model/SchemaDefinition.scala deleted file mode 100644 index bbf1464f4..000000000 --- a/tofhir-common/src/main/scala/io/tofhir/common/model/SchemaDefinition.scala +++ /dev/null @@ -1,58 +0,0 @@ -package io.tofhir.common.model - -import io.tofhir.common.util.HashUtil -import org.json4s.JsonAST.{JObject, JString} - -/** - * Entity representing a FHIR StructureDefinition in the context of toFHIR - * - * @param id Identifier of the schema - * @param url URL of the schema - * @param version Version of the schema - * @param `type` Type of entities that this schema represents - * @param name Name of the schema - * @param description Description of the schema - * @param rootDefinition Root element definition for the schema i.e. the first element in the definition - * @param fieldDefinitions Rest of the element definitions - */ -case class SchemaDefinition(id: String, - url: String, - version: String, - `type`: String, - name: String, - description: Option[String], - rootDefinition: Option[SimpleStructureDefinition], - fieldDefinitions: Option[Seq[SimpleStructureDefinition]]) { - - /** - * Retrieves metadata for this schema. The metadata is being used in the folder-based implementation for the time being. - * - * @return - */ - def getMetadata(): JObject = { - JObject( - List( - "id" -> JString(this.id), - "url" -> JString(this.url), - "type" -> JString(this.`type`), - "name" -> JString(this.name) - ) - ) - } -} - -object SchemaDefinition { - def apply(url: String, - version: String, - `type`: String, - name: String, - description: Option[String], - rootDefinition: Option[SimpleStructureDefinition], - fieldDefinitions: Option[Seq[SimpleStructureDefinition]]): SchemaDefinition = { - // If id is not provided, use MD5 hash of url as the id - val effectiveId = HashUtil.md5Hash(url) - SchemaDefinition(effectiveId, url, version, `type`, name, description, rootDefinition, fieldDefinitions) - } - - val VERSION_LATEST = "latest" -} \ No newline at end of file diff --git a/tofhir-common/src/main/scala/io/tofhir/common/model/SimpleStructureDefinition.scala b/tofhir-common/src/main/scala/io/tofhir/common/model/SimpleStructureDefinition.scala deleted file mode 100644 index 36803cf5b..000000000 --- a/tofhir-common/src/main/scala/io/tofhir/common/model/SimpleStructureDefinition.scala +++ /dev/null @@ -1,107 +0,0 @@ -package io.tofhir.common.model - -import io.onfhir.api.FHIR_ROOT_URL_FOR_DEFINITIONS - -/** - * Simplified (flat) model for FHIR StructureDefinition - * - * @param id - * @param path - * @param dataTypes - * @param isPrimitive - * @param isChoiceRoot - * @param isArray - * @param minCardinality - * @param maxCardinality - * @param boundToValueSet - * @param isValueSetBindingRequired - * @param referencableProfiles - * @param constraintDefinitions - * @param sliceDefinition - * @param sliceName - * @param fixedValue - * @param patternValue - * @param short - * @param definition - * @param comment - * @param elements - */ -case class SimpleStructureDefinition(id: String, - path: String, - dataTypes: Option[Seq[DataTypeWithProfiles]], - isPrimitive: Boolean, - isChoiceRoot: Boolean, - isArray: Boolean, - minCardinality: Int, - maxCardinality: Option[Int], - boundToValueSet: Option[String], - isValueSetBindingRequired: Option[Boolean], - referencableProfiles: Option[Seq[String]], - constraintDefinitions: Option[Seq[ConstraintDefinition]], - sliceDefinition: Option[SliceDefinition], - sliceName: Option[String], - fixedValue: Option[String], - patternValue: Option[String], - referringTo: Option[String], - short: Option[String], - definition: Option[String], - comment: Option[String], - elements: Option[Seq[SimpleStructureDefinition]]) { - - def withElements(_elements: Seq[SimpleStructureDefinition]): SimpleStructureDefinition = { - this.copy(elements = Some(_elements)) - } - - def withReferencedContent(newChildElement: SimpleStructureDefinition): SimpleStructureDefinition = { - this.copy(dataTypes = newChildElement.dataTypes, elements = newChildElement.elements, short = newChildElement.short, - definition = newChildElement.definition, comment = newChildElement.comment) - } - - def getProfileUrlForDataType: Option[String] = { - // If a single dataType exists for the createdElement, use it --> - // if profiles are associated with this data type, use the 1st profile - // otherwise, use the dataType - dataTypes match { - case Some(dts) if dts.length == 1 => - dts.head.profiles match { - case Some(prfls) => Some(prfls.head) // If there are multiple profiles defined for this dataType, consider the 1st one only. - case None => Some(s"$FHIR_ROOT_URL_FOR_DEFINITIONS/StructureDefinition/${dts.head.dataType}") - } - case _ => Option.empty[String] - } - } -} - -/** - * A Fhir data type may be associated with a profile (e.g., a code element might be indicated to conform to a CustomCodeableConcept. In this case, - * the dataType will be CondeableConcept while a single element will exist in profiles list which will be the URL of CustomCodeableConcept. - * - * @param dataType Name of the FHIR data type - * @param profiles URLs of the profiles to which this dataType conforms to - */ -case class DataTypeWithProfiles(dataType: String, profiles: Option[Seq[String]]) - -object DataTypeWithProfiles { - def apply(tuple: (String, Seq[String])): DataTypeWithProfiles = { - DataTypeWithProfiles(tuple._1, if(tuple._2.isEmpty) Option.empty[Seq[String]] else Some(tuple._2)) - } -} - -/** - * Fhir Slicing definition - * - * @param discriminators A sequence of Fhir Slicing Discriminators - * @param ordered If the elements of slices are ordered - * @param rule Rule for slicing (closed | open | openAtEnd) - */ -case class SliceDefinition(discriminators: Seq[SliceDiscriminator], ordered: Boolean, rule: String) -case class SliceDiscriminator(`type`: String, path: String) - -/** - * A FHIR Constraint on an element (Rule --> e.g., + Rule: Must have at least a low or a high or text) - * - * @param key Name of constraint - * @param desc Description of constraint - * @param isWarning If it is only warning - */ -case class ConstraintDefinition(key: String, desc: String, isWarning: Boolean = false) diff --git a/tofhir-common/src/main/scala/io/tofhir/common/util/HashUtil.scala b/tofhir-common/src/main/scala/io/tofhir/common/util/HashUtil.scala deleted file mode 100644 index d5c00e941..000000000 --- a/tofhir-common/src/main/scala/io/tofhir/common/util/HashUtil.scala +++ /dev/null @@ -1,27 +0,0 @@ -package io.tofhir.common.util - -import java.security.MessageDigest - -/** - * Utility class providing hash functions - */ -object HashUtil { - - /** - * Given a text input, return MD5 hash representation as text - * - * @param text - * @return - */ - def md5Hash(text: String): String = { - // Get an instance of the MD5 MessageDigest - val md = MessageDigest.getInstance("MD5") - - // Convert the input string to bytes and compute the hash - val hashBytes = md.digest(text.getBytes) - - // Convert the byte array to a hexadecimal string - hashBytes.map("%02x".format(_)).mkString - } - -} diff --git a/tofhir-common/src/main/scala/io/tofhir/common/util/JavaDateTimeSerializers.scala b/tofhir-common/src/main/scala/io/tofhir/common/util/JavaDateTimeSerializers.scala deleted file mode 100644 index c4ee25ace..000000000 --- a/tofhir-common/src/main/scala/io/tofhir/common/util/JavaDateTimeSerializers.scala +++ /dev/null @@ -1,25 +0,0 @@ -package io.tofhir.common.util - -import org.json4s.CustomSerializer -import org.json4s.JsonAST.{JNull, JString} - -import java.time.format.DateTimeFormatter -import java.time.{LocalDate, LocalDateTime} - -object JavaDateTimeSerializers { - - private val dateTimeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy[-MM[-dd['T'HH[:mm[:ss[.SSS][XXX]]]]]]") - - case object LocalDateTimeSerializer extends CustomSerializer[LocalDateTime](format => ( { - case JString(s) => - val temporalAccessor = dateTimeFormatter.parseBest(s, LocalDateTime.from(_), LocalDate.from(_)) - temporalAccessor match { - case ldt: LocalDateTime => ldt - case ld: LocalDate => ld.atStartOfDay() - } - case JNull => null - }, { - case d: LocalDateTime => JString(d.format(dateTimeFormatter)) - })) - -} diff --git a/tofhir-common/src/main/scala/io/tofhir/common/util/SchemaUtil.scala b/tofhir-common/src/main/scala/io/tofhir/common/util/SchemaUtil.scala index 64b9647d4..069c10731 100644 --- a/tofhir-common/src/main/scala/io/tofhir/common/util/SchemaUtil.scala +++ b/tofhir-common/src/main/scala/io/tofhir/common/util/SchemaUtil.scala @@ -1,7 +1,7 @@ package io.tofhir.common.util import io.onfhir.api.Resource -import io.tofhir.common.model.{SchemaDefinition, SimpleStructureDefinition} +import io.onfhir.definitions.common.model.{SchemaDefinition, SimpleStructureDefinition} import org.json4s.JArray import org.json4s.JsonDSL._ diff --git a/tofhir-engine/src/main/scala/io/tofhir/engine/cli/command/ExtractRedCapSchemas.scala b/tofhir-engine/src/main/scala/io/tofhir/engine/cli/command/ExtractRedCapSchemas.scala index 1425f38c6..95cc22cd8 100644 --- a/tofhir-engine/src/main/scala/io/tofhir/engine/cli/command/ExtractRedCapSchemas.scala +++ b/tofhir-engine/src/main/scala/io/tofhir/engine/cli/command/ExtractRedCapSchemas.scala @@ -1,7 +1,7 @@ package io.tofhir.engine.cli.command import io.onfhir.api.Resource -import io.tofhir.common.model.Json4sSupport.formats +import io.onfhir.definitions.common.model.Json4sSupport.formats import io.tofhir.engine.config.ToFhirConfig import io.tofhir.engine.util.redcap.RedCapUtil import io.tofhir.engine.util.{CsvUtil, FileUtils} diff --git a/tofhir-engine/src/main/scala/io/tofhir/engine/data/write/FhirRepositoryWriter.scala b/tofhir-engine/src/main/scala/io/tofhir/engine/data/write/FhirRepositoryWriter.scala index d76e4432f..d8b794853 100644 --- a/tofhir-engine/src/main/scala/io/tofhir/engine/data/write/FhirRepositoryWriter.scala +++ b/tofhir-engine/src/main/scala/io/tofhir/engine/data/write/FhirRepositoryWriter.scala @@ -7,7 +7,7 @@ import io.onfhir.api.client.{FHIRTransactionBatchBundle, FhirBatchTransactionReq import io.onfhir.api.model.OutcomeIssue import io.onfhir.api.util.FHIRUtil import io.onfhir.client.OnFhirNetworkClient -import io.tofhir.common.model.Json4sSupport.formats +import io.onfhir.definitions.common.model.Json4sSupport.formats import io.tofhir.engine.Execution import io.tofhir.engine.config.ToFhirConfig import io.tofhir.engine.model._ diff --git a/tofhir-engine/src/main/scala/io/tofhir/engine/mapping/FhirMappingService.scala b/tofhir-engine/src/main/scala/io/tofhir/engine/mapping/FhirMappingService.scala index c216adc96..13e5c33a7 100644 --- a/tofhir-engine/src/main/scala/io/tofhir/engine/mapping/FhirMappingService.scala +++ b/tofhir-engine/src/main/scala/io/tofhir/engine/mapping/FhirMappingService.scala @@ -4,7 +4,7 @@ import io.onfhir.api.Resource import io.onfhir.expression.{FhirExpression, FhirExpressionException} import io.onfhir.path.{FhirPathEvaluator, IFhirPathFunctionLibraryFactory} import io.onfhir.template.FhirTemplateExpressionHandler -import io.tofhir.common.model.Json4sSupport.formats +import io.onfhir.definitions.common.model.Json4sSupport.formats import io.tofhir.engine.mapping.fhirPath.FhirMappingFunctionsFactory import io.tofhir.engine.mapping.service.IntegratedServiceFactory import io.tofhir.engine.model._ diff --git a/tofhir-engine/src/main/scala/io/tofhir/engine/mapping/MappingTaskExecutor.scala b/tofhir-engine/src/main/scala/io/tofhir/engine/mapping/MappingTaskExecutor.scala index e16a54122..2c32a8ddc 100644 --- a/tofhir-engine/src/main/scala/io/tofhir/engine/mapping/MappingTaskExecutor.scala +++ b/tofhir-engine/src/main/scala/io/tofhir/engine/mapping/MappingTaskExecutor.scala @@ -5,7 +5,7 @@ import io.onfhir.api.client.FhirClientException import io.onfhir.expression.FhirExpressionException import io.onfhir.path.FhirPathException import org.json4s.jackson.Serialization -import io.tofhir.common.model.Json4sSupport.formats +import io.onfhir.definitions.common.model.Json4sSupport.formats import io.tofhir.common.util.ExceptionUtil import io.tofhir.engine.config.ToFhirConfig import io.tofhir.engine.data.read.SourceHandler diff --git a/tofhir-engine/src/main/scala/io/tofhir/engine/mapping/schema/SchemaConverter.scala b/tofhir-engine/src/main/scala/io/tofhir/engine/mapping/schema/SchemaConverter.scala index 8b9e26c8d..8522c2a81 100644 --- a/tofhir-engine/src/main/scala/io/tofhir/engine/mapping/schema/SchemaConverter.scala +++ b/tofhir-engine/src/main/scala/io/tofhir/engine/mapping/schema/SchemaConverter.scala @@ -5,7 +5,7 @@ import io.onfhir.api.validation.{ConstraintKeys, ElementRestrictions, ProfileRes import io.onfhir.api.{FHIR_DATA_TYPES, FHIR_ROOT_URL_FOR_DEFINITIONS, Resource} import io.onfhir.r4.parsers.R4Parser import io.onfhir.validation.{ArrayRestriction, CardinalityMinRestriction, TypeRestriction} -import io.tofhir.common.model.{DataTypeWithProfiles, SimpleStructureDefinition} +import io.onfhir.definitions.common.model.{DataTypeWithProfiles, SimpleStructureDefinition} import io.tofhir.engine.util.MajorFhirVersion import org.apache.spark.sql.types._ diff --git a/tofhir-engine/src/main/scala/io/tofhir/engine/repository/mapping/FhirMappingFolderRepository.scala b/tofhir-engine/src/main/scala/io/tofhir/engine/repository/mapping/FhirMappingFolderRepository.scala index 1a593efab..d86719ad9 100644 --- a/tofhir-engine/src/main/scala/io/tofhir/engine/repository/mapping/FhirMappingFolderRepository.scala +++ b/tofhir-engine/src/main/scala/io/tofhir/engine/repository/mapping/FhirMappingFolderRepository.scala @@ -2,7 +2,7 @@ package io.tofhir.engine.repository.mapping import com.typesafe.scalalogging.Logger import io.onfhir.api.util.IOUtil -import io.tofhir.common.model.Json4sSupport.formats +import io.onfhir.definitions.common.model.Json4sSupport.formats import io.tofhir.engine.mapping.context.MappingContextLoader import io.tofhir.engine.model.FhirMapping import io.tofhir.engine.model.exception.FhirMappingException diff --git a/tofhir-engine/src/main/scala/io/tofhir/engine/util/redcap/RedCapUtil.scala b/tofhir-engine/src/main/scala/io/tofhir/engine/util/redcap/RedCapUtil.scala index a313ea5e9..c89100bb8 100644 --- a/tofhir-engine/src/main/scala/io/tofhir/engine/util/redcap/RedCapUtil.scala +++ b/tofhir-engine/src/main/scala/io/tofhir/engine/util/redcap/RedCapUtil.scala @@ -1,7 +1,7 @@ package io.tofhir.engine.util.redcap import io.onfhir.api.{FHIR_DATA_TYPES, FHIR_FOUNDATION_RESOURCES, FHIR_ROOT_URL_FOR_DEFINITIONS, Resource} -import io.tofhir.common.model.{DataTypeWithProfiles, SchemaDefinition, SimpleStructureDefinition} +import io.onfhir.definitions.common.model.{DataTypeWithProfiles, SchemaDefinition, SimpleStructureDefinition} import io.tofhir.common.util.SchemaUtil import io.tofhir.engine.config.ToFhirConfig diff --git a/tofhir-engine/src/test/scala/io/tofhir/integrationtest/KafkaSourceIntegrationTest.scala b/tofhir-engine/src/test/scala/io/tofhir/integrationtest/KafkaSourceIntegrationTest.scala index e560a250e..423913382 100644 --- a/tofhir-engine/src/test/scala/io/tofhir/integrationtest/KafkaSourceIntegrationTest.scala +++ b/tofhir-engine/src/test/scala/io/tofhir/integrationtest/KafkaSourceIntegrationTest.scala @@ -5,7 +5,7 @@ import io.onfhir.api.client.FhirBatchTransactionRequestBuilder import io.onfhir.api.util.FHIRUtil import io.onfhir.path.FhirPathUtilFunctionsFactory import io.tofhir.{OnFhirTestContainer, ToFhirTestSpec} -import io.tofhir.common.model.Json4sSupport.formats +import io.onfhir.definitions.common.model.Json4sSupport.formats import io.tofhir.engine.Execution.actorSystem.dispatcher import io.tofhir.engine.mapping.job.FhirMappingJobManager import io.tofhir.engine.model._ diff --git a/tofhir-engine/src/test/scala/io/tofhir/test/SchedulingTest.scala b/tofhir-engine/src/test/scala/io/tofhir/test/SchedulingTest.scala index c4a7258b5..4d65234b9 100644 --- a/tofhir-engine/src/test/scala/io/tofhir/test/SchedulingTest.scala +++ b/tofhir-engine/src/test/scala/io/tofhir/test/SchedulingTest.scala @@ -5,7 +5,7 @@ import io.onfhir.api.client.FhirBatchTransactionRequestBuilder import io.onfhir.api.util.FHIRUtil import io.onfhir.path.FhirPathUtilFunctionsFactory import io.tofhir.{OnFhirTestContainer, ToFhirTestSpec} -import io.tofhir.common.model.Json4sSupport.formats +import io.onfhir.definitions.common.model.Json4sSupport.formats import io.tofhir.engine.config.ToFhirConfig import io.tofhir.engine.mapping.job.{FhirMappingJobManager, MappingJobScheduler} import io.tofhir.engine.mapping.context.MappingContextLoader diff --git a/tofhir-rxnorm/src/test/scala/RxNormApiClientTest.scala b/tofhir-rxnorm/src/test/scala/RxNormApiClientTest.scala index ffa501b54..8be8f73f7 100644 --- a/tofhir-rxnorm/src/test/scala/RxNormApiClientTest.scala +++ b/tofhir-rxnorm/src/test/scala/RxNormApiClientTest.scala @@ -1,5 +1,5 @@ import akka.actor.ActorSystem -import io.tofhir.common.model.Json4sSupport.formats +import io.onfhir.definitions.common.model.Json4sSupport.formats import io.tofhir.rxnorm.RxNormApiClient import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers diff --git a/tofhir-server-common/pom.xml b/tofhir-server-common/pom.xml index 7e3e399aa..2597a8f5e 100644 --- a/tofhir-server-common/pom.xml +++ b/tofhir-server-common/pom.xml @@ -82,5 +82,10 @@ com.typesafe.scala-logging scala-logging_${scala.binary.version} + + + io.onfhir + onfhir-resource-definitions + diff --git a/tofhir-server-common/src/main/scala/io/tofhir/server/common/interceptor/IErrorHandler.scala b/tofhir-server-common/src/main/scala/io/tofhir/server/common/interceptor/IErrorHandler.scala index ee0caa8aa..f87d98487 100644 --- a/tofhir-server-common/src/main/scala/io/tofhir/server/common/interceptor/IErrorHandler.scala +++ b/tofhir-server-common/src/main/scala/io/tofhir/server/common/interceptor/IErrorHandler.scala @@ -2,7 +2,9 @@ package io.tofhir.server.common.interceptor import com.typesafe.scalalogging.LazyLogging import akka.http.scaladsl.server.{Directives, ExceptionHandler} -import io.tofhir.server.common.model.{InternalError, ToFhirError, ToFhirRestCall} +import io.onfhir.definitions.resource.model +import io.onfhir.definitions.resource.model.FhirDefinitionsError +import io.tofhir.server.common.model.{BadRequest, InternalError, ToFhirError, ToFhirRestCall} trait IErrorHandler extends LazyLogging { @@ -28,6 +30,16 @@ trait IErrorHandler extends LazyLogging { case e: ToFhirError => logger.error(s"toFHIR error encountered: ${e.toString}", e) e + // handle the errors coming from onFHIR Definitions Microservice + case e: FhirDefinitionsError => + e match { + case model.BadRequest(title, detail, cause) => + logger.error(s"toFHIR error encountered: ${e.toString}", e) + BadRequest(title, detail, cause) + case _ => + logger.error("Unexpected internal error", e) + InternalError(s"Unexpected internal error: ${e.getClass.getName}", e.getMessage, Some(e)) + } case e: Exception => logger.error("Unexpected internal error", e) InternalError(s"Unexpected internal error: ${e.getClass.getName}", e.getMessage, Some(e)) diff --git a/tofhir-server/pom.xml b/tofhir-server/pom.xml index 451185c9c..f86ac1dde 100644 --- a/tofhir-server/pom.xml +++ b/tofhir-server/pom.xml @@ -106,6 +106,16 @@ logback-core + + + io.onfhir + onfhir-fhirpath-functions + + + io.onfhir + onfhir-resource-definitions + + com.typesafe diff --git a/tofhir-server/src/main/resources/application.conf b/tofhir-server/src/main/resources/application.conf index eb5e0515d..934b00302 100644 --- a/tofhir-server/src/main/resources/application.conf +++ b/tofhir-server/src/main/resources/application.conf @@ -110,6 +110,8 @@ fhir = { # fixed-token = "XXX" } + # A path to a context file/directory from where profiles, value sets and code systems reading should start. + context-path = "./" # Path to the zip file or folder that includes the FHIR resource and data type profile definitions (FHIR StructureDefinition) to be served by toFHIR webserver so that mappings can be performed accordingly. profiles-path = null diff --git a/tofhir-server/src/main/scala/io/tofhir/server/ToFhirServer.scala b/tofhir-server/src/main/scala/io/tofhir/server/ToFhirServer.scala index f2361a566..7da1ba722 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/ToFhirServer.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/ToFhirServer.scala @@ -4,7 +4,7 @@ import io.tofhir.engine.config.ToFhirConfig import io.tofhir.server.config.RedCapServiceConfig import io.tofhir.server.common.config.WebServerConfig import io.tofhir.server.endpoint.ToFhirServerEndpoint -import io.tofhir.server.fhir.FhirDefinitionsConfig +import io.onfhir.definitions.resource.fhir.FhirDefinitionsConfig import scala.util.Try diff --git a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/CodeSystemEndpoint.scala b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/CodeSystemEndpoint.scala index cf214e7de..ed77f6c89 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/CodeSystemEndpoint.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/CodeSystemEndpoint.scala @@ -9,7 +9,7 @@ import io.tofhir.engine.Execution.actorSystem.dispatcher import io.tofhir.server.common.model.{ResourceNotFound, ToFhirRestCall} import io.tofhir.server.endpoint.CodeSystemEndpoint.SEGMENT_CODE_SYSTEMS import io.tofhir.server.endpoint.TerminologyServiceManagerEndpoint._ -import io.tofhir.common.model.Json4sSupport._ +import io.onfhir.definitions.common.model.Json4sSupport._ import io.tofhir.server.model.csv.CsvHeader import io.tofhir.server.service.terminology.CodeSystemService import io.tofhir.server.repository.terminology.codesystem.ICodeSystemRepository diff --git a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/ConceptMapEndpoint.scala b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/ConceptMapEndpoint.scala index 0ee3a2092..d5f01256c 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/ConceptMapEndpoint.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/ConceptMapEndpoint.scala @@ -9,7 +9,7 @@ import io.tofhir.engine.Execution.actorSystem.dispatcher import io.tofhir.server.common.model.{ResourceNotFound, ToFhirRestCall} import io.tofhir.server.endpoint.ConceptMapEndpoint.SEGMENT_CONCEPT_MAPS import io.tofhir.server.endpoint.TerminologyServiceManagerEndpoint._ -import io.tofhir.common.model.Json4sSupport._ +import io.onfhir.definitions.common.model.Json4sSupport._ import io.tofhir.server.model.csv.CsvHeader import io.tofhir.server.service.terminology.ConceptMapService import io.tofhir.server.repository.terminology.conceptmap.IConceptMapRepository diff --git a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/FhirDefinitionsEndpoint.scala b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/FhirDefinitionsEndpoint.scala deleted file mode 100644 index 49a32e8bd..000000000 --- a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/FhirDefinitionsEndpoint.scala +++ /dev/null @@ -1,124 +0,0 @@ -package io.tofhir.server.endpoint - -import akka.http.scaladsl.model.{HttpResponse, StatusCodes} -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.Route -import com.typesafe.scalalogging.LazyLogging -import io.onfhir.api.Resource -import io.tofhir.common.model.Json4sSupport._ -import io.tofhir.engine.util.MajorFhirVersion -import io.tofhir.server.common.model.{BadRequest, ToFhirRestCall} -import io.tofhir.server.endpoint.FhirDefinitionsEndpoint._ -import io.tofhir.server.fhir.FhirDefinitionsConfig -import io.tofhir.server.service.fhir.FhirDefinitionsService -import io.tofhir.server.service.fhir.base.FhirBaseProfilesService - -class FhirDefinitionsEndpoint(fhirDefinitionsConfig: FhirDefinitionsConfig) extends LazyLogging { - - val service: FhirDefinitionsService = new FhirDefinitionsService(fhirDefinitionsConfig) - - def route(request: ToFhirRestCall): Route = - pathPrefix(SEGMENT_FHIR_DEFINITIONS) { - pathEndOrSingleSlash { - get { - parameterMap { queryParams => - queryParams.get(QUERY_PARAM_Q) match { - case Some(v) => - DefinitionsQuery.withName(v) match { - case DefinitionsQuery.RESOURCE_TYPES => complete(service.getResourceTypes()) - case DefinitionsQuery.PROFILES => - queryParams.get(QUERY_PARAM_RTYPE) match { - case Some(rtype) => complete(service.getProfilesFor(rtype)) - case None => throw BadRequest("Missing query parameter.", s"$SEGMENT_FHIR_DEFINITIONS?$QUERY_PARAM_Q=${DefinitionsQuery.PROFILES} cannot be invoked without the query parameter '$QUERY_PARAM_RTYPE'.") - } - case DefinitionsQuery.ELEMENTS => - queryParams.get(QUERY_PARAM_PROFILE) match { - case Some(profileUrl) => complete(service.getElementDefinitionsOfProfile(profileUrl)) - case None => complete(HttpResponse(StatusCodes.BadRequest)) // FIXME - } - case unk => throw BadRequest("Invalid parameter value.", s"$QUERY_PARAM_Q on $SEGMENT_FHIR_DEFINITIONS cannot take the value:$unk. Possible values are: ${DefinitionsQuery.values.mkString}") - } - case None => throw BadRequest("Missing query parameter.", s"$SEGMENT_FHIR_DEFINITIONS path cannot be invoked without the query parameter '$QUERY_PARAM_Q'.") - } - } - } - } - } ~ pathPrefix(SEGMENT_VALIDATE) { - pathEndOrSingleSlash { - validateResource() - } - } ~ pathPrefix(SEGMENT_BASE_PROFILES) { - pathEndOrSingleSlash { - get { - parameterMap { queryParams => - // FHIR version - val fhirVersion = queryParams.get(QUERY_PARAM_FHIR_VERSION) - // comma separated profile urls - val profile = queryParams.get(QUERY_PARAM_PROFILE) - - (fhirVersion, profile) match { - // return the SchemaDefinitions for the requested profiles - case (Some(v), Some(p)) => - v match { - case MajorFhirVersion.R4 | MajorFhirVersion.R5 => complete(FhirBaseProfilesService.apply(v).getProfile(p)) - case unk => throw BadRequest("Invalid parameter value.", s"$QUERY_PARAM_FHIR_VERSION on $SEGMENT_BASE_PROFILES cannot take the value: $unk. Possible values are: ${MajorFhirVersion.R4} and ${MajorFhirVersion.R5}") - } - // return the list of available resource types - case (Some(v), None) => - v match { - case MajorFhirVersion.R4 | MajorFhirVersion.R5 => complete(FhirBaseProfilesService.apply(v).resourceTypes) - case unk => throw BadRequest("Invalid parameter value.", s"$QUERY_PARAM_FHIR_VERSION on $SEGMENT_BASE_PROFILES cannot take the value: $unk. Possible values are: ${MajorFhirVersion.R4} and ${MajorFhirVersion.R5}") - } - case (None, _) => throw BadRequest("Missing query parameter.", s"$SEGMENT_BASE_PROFILES path cannot be invoked without the query parameter '$QUERY_PARAM_FHIR_VERSION'.") - } - } - } - } - } - - /** - * Validates a FHIR resource against a given FHIR validation URL in 'fhirValidationUrl' query param. - * - * @return - */ - private def validateResource(): Route = { - post { - parameterMap { paramMap => - paramMap.get(QUERY_PARAM_FHIRVALIDATIONURL) match { - case Some(fhirValidationUrl) => - entity(as[Resource]) { requestBody => - // call the API and return the response - onComplete(service.validateResource(requestBody, fhirValidationUrl)) { - case scala.util.Success(response) => - complete(response) - case scala.util.Failure(ex) => - complete(StatusCodes.InternalServerError, s"Proxy request failed: ${ex.getMessage}") - } - } - case None => throw BadRequest("Missing query parameter.", s"$SEGMENT_VALIDATE path cannot be invoked without the query parameter 'fhirValidationUrl'.") - } - } - } - } - - -} - -object FhirDefinitionsEndpoint { - val SEGMENT_FHIR_DEFINITIONS = "fhir-definitions" - val SEGMENT_VALIDATE = "validate" - val SEGMENT_BASE_PROFILES = "base-profiles" - val QUERY_PARAM_Q = "q" - val QUERY_PARAM_RTYPE = "rtype" - val QUERY_PARAM_PROFILE = "profile" - val QUERY_PARAM_FHIRVALIDATIONURL = "fhirValidationUrl" - val QUERY_PARAM_FHIR_VERSION = "fhirVersion" - - object DefinitionsQuery extends Enumeration { - type DefinitionsQuery = Value - final val RESOURCE_TYPES = Value("rtypes") - final val PROFILES = Value("profiles") - final val ELEMENTS = Value("elements") - } - -} diff --git a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/FhirPathFunctionsEndpoint.scala b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/FhirPathFunctionsEndpoint.scala deleted file mode 100644 index d88a90248..000000000 --- a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/FhirPathFunctionsEndpoint.scala +++ /dev/null @@ -1,40 +0,0 @@ -package io.tofhir.server.endpoint - -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.Route -import com.typesafe.scalalogging.LazyLogging -import io.tofhir.server.common.model.ToFhirRestCall -import io.tofhir.server.endpoint.FhirPathFunctionsEndpoint.SEGMENT_FHIR_PATH_FUNCTIONS -import io.tofhir.common.model.Json4sSupport._ -import io.tofhir.server.service.fhir.FhirPathFunctionsService - -/** - * Endpoint to manage FhirPath functions. - * */ -class FhirPathFunctionsEndpoint extends LazyLogging { - - val service: FhirPathFunctionsService = new FhirPathFunctionsService() - - def route(request: ToFhirRestCall): Route = { - pathPrefix(SEGMENT_FHIR_PATH_FUNCTIONS) { - pathEndOrSingleSlash { - getFhirPathFunctionsDocumentation - } - } - } - - /** - * Returns the documentations of FhirPath functions. - * */ - private def getFhirPathFunctionsDocumentation: Route = { - get { - complete { - service.getFhirPathFunctionsDocumentation - } - } - } -} - -object FhirPathFunctionsEndpoint { - val SEGMENT_FHIR_PATH_FUNCTIONS = "fhir-path-functions" -} diff --git a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/FileSystemTreeStructureEndpoint.scala b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/FileSystemTreeStructureEndpoint.scala index 63087aabd..cbfeffb98 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/FileSystemTreeStructureEndpoint.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/FileSystemTreeStructureEndpoint.scala @@ -5,7 +5,7 @@ import akka.http.scaladsl.server.Route import io.tofhir.server.common.model.ToFhirRestCall import io.tofhir.server.endpoint.FileSystemTreeStructureEndpoint._ import akka.http.scaladsl.server.Directives._ -import io.tofhir.common.model.Json4sSupport._ +import io.onfhir.definitions.common.model.Json4sSupport._ import io.tofhir.server.service.fhir.FileSystemTreeStructureService class FileSystemTreeStructureEndpoint { diff --git a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/JobEndpoint.scala b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/JobEndpoint.scala index 100131b5b..8c17b3715 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/JobEndpoint.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/JobEndpoint.scala @@ -7,7 +7,7 @@ import akka.stream.StreamTcpException import com.typesafe.scalalogging.LazyLogging import io.tofhir.engine.model.FhirMappingJob import io.tofhir.server.endpoint.JobEndpoint.{SEGMENT_EXECUTIONS, SEGMENT_JOB, SEGMENT_MAPPINGS, SEGMENT_RUN, SEGMENT_STATUS, SEGMENT_STOP, SEGMENT_TEST, SEGMENT_DESCHEDULE} -import io.tofhir.common.model.Json4sSupport._ +import io.onfhir.definitions.common.model.Json4sSupport._ import io.tofhir.server.model.{ExecuteJobTask, RowSelectionOrder, TestResourceCreationRequest} import io.tofhir.server.service.{ExecutionService, JobService} import io.tofhir.engine.Execution.actorSystem.dispatcher diff --git a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/MappingContextEndpoint.scala b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/MappingContextEndpoint.scala index 661c1496e..4574bb235 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/MappingContextEndpoint.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/MappingContextEndpoint.scala @@ -8,7 +8,7 @@ import com.typesafe.scalalogging.LazyLogging import io.tofhir.engine.Execution.actorSystem.dispatcher import io.tofhir.server.common.model.ToFhirRestCall import io.tofhir.server.endpoint.MappingContextEndpoint.{ATTACHMENT, SEGMENT_CONTENT, SEGMENT_CONTEXTS, SEGMENT_FILE, SEGMENT_HEADER} -import io.tofhir.common.model.Json4sSupport._ +import io.onfhir.definitions.common.model.Json4sSupport._ import io.tofhir.server.model.csv.CsvHeader import io.tofhir.server.repository.mappingContext.IMappingContextRepository import io.tofhir.server.service.MappingContextService diff --git a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/MappingEndpoint.scala b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/MappingEndpoint.scala index a44e9a380..e0ff085b1 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/MappingEndpoint.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/MappingEndpoint.scala @@ -8,7 +8,7 @@ import io.tofhir.engine.Execution.actorSystem.dispatcher import io.tofhir.engine.model.FhirMapping import io.tofhir.server.common.model.{ResourceNotFound, ToFhirRestCall} import io.tofhir.server.endpoint.MappingEndpoint.SEGMENT_MAPPINGS -import io.tofhir.common.model.Json4sSupport._ +import io.onfhir.definitions.common.model.Json4sSupport._ import io.tofhir.server.repository.job.IJobRepository import io.tofhir.server.repository.mapping.IMappingRepository import io.tofhir.server.service.MappingService diff --git a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/MetadataEndpoint.scala b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/MetadataEndpoint.scala index 4b5964bed..6b6eee0da 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/MetadataEndpoint.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/MetadataEndpoint.scala @@ -3,13 +3,13 @@ package io.tofhir.server.endpoint import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import com.typesafe.scalalogging.LazyLogging -import io.tofhir.common.model.Json4sSupport._ +import io.onfhir.definitions.common.model.Json4sSupport._ import io.tofhir.engine.config.ToFhirEngineConfig import io.tofhir.server.common.config.WebServerConfig import io.tofhir.server.common.model.ToFhirRestCall import io.tofhir.server.config.RedCapServiceConfig import io.tofhir.server.endpoint.MetadataEndpoint.SEGMENT_METADATA -import io.tofhir.server.fhir.FhirDefinitionsConfig +import io.onfhir.definitions.resource.fhir.FhirDefinitionsConfig import io.tofhir.server.service.MetadataService /** diff --git a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/ProjectEndpoint.scala b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/ProjectEndpoint.scala index 34c6786cf..eece92bbd 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/ProjectEndpoint.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/ProjectEndpoint.scala @@ -7,7 +7,7 @@ import com.typesafe.scalalogging.LazyLogging import io.tofhir.engine.Execution.actorSystem.dispatcher import io.tofhir.server.common.model.{ResourceNotFound, ToFhirRestCall} import io.tofhir.server.endpoint.ProjectEndpoint.SEGMENT_PROJECTS -import io.tofhir.common.model.Json4sSupport._ +import io.onfhir.definitions.common.model.Json4sSupport._ import io.tofhir.server.model.Project import io.tofhir.server.repository.job.IJobRepository import io.tofhir.server.repository.mapping.IMappingRepository diff --git a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/RedCapEndpoint.scala b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/RedCapEndpoint.scala index 888671a2d..dac5ee483 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/RedCapEndpoint.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/RedCapEndpoint.scala @@ -8,7 +8,7 @@ import io.tofhir.engine.Execution.actorSystem.dispatcher import io.tofhir.server.common.model.ToFhirRestCall import io.tofhir.server.config.RedCapServiceConfig import io.tofhir.server.endpoint.RedCapEndpoint.{SEGMENT_NOTIFICATION, SEGMENT_REDCAP} -import io.tofhir.common.model.Json4sSupport._ +import io.onfhir.definitions.common.model.Json4sSupport._ import io.tofhir.server.model.redcap.RedCapProjectConfig import io.tofhir.server.service.RedCapService diff --git a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/SchemaDefinitionEndpoint.scala b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/SchemaDefinitionEndpoint.scala index 95bedc46a..9d28b85a1 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/SchemaDefinitionEndpoint.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/SchemaDefinitionEndpoint.scala @@ -4,10 +4,10 @@ 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.onfhir.definitions.common.model.SchemaDefinition import io.tofhir.engine.Execution.actorSystem.dispatcher import io.tofhir.server.endpoint.SchemaDefinitionEndpoint.{SEGMENT_IMPORT, SEGMENT_IMPORT_ZIP, SEGMENT_INFER, SEGMENT_REDCAP, SEGMENT_SCHEMAS} -import io.tofhir.common.model.Json4sSupport._ +import io.onfhir.definitions.common.model.Json4sSupport._ import io.tofhir.server.model.{ImportSchemaSettings, InferTask} import io.tofhir.engine.util.FhirMappingJobFormatter.formats import io.tofhir.server.common.model.{BadRequest, InternalError, ResourceNotFound, ToFhirRestCall} diff --git a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/TerminologyServiceManagerEndpoint.scala b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/TerminologyServiceManagerEndpoint.scala index af222f75b..e4faed9d6 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/TerminologyServiceManagerEndpoint.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/TerminologyServiceManagerEndpoint.scala @@ -7,7 +7,7 @@ import com.typesafe.scalalogging.LazyLogging import io.tofhir.engine.Execution.actorSystem.dispatcher import io.tofhir.server.common.model.{ResourceNotFound, ToFhirRestCall} import io.tofhir.server.endpoint.TerminologyServiceManagerEndpoint._ -import io.tofhir.common.model.Json4sSupport._ +import io.onfhir.definitions.common.model.Json4sSupport._ import io.tofhir.server.model.TerminologySystem import io.tofhir.server.repository.job.JobFolderRepository import io.tofhir.server.repository.terminology.ITerminologySystemRepository diff --git a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/ToFhirServerEndpoint.scala b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/ToFhirServerEndpoint.scala index 877cd1fca..10f3267ad 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/endpoint/ToFhirServerEndpoint.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/endpoint/ToFhirServerEndpoint.scala @@ -6,7 +6,8 @@ import akka.http.scaladsl.server.{RejectionHandler, Route} import io.tofhir.engine.config.ToFhirEngineConfig import io.tofhir.server.config.RedCapServiceConfig import io.tofhir.server.common.config.WebServerConfig -import io.tofhir.server.fhir.FhirDefinitionsConfig +import io.onfhir.definitions.resource.fhir.FhirDefinitionsConfig +import io.onfhir.definitions.resource.endpoint.FhirDefinitionsEndpoint import io.tofhir.server.common.interceptor.{ICORSHandler, IErrorHandler} import io.tofhir.server.common.model.ToFhirRestCall import io.tofhir.server.repository.job.JobFolderRepository @@ -19,6 +20,7 @@ import io.tofhir.server.repository.terminology.codesystem.{CodeSystemRepository, import io.tofhir.server.repository.terminology.conceptmap.{ConceptMapRepository, IConceptMapRepository} import io.tofhir.server.service.db.FolderDBInitializer import io.tofhir.server.util.ToFhirRejectionHandler +import io.onfhir.definitions.fhirpath.endpoint.FhirPathFunctionsEndpoint import java.util.UUID @@ -42,7 +44,7 @@ class ToFhirServerEndpoint(toFhirEngineConfig: ToFhirEngineConfig, webServerConf val projectEndpoint = new ProjectEndpoint(schemaRepository, mappingRepository, mappingJobRepository, mappingContextRepository, projectRepository) val fhirDefinitionsEndpoint = new FhirDefinitionsEndpoint(fhirDefinitionsConfig) - val fhirPathFunctionsEndpoint = new FhirPathFunctionsEndpoint() + val fhirPathFunctionsEndpoint = new FhirPathFunctionsEndpoint(Seq("io.onfhir.path", "io.tofhir.engine.mapping")) val redcapEndpoint = redCapServiceConfig.map(config => new RedCapEndpoint(config)) val fileSystemTreeStructureEndpoint = new FileSystemTreeStructureEndpoint() val terminologyServiceManagerEndpoint = new TerminologyServiceManagerEndpoint(terminologySystemFolderRepository, conceptMapRepository, codeSystemRepository, mappingJobRepository) @@ -64,8 +66,8 @@ class ToFhirServerEndpoint(toFhirEngineConfig: ToFhirEngineConfig, webServerConf val routes = Seq( terminologyServiceManagerEndpoint.route(restCall), projectEndpoint.route(restCall), - fhirDefinitionsEndpoint.route(restCall), - fhirPathFunctionsEndpoint.route(restCall), + fhirDefinitionsEndpoint.route(), + fhirPathFunctionsEndpoint.route(), fileSystemTreeStructureEndpoint.route(restCall), metadataEndpoint.route(restCall) ) ++ redcapEndpoint.map(_.route(restCall)) diff --git a/tofhir-server/src/main/scala/io/tofhir/server/fhir/FhirDefinitionsConfig.scala b/tofhir-server/src/main/scala/io/tofhir/server/fhir/FhirDefinitionsConfig.scala deleted file mode 100644 index 576ddf555..000000000 --- a/tofhir-server/src/main/scala/io/tofhir/server/fhir/FhirDefinitionsConfig.scala +++ /dev/null @@ -1,38 +0,0 @@ -package io.tofhir.server.fhir - -import com.typesafe.config.Config - -import scala.jdk.CollectionConverters._ -import scala.util.Try - -class FhirDefinitionsConfig(fhirDefinitionsConfig: Config) { - - /** Major FHIR version (R4 or R5) */ - lazy val majorFhirVersion: String = Try(fhirDefinitionsConfig.getString("fhir-version")).getOrElse("R4") - /** - * List of root URLs while retrieving the definitions (profiles, valuesets, codesystems). - * The definitions below the given root URLs will be retrieved from the configured paths or FHIR endpoints. - */ - lazy val definitionsRootURLs: Option[Seq[String]] = Try(fhirDefinitionsConfig.getStringList("definitions-root-urls").asScala.toSeq).toOption - - /** FHIR URL to retrieve resource definitions (profiles, valuesets and codesystems). */ - lazy val definitionsFHIREndpoint: Option[String] = Try(fhirDefinitionsConfig.getString("definitions-fhir-endpoint")).toOption - - /** Auth configurations for the definitions FHIR endpoint */ - lazy val definitionsFHIRAuthMethod: Option[String] = Try(fhirDefinitionsConfig.getString("fhir-endpoint-auth.method")).toOption - lazy val authBasicUsername: Option[String] = Try(fhirDefinitionsConfig.getString("fhir-endpoint-auth.basic.username")).toOption - lazy val authBasicPassword: Option[String] = Try(fhirDefinitionsConfig.getString("fhir-endpoint-auth.basic.password")).toOption - lazy val authTokenClientId: Option[String] = Try(fhirDefinitionsConfig.getString("fhir-endpoint-auth.token.client-id")).toOption - lazy val authTokenClientSecret: Option[String] = Try(fhirDefinitionsConfig.getString("fhir-endpoint-auth.token.client-secret")).toOption - lazy val authTokenScopeList: Option[Seq[String]] = Try(fhirDefinitionsConfig.getStringList("fhir-endpoint-auth.token.scopes").asScala.toSeq).toOption - lazy val authTokenEndpoint: Option[String] = Try(fhirDefinitionsConfig.getString("fhir-endpoint-auth.token.token-endpoint")).toOption - lazy val authFixedToken: Option[String] = Try(fhirDefinitionsConfig.getString("fhir-endpoint-auth.fixed-token")).toOption - /** Path to the zip file or folder that includes the FHIR resource and data type profile definitions (FHIR StructureDefinition) to be served by toFHIR webserver so that mappings can be performed accordingly. */ - lazy val profilesPath: Option[String] = Try(fhirDefinitionsConfig.getString("profiles-path")).toOption - - /** Path to the zip file or folder that includes the FHIR Value Set definitions (FHIR ValueSet) that are referenced by your FHIR profiles. */ - lazy val valuesetsPath: Option[String] = Try(fhirDefinitionsConfig.getString("valuesets-path")).toOption - - /** Path to the zip file or folder that includes the FHIR Code system definitions (FHIR CodeSystem) that are referenced by your FHIR value sets. */ - lazy val codesystemsPath: Option[String] = Try(fhirDefinitionsConfig.getString("codesystems-path")).toOption -} diff --git a/tofhir-server/src/main/scala/io/tofhir/server/fhir/FhirEndpointResourceReader.scala b/tofhir-server/src/main/scala/io/tofhir/server/fhir/FhirEndpointResourceReader.scala deleted file mode 100644 index e7e936a7f..000000000 --- a/tofhir-server/src/main/scala/io/tofhir/server/fhir/FhirEndpointResourceReader.scala +++ /dev/null @@ -1,106 +0,0 @@ -package io.tofhir.server.fhir - -import io.onfhir.api.{FHIR_FOUNDATION_RESOURCES, Resource} -import io.onfhir.client.OnFhirNetworkClient -import io.onfhir.config.{FSConfigReader, IFhirConfigReader} -import io.tofhir.engine.model.{BasicAuthenticationSettings, BearerTokenAuthorizationSettings, FixedTokenAuthenticationSettings} -import io.tofhir.engine.util.FhirClientUtil -import io.tofhir.engine.Execution.actorSystem -import actorSystem.dispatcher - -import java.util.concurrent.TimeUnit -import scala.concurrent.{Await, TimeoutException} -import scala.concurrent.duration.FiniteDuration -import scala.util.{Failure, Success, Try} - -/** - * FHIR configuration reader from FHIR endpoint - */ -class FhirEndpointResourceReader(fhirDefinitionsConfig: FhirDefinitionsConfig) extends IFhirConfigReader { - - // So that we can read the base definitions from the standard's bundle file which is available in onfhir-server-r4 or onfhir-server-r5 - val fsConfigReader: IFhirConfigReader = new FSConfigReader(fhirVersion = fhirDefinitionsConfig.majorFhirVersion) - - val fhirClient: OnFhirNetworkClient = createOnFhirClient - - private def createOnFhirClient: OnFhirNetworkClient = { - if (fhirDefinitionsConfig.definitionsFHIREndpoint.isEmpty) { - throw new IllegalStateException("FhirEndpointResourceReader can only be instantiated with a valid FHIR endpoint in fhir.definitions-fhir-endpoint") - } - - fhirDefinitionsConfig.definitionsFHIRAuthMethod match { - case None => FhirClientUtil.createOnFhirClient(fhirDefinitionsConfig.definitionsFHIREndpoint.get) - case Some(method) => FhirAuthMethod.withName(method) match { - case FhirAuthMethod.BASIC => - if (fhirDefinitionsConfig.authBasicUsername.isEmpty || fhirDefinitionsConfig.authBasicPassword.isEmpty) { - throw new IllegalArgumentException("For basic authentication, a username and password must be provided!") - } - FhirClientUtil.createOnFhirClient(fhirDefinitionsConfig.definitionsFHIREndpoint.get, - Some(BasicAuthenticationSettings(fhirDefinitionsConfig.authBasicUsername.get, fhirDefinitionsConfig.authBasicPassword.get))) - case FhirAuthMethod.BEARER_TOKEN => - if (fhirDefinitionsConfig.authTokenClientId.isEmpty || fhirDefinitionsConfig.authTokenClientSecret.isEmpty || fhirDefinitionsConfig.authTokenScopeList.isEmpty || fhirDefinitionsConfig.authTokenEndpoint.isEmpty) { - throw new IllegalArgumentException("For bearer token authentication; client-id, client-secret, token-endpoint and scopes (event if empty) must be provided!") - } - FhirClientUtil.createOnFhirClient(fhirDefinitionsConfig.definitionsFHIREndpoint.get, - Some(BearerTokenAuthorizationSettings(fhirDefinitionsConfig.authTokenClientId.get, fhirDefinitionsConfig.authTokenClientSecret.get, fhirDefinitionsConfig.authTokenScopeList.get, fhirDefinitionsConfig.authTokenEndpoint.get))) - case FhirAuthMethod.FIXED_TOKEN => - if (fhirDefinitionsConfig.authFixedToken.isEmpty) { - throw new IllegalArgumentException("For fixed token authentication, a token must be provided!") - } - FhirClientUtil.createOnFhirClient(fhirDefinitionsConfig.definitionsFHIREndpoint.get, - Some(FixedTokenAuthenticationSettings(fhirDefinitionsConfig.authFixedToken.get))) - } - } - } - - override def readStandardBundleFile(fileName: String, resourceTypeFilter: Set[String]): Seq[Resource] = fsConfigReader.readStandardBundleFile(fileName, resourceTypeFilter) - - override def getInfrastructureResources(rtype: String): Seq[Resource] = { - val fhirSearchRequest = fhirDefinitionsConfig.definitionsRootURLs match { - case Some(urls) if urls.nonEmpty => - fhirClient.search(rtype).where("url:below", urls.map(url => if (url.endsWith("/")) url.dropRight(1) else url).mkString(",")) - case _ => - throw new IllegalArgumentException("fhir.definitions-root-urls must be defined so that the system can read resources under those urls!!") - } - - rtype match { - case FHIR_FOUNDATION_RESOURCES.FHIR_SEARCH_PARAMETER | FHIR_FOUNDATION_RESOURCES.FHIR_OPERATION_DEFINITION | FHIR_FOUNDATION_RESOURCES.FHIR_COMPARTMENT_DEFINITION => - Seq.empty[Resource] // SearchParameter, OperationDefinition and CompartmentDefinition are not supported! - case FHIR_FOUNDATION_RESOURCES.FHIR_STRUCTURE_DEFINITION | FHIR_FOUNDATION_RESOURCES.FHIR_VALUE_SET | FHIR_FOUNDATION_RESOURCES.FHIR_CODE_SYSTEM => - try { - Await.result( - fhirSearchRequest.executeAndMergeBundle().map(_.searchResults).transform { - case Success(value) => Try(value) - case Failure(exception) => - actorSystem.log.error(s"An error occurred while searching infrastructure resources from ${fhirSearchRequest.request.requestUri}", exception) - throw exception - }, - FiniteDuration(30, TimeUnit.SECONDS)) - } catch { - case ex: TimeoutException => - actorSystem.log.error(s"Timeout! Cannot reach FHIR endpoint while searching infrastructure resources from ${fhirSearchRequest.request.requestUri}", ex) - throw ex - } - } - - } - - override def readCapabilityStatement(): Resource = { - val fhirSearchRequest = fhirClient.search("metadata") - try { - Await.result( - fhirSearchRequest.executeAndReturnResource().transform { - case Success(value) => Try(value) - case Failure(exception) => - actorSystem.log.error(s"An error occurred while reading the CapabilityStatement", exception) - throw exception - }, - FiniteDuration(5, TimeUnit.SECONDS)) - } catch { - case ex: TimeoutException => - actorSystem.log.error(s"Timeout! Cannot reach FHIR endpoint while reading capability statement from ${fhirSearchRequest.request.requestUri}", ex) - throw ex - } - } -} - diff --git a/tofhir-server/src/main/scala/io/tofhir/server/fhir/package.scala b/tofhir-server/src/main/scala/io/tofhir/server/fhir/package.scala deleted file mode 100644 index 572fe6136..000000000 --- a/tofhir-server/src/main/scala/io/tofhir/server/fhir/package.scala +++ /dev/null @@ -1,10 +0,0 @@ -package io.tofhir.server - -package object fhir { - object FhirAuthMethod extends Enumeration { - type FhirAuthMethod = Value - final val BASIC = Value("basic") - final val BEARER_TOKEN = Value("token") - final val FIXED_TOKEN = Value("fixed-token") - } -} diff --git a/tofhir-server/src/main/scala/io/tofhir/server/model/CountingMap.scala b/tofhir-server/src/main/scala/io/tofhir/server/model/CountingMap.scala deleted file mode 100644 index d914580ed..000000000 --- a/tofhir-server/src/main/scala/io/tofhir/server/model/CountingMap.scala +++ /dev/null @@ -1,45 +0,0 @@ -package io.tofhir.server.model - -/** - * An immutable map that keeps track of the count of each key. - * - * @param counts The underlying map containing key-value pairs where the key is the element, and the value is the count. - * @tparam K The type of keys in the map. - */ -case class CountingMap[K](counts: Map[K, Int] = Map.empty[K, Int]) { - /** - * Increments the count for the specified key and returns a new CountingMap with the updated counts. - * - * @param key The optional key for which the count should be incremented. - * @return A new CountingMap with the updated counts. - */ - def apply(key: Option[K]): CountingMap[K] = { - key match { - case Some(innerKey) => - CountingMap(counts.updated(innerKey, counts.getOrElse(innerKey, 0) + 1)) - case None => - // Return the current state for None - this - } - } - - /** - * Retrieves the count associated with the specified key. - * - * @param key The key for which to retrieve the count. - * @return The count associated with the key, or 0 if the key is not present. - */ - def get(key: K): Int = { - counts.getOrElse(key, 0) - } -} - -object CountingMap { - /** - * Creates an empty CountingMap. - * - * @tparam A The type of keys. - * @return An empty CountingMap. - */ - def empty[A]: CountingMap[A] = CountingMap(Map.empty[A, Int]) -} \ No newline at end of file diff --git a/tofhir-server/src/main/scala/io/tofhir/server/model/ProfileInfo.scala b/tofhir-server/src/main/scala/io/tofhir/server/model/ProfileInfo.scala deleted file mode 100644 index d6ee66e87..000000000 --- a/tofhir-server/src/main/scala/io/tofhir/server/model/ProfileInfo.scala +++ /dev/null @@ -1,3 +0,0 @@ -package io.tofhir.server.model - -case class ProfileInfo(url: String, version: String) diff --git a/tofhir-server/src/main/scala/io/tofhir/server/model/Project.scala b/tofhir-server/src/main/scala/io/tofhir/server/model/Project.scala index 40f08deb6..5b7b1b1e9 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/model/Project.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/model/Project.scala @@ -5,7 +5,7 @@ import org.json4s.{JArray, JObject, JString} import java.util.UUID -import io.tofhir.common.model.SchemaDefinition +import io.onfhir.definitions.common.model.SchemaDefinition /** * Definition of a project which holds relevant schemas, mapping, mapping-jobs, concept maps etc. diff --git a/tofhir-server/src/main/scala/io/tofhir/server/repository/mapping/ProjectMappingFolderRepository.scala b/tofhir-server/src/main/scala/io/tofhir/server/repository/mapping/ProjectMappingFolderRepository.scala index ee897892d..a1eabc6e1 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/repository/mapping/ProjectMappingFolderRepository.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/repository/mapping/ProjectMappingFolderRepository.scala @@ -2,7 +2,7 @@ package io.tofhir.server.repository.mapping import com.typesafe.scalalogging.Logger import io.onfhir.api.util.IOUtil -import io.tofhir.common.model.Json4sSupport.formats +import io.onfhir.definitions.common.model.Json4sSupport.formats import io.tofhir.engine.Execution.actorSystem.dispatcher import io.tofhir.engine.model.FhirMapping import io.tofhir.engine.util.FileUtils diff --git a/tofhir-server/src/main/scala/io/tofhir/server/repository/project/ProjectFolderRepository.scala b/tofhir-server/src/main/scala/io/tofhir/server/repository/project/ProjectFolderRepository.scala index f0947c1b1..2b87bf67c 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/repository/project/ProjectFolderRepository.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/repository/project/ProjectFolderRepository.scala @@ -1,8 +1,8 @@ package io.tofhir.server.repository.project import com.typesafe.scalalogging.Logger -import io.tofhir.common.model.Json4sSupport.formats -import io.tofhir.common.model.SchemaDefinition +import io.onfhir.definitions.common.model.Json4sSupport.formats +import io.onfhir.definitions.common.model.SchemaDefinition import io.tofhir.engine.Execution.actorSystem.dispatcher import io.tofhir.engine.config.ToFhirEngineConfig import io.tofhir.engine.model.{FhirMapping, FhirMappingJob} diff --git a/tofhir-server/src/main/scala/io/tofhir/server/repository/schema/ISchemaRepository.scala b/tofhir-server/src/main/scala/io/tofhir/server/repository/schema/ISchemaRepository.scala index b8b23c0b9..3869bf6c7 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/repository/schema/ISchemaRepository.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/repository/schema/ISchemaRepository.scala @@ -1,7 +1,7 @@ package io.tofhir.server.repository.schema import io.onfhir.api.Resource -import io.tofhir.common.model.SchemaDefinition +import io.onfhir.definitions.common.model.SchemaDefinition import io.tofhir.engine.mapping.schema.IFhirSchemaLoader import scala.concurrent.Future diff --git a/tofhir-server/src/main/scala/io/tofhir/server/repository/schema/SchemaFolderRepository.scala b/tofhir-server/src/main/scala/io/tofhir/server/repository/schema/SchemaFolderRepository.scala index c2b1be3fc..7ac1da7f4 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/repository/schema/SchemaFolderRepository.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/repository/schema/SchemaFolderRepository.scala @@ -6,10 +6,12 @@ import io.onfhir.api.util.IOUtil import io.onfhir.api.validation.ProfileRestrictions import io.onfhir.api.{FHIR_FOUNDATION_RESOURCES, FHIR_ROOT_URL_FOR_DEFINITIONS, Resource} import io.onfhir.config.{BaseFhirConfig, FSConfigReader, IFhirConfigReader} +import io.onfhir.definitions.resource.service.SimpleStructureDefinitionService import io.onfhir.exception.InitializationException import io.onfhir.util.JsonFormatter._ -import io.tofhir.common.model.SchemaDefinition -import io.tofhir.common.util.{HashUtil, SchemaUtil} +import io.onfhir.definitions.common.model.SchemaDefinition +import io.onfhir.definitions.common.util.HashUtil +import io.tofhir.common.util.SchemaUtil import io.tofhir.engine.Execution.actorSystem.dispatcher import io.tofhir.engine.config.ToFhirConfig import io.tofhir.engine.mapping.schema.SchemaConverter @@ -18,7 +20,6 @@ import io.tofhir.engine.util.FileUtils.FileExtensions import io.tofhir.engine.util.{FhirVersionUtil, FileUtils} import io.tofhir.server.common.model.{AlreadyExists, BadRequest, ResourceNotFound} import io.tofhir.server.repository.project.ProjectFolderRepository -import io.tofhir.server.service.fhir.SimpleStructureDefinitionService import io.tofhir.server.util.FileOperations import org.apache.spark.sql.types.StructType diff --git a/tofhir-server/src/main/scala/io/tofhir/server/repository/terminology/codesystem/CodeSystemRepository.scala b/tofhir-server/src/main/scala/io/tofhir/server/repository/terminology/codesystem/CodeSystemRepository.scala index d0582e29a..8632af97e 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/repository/terminology/codesystem/CodeSystemRepository.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/repository/terminology/codesystem/CodeSystemRepository.scala @@ -2,7 +2,7 @@ package io.tofhir.server.repository.terminology.codesystem import akka.stream.scaladsl.{FileIO, Source} import akka.util.ByteString -import io.tofhir.common.model.Json4sSupport.formats +import io.onfhir.definitions.common.model.Json4sSupport.formats import io.tofhir.engine.Execution.actorSystem.dispatcher import io.tofhir.engine.util.FileUtils import io.tofhir.server.common.model.{AlreadyExists, BadRequest, ResourceNotFound} diff --git a/tofhir-server/src/main/scala/io/tofhir/server/repository/terminology/conceptmap/ConceptMapRepository.scala b/tofhir-server/src/main/scala/io/tofhir/server/repository/terminology/conceptmap/ConceptMapRepository.scala index 93e487a58..14d3b121f 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/repository/terminology/conceptmap/ConceptMapRepository.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/repository/terminology/conceptmap/ConceptMapRepository.scala @@ -2,7 +2,7 @@ package io.tofhir.server.repository.terminology.conceptmap import akka.stream.scaladsl.{Concat, FileIO, Framing, Source} import akka.util.ByteString -import io.tofhir.common.model.Json4sSupport.formats +import io.onfhir.definitions.common.model.Json4sSupport.formats import io.tofhir.engine.Execution.actorSystem import io.tofhir.engine.Execution.actorSystem.dispatcher import io.tofhir.engine.util.FileUtils diff --git a/tofhir-server/src/main/scala/io/tofhir/server/service/MetadataService.scala b/tofhir-server/src/main/scala/io/tofhir/server/service/MetadataService.scala index c4821c5fe..10f2a106d 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/service/MetadataService.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/service/MetadataService.scala @@ -7,7 +7,7 @@ import akka.http.scaladsl.unmarshalling.Unmarshal import io.tofhir.engine.config.ToFhirEngineConfig import io.tofhir.server.common.config.WebServerConfig import io.tofhir.server.config.RedCapServiceConfig -import io.tofhir.server.fhir.FhirDefinitionsConfig +import io.onfhir.definitions.resource.fhir.FhirDefinitionsConfig import io.tofhir.engine.Execution.actorSystem import io.tofhir.engine.Execution.actorSystem.dispatcher import io.tofhir.server.endpoint.MetadataEndpoint.SEGMENT_METADATA diff --git a/tofhir-server/src/main/scala/io/tofhir/server/service/SchemaDefinitionService.scala b/tofhir-server/src/main/scala/io/tofhir/server/service/SchemaDefinitionService.scala index 165c7e993..9d3651b66 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/service/SchemaDefinitionService.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/service/SchemaDefinitionService.scala @@ -4,7 +4,7 @@ import akka.stream.scaladsl.Source import akka.util.ByteString import com.typesafe.scalalogging.LazyLogging import io.onfhir.api.Resource -import io.tofhir.common.model.SchemaDefinition +import io.onfhir.definitions.common.model.SchemaDefinition import io.tofhir.engine.Execution.actorSystem import io.tofhir.engine.config.ToFhirConfig import io.tofhir.engine.data.read.SourceHandler diff --git a/tofhir-server/src/main/scala/io/tofhir/server/service/db/FolderDBInitializer.scala b/tofhir-server/src/main/scala/io/tofhir/server/service/db/FolderDBInitializer.scala index d65b120b7..ece7d0411 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/service/db/FolderDBInitializer.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/service/db/FolderDBInitializer.scala @@ -1,8 +1,8 @@ package io.tofhir.server.service.db import com.typesafe.scalalogging.Logger -import io.tofhir.common.model.Json4sSupport.formats -import io.tofhir.common.model.SchemaDefinition +import io.onfhir.definitions.common.model.Json4sSupport.formats +import io.onfhir.definitions.common.model.SchemaDefinition import io.tofhir.engine.Execution.actorSystem.dispatcher import io.tofhir.engine.model.{FhirMapping, FhirMappingJob} import io.tofhir.engine.util.FileUtils diff --git a/tofhir-server/src/main/scala/io/tofhir/server/service/fhir/FhirDefinitionsService.scala b/tofhir-server/src/main/scala/io/tofhir/server/service/fhir/FhirDefinitionsService.scala deleted file mode 100644 index 26c87eafe..000000000 --- a/tofhir-server/src/main/scala/io/tofhir/server/service/fhir/FhirDefinitionsService.scala +++ /dev/null @@ -1,179 +0,0 @@ -package io.tofhir.server.service.fhir - -import akka.http.scaladsl.Http -import akka.http.scaladsl.model.headers.RawHeader -import akka.http.scaladsl.model.{ContentTypes, HttpEntity, HttpMethods, HttpRequest} -import com.typesafe.scalalogging.Logger -import io.onfhir.api -import io.onfhir.api.Resource -import io.onfhir.config.{BaseFhirConfig, FSConfigReader, IFhirConfigReader} -import io.onfhir.r4.config.FhirR4Configurator -import io.onfhir.r5.config.FhirR5Configurator -import io.tofhir.common.model.Json4sSupport._ -import io.tofhir.common.model.SimpleStructureDefinition -import io.tofhir.engine.Execution.actorSystem -import io.tofhir.engine.Execution.actorSystem.dispatcher -import io.tofhir.engine.util.{FileUtils, MajorFhirVersion} -import io.tofhir.server.common.model.BadRequest -import io.tofhir.server.fhir.{FhirDefinitionsConfig, FhirEndpointResourceReader} -import io.tofhir.server.model.ProfileInfo -import org.json4s.JsonAST.JObject -import org.json4s.jackson.JsonMethods -import org.json4s.jackson.JsonMethods.compact - -import java.net.{MalformedURLException, URL} -import scala.collection.mutable -import scala.concurrent.Future -import scala.concurrent.duration.{DurationInt, FiniteDuration} - -class FhirDefinitionsService(fhirDefinitionsConfig: FhirDefinitionsConfig) { - - private val logger: Logger = Logger(this.getClass) - - // timeout for the proxy request to the FHIR validator - val timeout: FiniteDuration = 20.seconds - - val fhirConfigReader: IFhirConfigReader = fhirDefinitionsConfig.definitionsFHIREndpoint match { - case Some(_) => - new FhirEndpointResourceReader(fhirDefinitionsConfig) - case None => - // use the local file system, create Option[String] from Option[File] if file exists or None if not - val profilesPath = fhirDefinitionsConfig.profilesPath.flatMap(path => { - val filePath = FileUtils.getPath(path) - if (filePath.toFile.exists()) - Some(filePath.toString) - else { - logger.warn("Profiles folder path is configured but folder does not exist.") - None - } - }) - - val codeSystemsPath = fhirDefinitionsConfig.codesystemsPath.flatMap(path => { - val filePath = FileUtils.getPath(path) - if (filePath.toFile.exists()) - Some(filePath.toString) - else { - logger.warn("CodeSystems folder path is configured but folder does not exist.") - None - } - }) - - val valueSetsPath = fhirDefinitionsConfig.valuesetsPath.flatMap(path => { - val filePath = FileUtils.getPath(path) - if (filePath.toFile.exists()) - Some(filePath.toString) - else { - logger.warn("ValueSets folder path is configured but folder does not exist.") - None - } - }) - - new FSConfigReader(fhirVersion = fhirDefinitionsConfig.majorFhirVersion, - profilesPath = profilesPath, - codeSystemsPath = codeSystemsPath, - valueSetsPath = valueSetsPath) - } - - val baseFhirConfig: BaseFhirConfig = fhirDefinitionsConfig.majorFhirVersion match { - case MajorFhirVersion.R4 => - new FhirR4Configurator().initializePlatform(fhirConfigReader) - case MajorFhirVersion.R5 => - new FhirR5Configurator().initializePlatform(fhirConfigReader) - case _ => - throw new RuntimeException("Unsupported FHIR version.") - } - - val simpleStructureDefinitionService = new SimpleStructureDefinitionService(baseFhirConfig) - - // A cache to hold rtype -> ProfileInfo tuples which mean the profileUrl is of type rtype - val profileInfoCache: mutable.Map[String, Set[ProfileInfo]] = mutable.HashMap() - - // TODO: Add comment to explain what this cache does - val simplifiedStructureDefinitionCache: mutable.Map[String, Seq[SimpleStructureDefinition]] = mutable.HashMap() - - /** - * Get all available FHIR resource types. - * - * @return - */ - def getResourceTypes(): Set[String] = { - baseFhirConfig.FHIR_RESOURCE_TYPES - } - - /** - * Given a resource type (e.g., Observation), get the profile URLs whose type are that rtype. - * - * @param rtype Resource type (e.g., Condition) - * @return - */ - def getProfilesFor(rtype: String): Set[ProfileInfo] = { - val profileInfoSet = baseFhirConfig.profileRestrictions - .filter(urlMap => // (url -> (version -> ProfileRestrictions)) Filter the ones whose ProfileRestrictions contain the rtype - urlMap._2.exists(versionMap => versionMap._2.resourceType == rtype)) // The resourceType must be the same for all ProfileRestrictions of a URL (the StructureDefinition). .exists and .forall should evaluate to the same thing - .flatMap(urlMap => urlMap._2.keySet.map(version => ProfileInfo(urlMap._1, version))) // Create the Set of ProfileInfo for each version of each url (and flatten with flatMap) - .toSet - profileInfoCache.getOrElseUpdate(rtype, profileInfoSet) - } - - /** - * Retrieves the simplified structure definitions for a given FHIR profile or schema URL. - * - * This method first attempts to obtain the structure definition from the SimpleStructureDefinitionService. - * If the profile is not found, it queries the Schema Repository. - * - * @param url The URL of the FHIR profile or Schema to retrieve the structure definition for. - * @return A sequence of SimpleStructureDefinition objects representing the simplified structure definition of the profile or schema. - */ - def getElementDefinitionsOfProfile(url: String): Seq[SimpleStructureDefinition] = { - // parse the canonical URL to extract URL and optional version part - val canonicalParts = url.split('|') - val urlWithoutVersion = canonicalParts.head - val version = canonicalParts.drop(1).lastOption - // retrieve the simplified structure definitions from the cache, or simplify it using SimpleStructureDefinitionService if not cached - simplifiedStructureDefinitionCache.getOrElseUpdate(url, simpleStructureDefinitionService.simplifyStructureDefinition(urlWithoutVersion, version)) - } - - /** - * Validates a FHIR resource against a specified FHIR validation URL. - * - * @param requestBody The FHIR resource to be validated. - * @param fhirValidationUrl The URL for FHIR validation. - * @return A Future containing the validated FHIR resource as a JObject. - * @throws BadRequestException If the provided FHIR validation URL is not a valid URL. - */ - def validateResource(requestBody: Resource, fhirValidationUrl: String): Future[JObject] = { - if (!isValidUrl(fhirValidationUrl)) { - throw BadRequest("Invalid fhirValidationUrl", s"$fhirValidationUrl is not a valid URL.") - } - val proxiedRequest = HttpRequest( - method = HttpMethods.POST, - uri = s"$fhirValidationUrl", - headers = RawHeader("Content-Type", "application/json") :: Nil, - entity = HttpEntity(ContentTypes.`application/json`, compact(JsonMethods.render(requestBody))) - ) - - // add timeout and transform response to json - Http().singleRequest(proxiedRequest) - .flatMap { resp => resp.entity.toStrict(timeout) } - .map(strictEntity => { - val response = strictEntity.data.utf8String - JsonMethods.parse(response).extract[JObject] - }) - } - - /** - * Validates if the given string is a valid URL - * - * @param url string to validate - * @return - */ - private def isValidUrl(url: String): Boolean = { - try { - new URL(url) - true - } catch { - case _: MalformedURLException => false - } - } - -} diff --git a/tofhir-server/src/main/scala/io/tofhir/server/service/fhir/FhirPathFunctionsService.scala b/tofhir-server/src/main/scala/io/tofhir/server/service/fhir/FhirPathFunctionsService.scala deleted file mode 100644 index 35ca1d8d9..000000000 --- a/tofhir-server/src/main/scala/io/tofhir/server/service/fhir/FhirPathFunctionsService.scala +++ /dev/null @@ -1,45 +0,0 @@ -package io.tofhir.server.service.fhir - -import io.onfhir.path.AbstractFhirPathFunctionLibrary -import io.onfhir.path.annotation.FhirPathFunction -import org.reflections.Reflections - -import scala.jdk.javaapi.CollectionConverters.asScala -import scala.reflect.runtime.currentMirror -import scala.reflect.runtime.universe._ - -/** - * Service to manage FhirPath functions. - * */ -class FhirPathFunctionsService { - - // list of packages where FhirPathFunction libraries will be searched - private val packages: Seq[String] = Seq("io.onfhir.path", "io.tofhir.engine.mapping") - - // keeps the FhirPath functions documentation of FhirPath libraries - // it searches for the libraries in 'packages' and calls 'getFunctionDocumentation' method of classes - // to retrieve function documentation. - private lazy val fhirPathFunctionsDocumentation: Seq[FhirPathFunction] = packages.flatMap(p => { - val reflections = new Reflections(p) - asScala(reflections.getSubTypesOf(classOf[AbstractFhirPathFunctionLibrary])).flatMap(c => { - val classSymbol = currentMirror.classSymbol(c) - - // create an instance of class - val constructorSymbol: MethodSymbol = classSymbol.primaryConstructor.asMethod - val classInstance = currentMirror.reflectClass(classSymbol).reflectConstructor(constructorSymbol).apply(constructorSymbol.paramLists.head.map(_ => null): _*) - val instanceMirror: InstanceMirror = currentMirror.reflect(classInstance) - - // call getFunctionDocumentation method - instanceMirror.reflectMethod(classSymbol.toType.members.find(_.name.toString == "getFunctionDocumentation").map(_.asMethod).get).apply().asInstanceOf[Seq[FhirPathFunction]] - }).toSeq - }) - - /** - * Returns the documentations of FhirPath functions. - * - * @return the documentations of FhirPath functions i.e. a list of FhirPathFunction - * */ - def getFhirPathFunctionsDocumentation: Seq[FhirPathFunction] = { - fhirPathFunctionsDocumentation - } -} diff --git a/tofhir-server/src/main/scala/io/tofhir/server/service/fhir/SimpleStructureDefinitionService.scala b/tofhir-server/src/main/scala/io/tofhir/server/service/fhir/SimpleStructureDefinitionService.scala deleted file mode 100644 index 9200911e6..000000000 --- a/tofhir-server/src/main/scala/io/tofhir/server/service/fhir/SimpleStructureDefinitionService.scala +++ /dev/null @@ -1,452 +0,0 @@ -package io.tofhir.server.service.fhir - -import io.onfhir.api.validation.{ConstraintKeys, ElementRestrictions, ProfileRestrictions} -import io.onfhir.config.BaseFhirConfig -import io.onfhir.validation._ -import io.tofhir.common.model.Json4sSupport.formats -import io.tofhir.common.model._ -import io.tofhir.server.model.CountingMap -import org.json4s.jackson.Serialization - -class SimpleStructureDefinitionService(fhirConfig: BaseFhirConfig) { - - /** - * Convert ProfileRestrictions into a SchemaDefinition instance. - * - * @param profileRestrictions - * @return - */ - def convertToSchemaDefinition(schemaId: String, profileRestrictions: ProfileRestrictions): SchemaDefinition = { - val rootElementDefinition = createRootElement(profileRestrictions.resourceType) - SchemaDefinition(id = schemaId, - url = profileRestrictions.url, - version = profileRestrictions.version.getOrElse(SchemaDefinition.VERSION_LATEST), - `type` = profileRestrictions.resourceType, - name = profileRestrictions.resourceName.getOrElse(profileRestrictions.resourceType), - description = profileRestrictions.resourceDescription, - rootDefinition = Some(rootElementDefinition), - fieldDefinitions = Some(simplifyStructureDefinition(profileRestrictions.url, profileRestrictions.version, withResourceTypeInPaths = true))) - } - - /** - * Given a URL for a profile, return a sequence of definitions for all elements of the resource type indicated by this profile. - * - * @param profileUrl The URL of the profile to be simplified. - * @param profileVersion Version of the profile to be simplified. (profileUrl, version) tuple provides the uniqueness. The same profileUrl can have multiple versions. - * @param withResourceTypeInPaths If true, the resource type of the given profileUrl will be added to the beginning of all FHIR paths of the inner elements. - * @return - */ - def simplifyStructureDefinition(profileUrl: String, profileVersion: Option[String], withResourceTypeInPaths: Boolean = false): Seq[SimpleStructureDefinition] = { - - /** - * Recursive helper function to create the SimpleStructureDefinition sequence for a given profile, carrying the ElementRestrictions to inner elements. - * - * @param profileUrl URL of a FHIR profile which can be empty. If empty, only restrictionsFromParentElement will be considered while creating the element definitions. - * @param profileVersion Version of the FHIR profile. - * @param parentPath FHIRPath until now. The SimpleStructureDefinitions will be created under the given parentPath. - * @param restrictionsFromParentElement ElementRestrictions from parent profiles. - * @param accumulatingTypeUrls Data types throughout the recursive chain so that recursion can stop if a loop over the data types exists. - * @return - */ - def simplifier(profileUrl: Option[String], profileVersion: Option[String], parentPath: Option[String], restrictionsFromParentElement: Seq[(String, ElementRestrictions)], accumulatingTypeUrls: CountingMap[String]): Seq[SimpleStructureDefinition] = { - - /** - * Helper function to create a single SimpleStructureDefinition for a slice of a field (e.g., valueQuantity under value[x]) - * together with inner elements (by calling the simplifier recursively) - * - * @param fieldName The name of the parent field (e.g., coding) - * @param sliceName The name of the slice under the parent field (e.g., aicNonMotorSymptom) - * @param parentPath FHIRPath under which the definition will be created. - * @param profileUrlForDataType For non-choice slices, URL of the profile for the data type (if exists) - * @param restrictionsOnSlicesOfField ElementRestrictions on the slice - * @param accumulatingTypeUrls Accumulating data types to break the loop in the recursion - * @param dataTypeWithProfiles For choice field slices, the data type for this slice - * @return - */ - def createDefinitionWithElements(fieldName: String, sliceName: String, parentPath: Option[String], profileUrlForDataType: Option[String], - restrictionsOnSlicesOfField: Seq[(String, ElementRestrictions)], restrictionsOnChildrenOfField: Seq[(String, ElementRestrictions)], - accumulatingTypeUrls: CountingMap[String], dataTypeWithProfiles: Option[DataTypeWithProfiles] = None): SimpleStructureDefinition = { - // Partition the restriction into 2: (i) directly on that field (e.g., value[x]:valueQuantity) and (ii) on the children (e.g., value[x]:valueQuantity.system) - val (restrictionsOnSliceField, restrictionsOnChildrenOfSlices) = restrictionsOnSlicesOfField.partition(_._1 == s"$fieldName:$sliceName") - val typeRestrictionForThisTypeField = dataTypeWithProfiles.map(dt => - ElementRestrictions(path = s"$fieldName:$sliceName", restrictions = Map(ConstraintKeys.DATATYPE -> TypeRestriction(Seq(dt.dataType -> Seq.empty[String]))), sliceName = None, contentReference = None)) - // for the sliced elements, their paths should include the current element. Therefore, pass the field name with parent path to generateSimpleDefinition function - val createdChoiceTypeElement = generateSimpleDefinition(sliceName, Some(parentPath.map(pp => s"$pp.$fieldName").getOrElse(fieldName)), restrictionsOnSliceField.map(_._2) ++ typeRestrictionForThisTypeField) - if (createdChoiceTypeElement.isPrimitive) createdChoiceTypeElement - else { - val navigatedRestrictionsOnChildrenOfSlices = restrictionsOnChildrenOfSlices - .filter(res => { - val searchText = s"$fieldName:$sliceName" - // startsWith check is not enough when there are some slices starting with the same word such as qtInterval and qtIntervalCorrected - // therefore, we check the first character following searchText to determine correct restrictions for the slice - res._1.startsWith(searchText) && res._1.substring(searchText.length).charAt(0).==('.') - }) // Take the children only for this slice, other slices will also be in restrictionsOnChildrenOfSlices because of our use of partition (above) - .map(navigateFhirPathFromField(s"$fieldName:$sliceName", _)) - val navigatedRestrictionsOnChildrenOfField = restrictionsOnChildrenOfField - .map(navigateFhirPathFromField(fieldName, _)) - val navigatedRestrictionsOnChildren = - (navigatedRestrictionsOnChildrenOfSlices ++ navigatedRestrictionsOnChildrenOfField) // Merge the restrictions on children of the slices and children of the field itself. - .groupBy(_._1) // Group by using the paths (field names) - .map { kv => // Iterate over the groups to merge the ElementRestrictions on each field - kv._1 -> kv._2.map(_._2) // Create the tuple of (fieldName, ElementRestrictions) - .reduceLeft((er1, er2) => er1.addNewRestrictions(er2.restrictions)) // Merge the restrictions going from left-to-right by adding the right's restrictions on top of left's. - }.toSeq - val dataTypeOfCreatedTypeElement = createdChoiceTypeElement.getProfileUrlForDataType - val definitionsOfChoiceTypeElementsChildren = - simplifier( - profileUrl = if (dataTypeOfCreatedTypeElement.isDefined) dataTypeOfCreatedTypeElement else profileUrlForDataType, - profileVersion = None, - parentPath = Some(createdChoiceTypeElement.path), - restrictionsFromParentElement = navigatedRestrictionsOnChildren, - accumulatingTypeUrls = accumulatingTypeUrls.apply(profileUrl)) - createdChoiceTypeElement.withElements(definitionsOfChoiceTypeElementsChildren) - } - } - - // Start of the simplifier method - if (profileUrl.isDefined && - // Stop the recursion if the profile is already visited twice (to avoid infinite recursion) - (accumulatingTypeUrls.get(profileUrl.get)>= 2 || - // The following is a hack to decrease the size of the SimpleStructureDefinition because Identifier type occurs in many places in the FHIR spec. - // If more examples like this are found, either we should find a better way to stop the recursion or we should add them here - (profileUrl.get == "http://hl7.org/fhir/StructureDefinition/Identifier") && accumulatingTypeUrls.get(profileUrl.get) >= 1)) { - // Stop the recursion here because we are entering into a recursive type chain (e.g., Identifier -> Reference -> Identifier) - Seq.empty[SimpleStructureDefinition] - } else { - val profileRestrictionsSeq: Seq[ProfileRestrictions] = profileUrl.map(url => fhirConfig.findProfileChain(url, profileVersion)).getOrElse(Seq.empty[ProfileRestrictions]) - val elementRestrictionsFromProfile = profileRestrictionsSeq - .flatMap { pr => - // Make a list of all ElementRestrictions (respect their order) - // But, filter out extension, modifierExtension and id fields if they come from Element and BackboneElement profiles. Otherwise, the SimpleStructureDefinition becomes huge! - if (pr.url.endsWith("Element") || pr.url.endsWith("BackboneElement")) - pr.elementRestrictions.filterNot(er => er._1 == "extension" || er._1 == "modifierExtension" || er._1 == "id") - // remove the extensions coming from DomainResource. SimpleStructureDefinition will include "extension" element - // iff the profile has some slices for it. - else if (pr.url.endsWith("DomainResource")) - pr.elementRestrictions.filterNot(er => er._1 == "extension"|| er._1 == "modifierExtension") - else - pr.elementRestrictions - } - val allRestrictions = restrictionsFromParentElement ++ elementRestrictionsFromProfile - - import io.tofhir.server.util.GroupByOrdered._ - - val groupedFieldRestrictions = allRestrictions - .filterNot(r => r._1.split('.').head.contains(":")) // Eliminate the slice definitions such as coding:aic or value[x]:valueQuantity, but not code.coding:aic - .groupByASequenceOrdered(r => r._1.split('.').head) // Group by immediate field name (e.g., group {code, code.coding.system, code.coding.value} together) - - groupedFieldRestrictions.flatMap { // flatMap to get rid of the None objects in the sequence - case (fieldName, restrictionsOnFieldAndItsChildren) => - val (restrictionsOnField, restrictionsOnChildren) = restrictionsOnFieldAndItsChildren.partition(_._1 == fieldName) - - // Create the SimpleStructureDefinition for this fieldName - val createdElementDefinition = generateSimpleDefinition(fieldName, parentPath, restrictionsOnField.map(_._2)) - - if(createdElementDefinition.maxCardinality.contains(0)) { - Option.empty[SimpleStructureDefinition] // Do not put an element into our result if it is removed (max-cardinality = 0) in the profile definitions. - } - else if (createdElementDefinition.isPrimitive && !createdElementDefinition.isChoiceRoot) { - // For choice fields (e.g., value[x]), if it has a single simple type (e.g., boolean), then we count is as primitive. - // That's why the extra check on whether it is choiceRoot or not. - Some(createdElementDefinition) - } else { - val restrictionsOnSlicesOfField = allRestrictions.filter(t => t._1.startsWith(s"$fieldName:")) - if (createdElementDefinition.isChoiceRoot) { - // Add the complex types of the choice as restrictions under this field so that they are created as elements - val definitionsOfChoiceTypes: Seq[SimpleStructureDefinition] = createdElementDefinition.dataTypes match { - case Some(typesWithProfiles) => - if (restrictionsOnSlicesOfField.isEmpty) { - // If there are not restrictions for any of the data types of the choice, then do not populate them. - // The client can make further requests to retrieve simplified definitions of them. - Seq.empty[SimpleStructureDefinition] - } else { - typesWithProfiles.map { dt => - val choiceTypeFieldName = s"${fieldName.replace("[x]", "")}${dt.dataType.capitalize}" // Create the field name such as valueQuantity, valueBoolean etc. - createDefinitionWithElements(fieldName, choiceTypeFieldName, parentPath, None, restrictionsOnSlicesOfField, Seq.empty, accumulatingTypeUrls, Some(dt)) - } - } - case None => throw new IllegalArgumentException("A choice root cannot exist without any data types!!") - } - Some(createdElementDefinition.withElements(definitionsOfChoiceTypes)) - } else if (createdElementDefinition.sliceDefinition.isDefined) { - val sliceNames = restrictionsOnSlicesOfField.collect { - // consider only the direct slices of field - case t if t._2.sliceName.isDefined && t._1.contentEquals(s"$fieldName:${t._2.sliceName.get}") => t._2.sliceName.get - }.distinct // Read the following explanation - // Same slice (using the same slice name) can be extended/restricted within the profile definitions tree. - // We take only one of these slices at this point, but we send the whole restrictions to createDefinitionWithElements function - // in which we partition those restrictions w.r.t the slice name which allows us to take all restrictions on the slice. - // This means we process the deepest restrictions on the slice and apply in the order of the profile tree as we handle in generateSimpleDefinition function. - val definitionsOfSlices: Seq[SimpleStructureDefinition] = sliceNames.map { sliceFieldName => - createDefinitionWithElements(fieldName, sliceFieldName, parentPath, createdElementDefinition.getProfileUrlForDataType, restrictionsOnSlicesOfField, restrictionsOnChildren, accumulatingTypeUrls) - } - val createdNoSliceElement = generateSimpleDefinition("No Slice", parentPath, Seq.empty[ElementRestrictions]) - val navigatedRestrictionsOnChildren = restrictionsOnChildren.map(navigateFhirPathFromField(fieldName, _)) - val createdNoSliceElementWithChildren = createdNoSliceElement - .withElements(simplifier(createdElementDefinition.getProfileUrlForDataType, None, Some(createdNoSliceElement.path), navigatedRestrictionsOnChildren, accumulatingTypeUrls.apply(profileUrl))) - Some(createdElementDefinition.withElements(createdNoSliceElementWithChildren +: definitionsOfSlices)) - } else { - val navigatedRestrictionsOnChildren = restrictionsOnChildren.map(navigateFhirPathFromField(fieldName, _)) - val definitionsOfChildren = - simplifier(profileUrl = createdElementDefinition.getProfileUrlForDataType, - profileVersion = None, - parentPath = Some(createdElementDefinition.path), - restrictionsFromParentElement = navigatedRestrictionsOnChildren, - accumulatingTypeUrls = accumulatingTypeUrls.apply(profileUrl)) - Some(createdElementDefinition.withElements(definitionsOfChildren)) - } - } - } - } - } - - // Start of the simplifyStructureDefinition method - val simplifiedElementsOfProfile = simplifier( - profileUrl = Some(profileUrl), - profileVersion = profileVersion, - parentPath = if (withResourceTypeInPaths) fhirConfig.findResourceType(profileUrl) else Option.empty[String], - restrictionsFromParentElement = Seq.empty[(String, ElementRestrictions)], - accumulatingTypeUrls = CountingMap.empty[String]) - - // Handle the content references to populate them. - populateContentReferences(simplifiedElementsOfProfile, simplifiedElementsOfProfile) - } - - /** - * Recursively iterate over the given sequence of SimpleStructureDefinitions and resolve the definitions with a - * reference to another definition. - * - * @param rootElements - * @param children - * @return The updated sequence of definitions - */ - private def populateContentReferences(rootElements: Seq[SimpleStructureDefinition], children: Seq[SimpleStructureDefinition]): Seq[SimpleStructureDefinition] = { - - /** - * Find the element at path. - * - * @param path FHIRPath of the element to be fetched. - * @param elements Sequence of SimpleStructureDefinition elemenets - * @return The element at path or None if cannot be found. - */ - def findElementDefinition(path: List[String], elements: Seq[SimpleStructureDefinition]): Option[SimpleStructureDefinition] = { - path match { - case head :: Nil => - elements.find(_.id == head) - case head :: tail => - elements.find(_.id == head) flatMap { matchingElement => - if (matchingElement.elements.isEmpty) { - throw new IllegalArgumentException(s"Reached at the end of elements! Cannot go further for the remaining path:${tail.mkString(".")}") - } else { - findElementDefinition(tail, matchingElement.elements.get) - } - } - case Nil => - throw new IllegalArgumentException("A path must exist to find an element definition within Seq[SimpleStructureDefinition]") - } - } - - children.map { elementDefinition => - if (elementDefinition.referringTo.isEmpty) { - elementDefinition.elements match { - case Some(elementCh) => elementDefinition.withElements(populateContentReferences(rootElements, elementCh)) - case None => elementDefinition - } - } else { - val referencePath = elementDefinition.referringTo.get.split('.').toList - val referencedElement = findElementDefinition(referencePath, rootElements) - referencedElement match { - case Some(el) => elementDefinition.withReferencedContent(el) - case None => throw new IllegalStateException(s"Reference path of the element does not refer to any definition. Element:${elementDefinition.id} -- referencePath:${elementDefinition.referringTo.get}") - } - } - } - - } - - /** - * Given the (path, ElementRestrictions) tuple on a field, navigate 1-step on the FHIR path. - * - * @param fieldName - * @param restriction - * @return - */ - private def navigateFhirPathFromField(fieldName: String, restriction: (String, ElementRestrictions)): (String, ElementRestrictions) = { - val path = restriction._1 - val arr = path.split('.') - if (arr.head != fieldName) { - throw new IllegalStateException(s"This path $path does not belong to the element $fieldName") - } - val newPath = arr.takeRight(arr.length - 1).mkString(".") - if (newPath.isEmpty) { - throw new IllegalStateException(s"There is a child path with $path in this field:$fieldName which navigates to the field itself, not to any child element!") - } - newPath -> restriction._2 - } - - /** - * Given the field name, FHIRPath until this field (if exists), and ElementRestrictions directly on this field, create a SimpleStructureDefinition representing this field. - * - * @param fieldName - * @param parentPath - * @param restrictionsOnField - * @return - */ - private def generateSimpleDefinition(fieldName: String, parentPath: Option[String], restrictionsOnField: Seq[ElementRestrictions]): SimpleStructureDefinition = { - var dataTypes: Option[Seq[DataTypeWithProfiles]] = None - var isArray: Boolean = false - val isChoiceRoot: Boolean = fieldName.endsWith("[x]") - var sliceDefinition: Option[SliceDefinition] = None - var sliceName: Option[String] = None - var minCardinality: Option[Int] = None - var maxCardinality: Option[Int] = None - var valueSetUrl: Option[String] = None - var isValueSetBindingRequired: Option[Boolean] = None - var referencableProfiles: Option[Seq[String]] = None - var constraintDefinitions: Seq[ConstraintDefinition] = Seq.empty - var fixedValue: Option[String] = None - var patternValue: Option[String] = None - val fhirPath: String = parentPath match { - case None => fieldName - case Some(path) => s"$path.$fieldName" - } - var contentReference: Option[String] = None - var shortDescription: Option[String] = None - var definition: Option[String] = None - var comment: Option[String] = None - - restrictionsOnField.foreach { elementRestrictions => - shortDescription = elementRestrictions.metadata.flatMap(md => md.short) - definition = elementRestrictions.metadata.flatMap(md => md.definition) - comment = elementRestrictions.metadata.flatMap(md => md.comment) - - if (elementRestrictions.contentReference.isDefined) { - if (contentReference.isEmpty) { - contentReference = elementRestrictions.contentReference - } - } - - if (elementRestrictions.slicing.isDefined) { - // This is a slice definition for this fieldName, process the most upper-level one only. - if (sliceDefinition.isEmpty) { - val fhirSlicing = elementRestrictions.slicing.get - sliceDefinition = Some(SliceDefinition( - fhirSlicing.discriminators.map(d => SliceDiscriminator(d._1, d._2)), - fhirSlicing.ordered, - fhirSlicing.rule)) - } - } - - if (elementRestrictions.sliceName.isDefined) { - if (sliceName.isEmpty) { - sliceName = Some(elementRestrictions.sliceName.get) - } - } - - // Iterate over the restriction to process the constraints by respecting the order given the type of the restriction. - elementRestrictions.restrictions.values.toSeq.map { - case typeRestriction: TypeRestriction => - if (dataTypes.isEmpty) { - dataTypes = Some(typeRestriction.dataTypesAndProfiles.map(DataTypeWithProfiles(_))) - if (!isChoiceRoot) { - if (typeRestriction.dataTypesAndProfiles.length > 1) { - throw new NotImplementedError(s"Although the field is not a choice of data types, there are more than one data types in the TypeRestriction of this field:$fieldName. " + - s"List of type restrictions:${typeRestriction.dataTypesAndProfiles.map(tr => tr._1 -> tr._2.mkString)}") - } - } - } - case arrayRestriction: ArrayRestriction => - isArray = arrayRestriction.isArray // Assign the last one after the traversal of all restrictions within the ElementRestrictions - case minRestriction: CardinalityMinRestriction => - if (minCardinality.isEmpty) { - minCardinality = Some(minRestriction.n) - } - case maxRestriction: CardinalityMaxRestriction => - if (maxCardinality.isEmpty) { - maxCardinality = Some(maxRestriction.n) - } - case codeBindingRestriction: CodeBindingRestriction => - if (valueSetUrl.isEmpty) { - valueSetUrl = Some(codeBindingRestriction.valueSetUrl) - isValueSetBindingRequired = Some(codeBindingRestriction.isRequired) - } - case referenceRestrictions: ReferenceRestrictions => - if (referencableProfiles.isEmpty) { - referencableProfiles = Some(referenceRestrictions.targetProfiles) - } - case constraintsRestriction: ConstraintsRestriction => - // TODO: Shall we accumulate these constraints or shall we only get the last one? - constraintDefinitions = constraintDefinitions ++ constraintsRestriction.fhirConstraints.map(fc => ConstraintDefinition(fc.key, fc.desc, fc.isWarning)) - case fixedOrPatternRestriction: FixedOrPatternRestriction => - if (fixedOrPatternRestriction.isFixed) { - if (fixedValue.isEmpty) { - fixedValue = Some(Serialization.write(fixedOrPatternRestriction.fixedValue)) - } - } else { - if (patternValue.isEmpty) { - patternValue = Some(Serialization.write(fixedOrPatternRestriction.fixedValue)) - } - } - case unk => - throw new IllegalArgumentException(s"Unknown FhirRestriction! ${unk.toString}") - } - } - - if (sliceDefinition.isDefined && sliceName.isDefined) { - throw new IllegalStateException(s"A field cannot be a slice definition and a part of a slice (with slice name) at the same time!!! FieldName:$fieldName") - } - - val isPrimitive = dataTypes.isDefined && dataTypes.get.length == 1 && Character.isLowerCase(dataTypes.get.head.dataType.head) - - SimpleStructureDefinition(id = fieldName, - path = fhirPath, - dataTypes = dataTypes, - isPrimitive = isPrimitive, - isChoiceRoot = isChoiceRoot, - isArray = isArray, - minCardinality = minCardinality.getOrElse(0), - maxCardinality = maxCardinality, - boundToValueSet = valueSetUrl, - isValueSetBindingRequired = isValueSetBindingRequired, - referencableProfiles = referencableProfiles, - constraintDefinitions = if (constraintDefinitions.isEmpty) None else Some(constraintDefinitions), - sliceDefinition = sliceDefinition, - sliceName = sliceName, - fixedValue = fixedValue, - patternValue = patternValue, - referringTo = contentReference, - short = shortDescription, - definition = definition, - comment = comment, - elements = None) - } - - /** - * Helper function to create the root element (1st element of the Element definitions which is dropped by IFhirFoundationParser. - * - * @param resourceType - * @return - */ - private def createRootElement(resourceType: String): SimpleStructureDefinition = { - SimpleStructureDefinition( - id = resourceType, - path = resourceType, - dataTypes = Some(Seq(DataTypeWithProfiles("Element", None))), - isPrimitive = false, - isChoiceRoot = false, - isArray = false, - minCardinality = 0, maxCardinality = None, - boundToValueSet = None, - isValueSetBindingRequired = None, - referencableProfiles = None, - constraintDefinitions = None, - sliceDefinition = None, - sliceName = None, - fixedValue = None, patternValue = None, - referringTo = None, - short = None, - definition = None, - comment = None, - elements = None) - } -} diff --git a/tofhir-server/src/main/scala/io/tofhir/server/service/fhir/base/FhirBaseProfilesService.scala b/tofhir-server/src/main/scala/io/tofhir/server/service/fhir/base/FhirBaseProfilesService.scala deleted file mode 100644 index a1805f56e..000000000 --- a/tofhir-server/src/main/scala/io/tofhir/server/service/fhir/base/FhirBaseProfilesService.scala +++ /dev/null @@ -1,89 +0,0 @@ -package io.tofhir.server.service.fhir.base - -import io.onfhir.api.FHIR_FOUNDATION_RESOURCES.FHIR_STRUCTURE_DEFINITION -import io.onfhir.api.Resource -import io.onfhir.api.util.FHIRUtil -import io.onfhir.api.validation.ProfileRestrictions -import io.onfhir.config.{BaseFhirServerConfigurator, FSConfigReader} -import io.onfhir.r4.parsers.R4Parser -import io.tofhir.common.model.SchemaDefinition -import io.tofhir.common.util.HashUtil -import io.tofhir.engine.util.MajorFhirVersion -import io.tofhir.server.service.fhir.SimpleStructureDefinitionService - -/** - * Abstract class representing a service for managing FHIR base profiles. - * Provides methods to retrieve resource types and profile schemas. - */ -abstract class FhirBaseProfilesService { - /** A configuration reader for reading FHIR-related configuration. */ - val configReader: FSConfigReader - /** A configurator for setting up the FHIR server platform. */ - val fhirServerConfigurator: BaseFhirServerConfigurator - /** A sequence of base profile resources read from a configuration file. */ - private lazy val baseProfileResources = configReader.readStandardBundleFile("profiles-resources.json", Set(FHIR_STRUCTURE_DEFINITION)) - /** A sequence of resource types derived from base profile resources. */ - lazy val resourceTypes: Seq[String] = baseProfileResources.flatMap(getTypeFromStructureDefinition) - /** A service to create SchemaDefinitions from the base profiles. */ - lazy val simpleStructureDefinitionService: SimpleStructureDefinitionService = new SimpleStructureDefinitionService(fhirServerConfigurator.initializePlatform(configReader)) - - /** - * Retrieves a sequence of schema definitions based on the given profile URLs. - * - * @param url A comma-separated string of profile URLs. - * @return A sequence of schema definitions corresponding to the provided profile URLs. - */ - def getProfile(url: String): Seq[SchemaDefinition] = { - // split the provided URL string into a set of individual profile URLs - val profileUrls = url.split(",").toSet - - baseProfileResources - // filter the base profile resources to include only those with URLs matching the provided profile URLs - .filter(p => profileUrls.contains(FHIRUtil.extractValueOption[String](p, "url").get)) - // map each filtered resource to a schema definition - .map(resource => { - // parse the structure definition from the resource - val structureDefinition: ProfileRestrictions = new R4Parser().parseStructureDefinition(resource) - // convert the parsed structure definition to a schema definition - simpleStructureDefinitionService.convertToSchemaDefinition(structureDefinition.id.getOrElse(HashUtil.md5Hash(structureDefinition.url)), structureDefinition) - }) - } - - /** - * Extracts the type from a given structure definition resource. - * - * @param structureDefinition The structure definition resource. - * @return An optional string representing the resource type, or None if the resource is abstract. - */ - private def getTypeFromStructureDefinition(structureDefinition: Resource): Option[String] = { - if (FHIRUtil.extractValueOption[Boolean](structureDefinition, "abstract").get) - None - else - Some(FHIRUtil.extractValueOption[String](structureDefinition, "type").get) - } -} - -/** - * Companion object for FhirBaseProfilesService providing factory methods to create instances - * based on the FHIR version. - */ -object FhirBaseProfilesService { - /** Lazily initialized instance for FHIR R4 base profiles service. */ - lazy private val r4FhirBaseProfilesService = new R4FhirBaseProfilesService - /** Lazily initialized instance for FHIR R5 base profiles service. */ - lazy private val r5FhirBaseProfilesService = new R5FhirBaseProfilesService - - /** - * Factory method to create a FhirBaseProfilesService instance based on the FHIR version. - * - * @param fhirVersion The FHIR version (e.g., "R4", "R5"). - * @return An instance of FhirBaseProfilesService for the specified FHIR version. - */ - def apply(fhirVersion: String): FhirBaseProfilesService = { - fhirVersion match { - case MajorFhirVersion.R4 => r4FhirBaseProfilesService - case MajorFhirVersion.R5 => r5FhirBaseProfilesService - case _ => throw new NotImplementedError() - } - } -} \ No newline at end of file diff --git a/tofhir-server/src/main/scala/io/tofhir/server/service/fhir/base/R4FhirBaseProfilesService.scala b/tofhir-server/src/main/scala/io/tofhir/server/service/fhir/base/R4FhirBaseProfilesService.scala deleted file mode 100644 index 70f2a6dcd..000000000 --- a/tofhir-server/src/main/scala/io/tofhir/server/service/fhir/base/R4FhirBaseProfilesService.scala +++ /dev/null @@ -1,13 +0,0 @@ -package io.tofhir.server.service.fhir.base - -import io.onfhir.config.{BaseFhirServerConfigurator, FSConfigReader} -import io.onfhir.r4.config.FhirR4Configurator -import io.tofhir.engine.util.MajorFhirVersion - -/** - * Implementation of FhirBaseProfilesService for FHIR R4. - */ -class R4FhirBaseProfilesService extends FhirBaseProfilesService { - override val configReader: FSConfigReader = new FSConfigReader(fhirVersion = MajorFhirVersion.R4) - override val fhirServerConfigurator: BaseFhirServerConfigurator = new FhirR4Configurator() -} diff --git a/tofhir-server/src/main/scala/io/tofhir/server/service/fhir/base/R5FhirBaseProfilesService.scala b/tofhir-server/src/main/scala/io/tofhir/server/service/fhir/base/R5FhirBaseProfilesService.scala deleted file mode 100644 index efc6aa46f..000000000 --- a/tofhir-server/src/main/scala/io/tofhir/server/service/fhir/base/R5FhirBaseProfilesService.scala +++ /dev/null @@ -1,13 +0,0 @@ -package io.tofhir.server.service.fhir.base - -import io.onfhir.config.{BaseFhirServerConfigurator, FSConfigReader} -import io.onfhir.r5.config.FhirR5Configurator -import io.tofhir.engine.util.MajorFhirVersion - -/** - * Implementation of FhirBaseProfilesService for FHIR R5. - */ -class R5FhirBaseProfilesService extends FhirBaseProfilesService { - override val configReader: FSConfigReader = new FSConfigReader(fhirVersion = MajorFhirVersion.R5) - override val fhirServerConfigurator: BaseFhirServerConfigurator = new FhirR5Configurator() -} diff --git a/tofhir-server/src/main/scala/io/tofhir/server/util/GroupByOrdered.scala b/tofhir-server/src/main/scala/io/tofhir/server/util/GroupByOrdered.scala deleted file mode 100644 index 9bc240492..000000000 --- a/tofhir-server/src/main/scala/io/tofhir/server/util/GroupByOrdered.scala +++ /dev/null @@ -1,40 +0,0 @@ -package io.tofhir.server.util - -import scala.collection.mutable -import scala.language.implicitConversions - -/** - * Object to be imported so that Sequences (Seq) have a new functions called groupByASequenceOrdered - * which is a groupBy implementation that keeps the order of Sequence entries while creating the Map of the - * groups. The return type is the Seq of those Map entries. - */ -object GroupByOrdered { - - class OrderedGrouped[A](xs: Seq[A]) { - - /** - * Helper, parent function to handle type conversion from immutable.Seq to collection.Seq - */ - def groupByASequenceOrdered[K](f: A => K): Seq[(K, Seq[A])] = { - _groupByASequenceOrdered(f).toSeq.map(s => s._1 -> s._2.toSeq) - } - - /** - * Implementation of groupBy with LinedHashMap and ArrayBuffer. - */ - private def _groupByASequenceOrdered[K](f: A => K): collection.Seq[(K, collection.Seq[A])] = { - val m = mutable.LinkedHashMap.empty[K, collection.Seq[A]].withDefault(_ => new mutable.ArrayBuffer[A]) - xs.foreach { x => - val k = f(x) - m(k) = m(k) :+ x - } - m.toSeq - } - - } - - /** - * The method added to Seq types. - */ - implicit def groupByOrdered[A](seq: Seq[A]): OrderedGrouped[A] = new OrderedGrouped[A](seq) -} diff --git a/tofhir-server/src/test/resources/application.conf b/tofhir-server/src/test/resources/application.conf index 80400edcd..f84500e2f 100644 --- a/tofhir-server/src/test/resources/application.conf +++ b/tofhir-server/src/test/resources/application.conf @@ -103,6 +103,8 @@ fhir = { # fixed-token = "XXX" } + # A path to a context file/directory from where profiles, value sets and code systems reading should start. + context-path = "test-context-conf" # Path to the zip file or folder that includes the FHIR resource and data type profile definitions (FHIR StructureDefinition) to be served by toFHIR webserver so that mappings can be performed accordingly. profiles-path = "profiles" diff --git a/tofhir-server/src/test/scala/io/tofhir/server/BaseEndpointTest.scala b/tofhir-server/src/test/scala/io/tofhir/server/BaseEndpointTest.scala index 9692cf843..45a06f54a 100644 --- a/tofhir-server/src/test/scala/io/tofhir/server/BaseEndpointTest.scala +++ b/tofhir-server/src/test/scala/io/tofhir/server/BaseEndpointTest.scala @@ -4,13 +4,13 @@ import akka.http.scaladsl.model.{ContentTypes, HttpEntity, StatusCodes} import akka.http.scaladsl.server.Route import akka.http.scaladsl.testkit.ScalatestRouteTest import io.onfhir.client.OnFhirNetworkClient -import io.tofhir.common.model.Json4sSupport.formats +import io.onfhir.definitions.common.model.Json4sSupport.formats import io.tofhir.engine.config.ToFhirEngineConfig import io.tofhir.engine.util.FileUtils import io.tofhir.server.config.RedCapServiceConfig import io.tofhir.server.common.config.WebServerConfig import io.tofhir.server.endpoint.{ProjectEndpoint, ToFhirServerEndpoint} -import io.tofhir.server.fhir.FhirDefinitionsConfig +import io.onfhir.definitions.resource.fhir.FhirDefinitionsConfig import io.tofhir.server.model.Project import io.tofhir.server.repository.project.ProjectFolderRepository import org.json4s.jackson.JsonMethods diff --git a/tofhir-server/src/test/scala/io/tofhir/server/endpoint/FhirDefinitionsEndpointTest.scala b/tofhir-server/src/test/scala/io/tofhir/server/endpoint/FhirDefinitionsEndpointTest.scala index d76e09ad3..4cccb0cfb 100644 --- a/tofhir-server/src/test/scala/io/tofhir/server/endpoint/FhirDefinitionsEndpointTest.scala +++ b/tofhir-server/src/test/scala/io/tofhir/server/endpoint/FhirDefinitionsEndpointTest.scala @@ -1,12 +1,13 @@ package io.tofhir.server.endpoint import akka.http.scaladsl.model.{ContentTypes, HttpEntity, StatusCodes} +import io.onfhir.definitions.resource.endpoint.FhirDefinitionsEndpoint +import io.onfhir.definitions.resource.endpoint.FhirDefinitionsEndpoint.{DefinitionsQuery, QUERY_PARAM_PROFILE, QUERY_PARAM_Q} import io.tofhir.OnFhirTestContainer -import io.tofhir.common.model.Json4sSupport.formats -import io.tofhir.common.model.{SchemaDefinition, SimpleStructureDefinition} +import io.onfhir.definitions.common.model.Json4sSupport.formats +import io.onfhir.definitions.common.model.{SchemaDefinition, SimpleStructureDefinition} import io.tofhir.engine.util.MajorFhirVersion import io.tofhir.server.BaseEndpointTest -import io.tofhir.server.endpoint.FhirDefinitionsEndpoint.{DefinitionsQuery, QUERY_PARAM_PROFILE, QUERY_PARAM_Q} import org.json4s.JsonAST.{JString, JValue} import org.json4s._ import org.json4s.jackson.JsonMethods diff --git a/tofhir-server/src/test/scala/io/tofhir/server/endpoint/FileSystemTreeStructureEndpointTest.scala b/tofhir-server/src/test/scala/io/tofhir/server/endpoint/FileSystemTreeStructureEndpointTest.scala index 364b7e794..75d705d02 100644 --- a/tofhir-server/src/test/scala/io/tofhir/server/endpoint/FileSystemTreeStructureEndpointTest.scala +++ b/tofhir-server/src/test/scala/io/tofhir/server/endpoint/FileSystemTreeStructureEndpointTest.scala @@ -4,7 +4,7 @@ import akka.http.scaladsl.model.StatusCodes import io.tofhir.server.BaseEndpointTest import io.tofhir.server.endpoint.FileSystemTreeStructureEndpoint import io.tofhir.server.model.FilePathNode -import io.tofhir.common.model.Json4sSupport.formats +import io.onfhir.definitions.common.model.Json4sSupport.formats import io.tofhir.server.util.FileOperations import org.json4s.jackson.JsonMethods diff --git a/tofhir-server/src/test/scala/io/tofhir/server/endpoint/MappingContextEndpointTest.scala b/tofhir-server/src/test/scala/io/tofhir/server/endpoint/MappingContextEndpointTest.scala index ab8cbcfba..d66a78934 100644 --- a/tofhir-server/src/test/scala/io/tofhir/server/endpoint/MappingContextEndpointTest.scala +++ b/tofhir-server/src/test/scala/io/tofhir/server/endpoint/MappingContextEndpointTest.scala @@ -1,7 +1,7 @@ package io.tofhir.server.endpoint import akka.http.scaladsl.model.{ContentTypes, HttpEntity, Multipart, StatusCodes} -import io.tofhir.common.model.Json4sSupport.formats +import io.onfhir.definitions.common.model.Json4sSupport.formats import io.tofhir.engine.util.FileUtils import io.tofhir.server.BaseEndpointTest import io.tofhir.server.endpoint.{MappingContextEndpoint, ProjectEndpoint} diff --git a/tofhir-server/src/test/scala/io/tofhir/server/endpoint/MappingExecutionEndpointTest.scala b/tofhir-server/src/test/scala/io/tofhir/server/endpoint/MappingExecutionEndpointTest.scala index 6b59a0a4f..e6b3b130a 100644 --- a/tofhir-server/src/test/scala/io/tofhir/server/endpoint/MappingExecutionEndpointTest.scala +++ b/tofhir-server/src/test/scala/io/tofhir/server/endpoint/MappingExecutionEndpointTest.scala @@ -7,7 +7,7 @@ import akka.testkit.TestDuration import io.onfhir.api.Resource import io.onfhir.api.client.FhirBatchTransactionRequestBuilder import io.tofhir.OnFhirTestContainer -import io.tofhir.common.model.SchemaDefinition +import io.onfhir.definitions.common.model.SchemaDefinition import io.tofhir.engine.config.ToFhirConfig import io.tofhir.engine.data.write.FileSystemWriter.SinkContentTypes import io.tofhir.engine.model._ diff --git a/tofhir-server/src/test/scala/io/tofhir/server/endpoint/MetadataEndpointTest.scala b/tofhir-server/src/test/scala/io/tofhir/server/endpoint/MetadataEndpointTest.scala index 11a231cb2..13c0d7e8e 100644 --- a/tofhir-server/src/test/scala/io/tofhir/server/endpoint/MetadataEndpointTest.scala +++ b/tofhir-server/src/test/scala/io/tofhir/server/endpoint/MetadataEndpointTest.scala @@ -5,7 +5,7 @@ import io.tofhir.server.BaseEndpointTest import io.tofhir.server.endpoint.MetadataEndpoint import io.tofhir.server.model.Metadata import org.json4s.jackson.JsonMethods -import io.tofhir.common.model.Json4sSupport.formats +import io.onfhir.definitions.common.model.Json4sSupport.formats import io.tofhir.engine.config.ToFhirConfig import java.io.File diff --git a/tofhir-server/src/test/scala/io/tofhir/server/endpoint/ProjectEndpointTest.scala b/tofhir-server/src/test/scala/io/tofhir/server/endpoint/ProjectEndpointTest.scala index cfdc865b6..3340f8809 100644 --- a/tofhir-server/src/test/scala/io/tofhir/server/endpoint/ProjectEndpointTest.scala +++ b/tofhir-server/src/test/scala/io/tofhir/server/endpoint/ProjectEndpointTest.scala @@ -1,8 +1,8 @@ package io.tofhir.server.endpoint import akka.http.scaladsl.model.{ContentTypes, StatusCodes} -import io.tofhir.common.model.Json4sSupport.formats -import io.tofhir.common.model.SchemaDefinition +import io.onfhir.definitions.common.model.Json4sSupport.formats +import io.onfhir.definitions.common.model.SchemaDefinition import io.tofhir.engine.util.FileUtils import io.tofhir.server.BaseEndpointTest import io.tofhir.server.model.{Project, ProjectEditableFields} diff --git a/tofhir-server/src/test/scala/io/tofhir/server/endpoint/SchemaEndpointTest.scala b/tofhir-server/src/test/scala/io/tofhir/server/endpoint/SchemaEndpointTest.scala index 3c3de33b6..a80d0391b 100644 --- a/tofhir-server/src/test/scala/io/tofhir/server/endpoint/SchemaEndpointTest.scala +++ b/tofhir-server/src/test/scala/io/tofhir/server/endpoint/SchemaEndpointTest.scala @@ -6,7 +6,7 @@ import akka.http.scaladsl.testkit.RouteTestTimeout import io.onfhir.api.client.FhirBatchTransactionRequestBuilder import io.onfhir.api.{FHIR_FOUNDATION_RESOURCES, Resource} import io.tofhir.OnFhirTestContainer -import io.tofhir.common.model.{DataTypeWithProfiles, SchemaDefinition, SimpleStructureDefinition} +import io.onfhir.definitions.common.model.{DataTypeWithProfiles, SchemaDefinition, SimpleStructureDefinition} import io.tofhir.engine.model._ import io.tofhir.engine.util.FhirMappingJobFormatter.formats import io.tofhir.engine.util.FileUtils From 1573e6c12703f0d34c784d2c89af92f19cd846ba Mon Sep 17 00:00:00 2001 From: Dogukan Cavdaroglu Date: Mon, 18 Nov 2024 14:16:53 +0300 Subject: [PATCH 2/2] :recycle: Use FhirClientUtil to create OnFhirNetworkClient from onFHIR client package --- .../read/FhirServerDataSourceReader.scala | 1 + .../engine/model/FhirSinkSettings.scala | 40 ++----------------- .../model/MappingJobSourceSettings.scala | 1 + .../tofhir/engine/util/FhirClientUtil.scala | 25 ------------ .../engine/util/FhirMappingJobFormatter.scala | 3 +- .../server/model/ImportSchemaSettings.scala | 2 +- .../service/SchemaDefinitionService.scala | 3 +- 7 files changed, 10 insertions(+), 65 deletions(-) delete mode 100644 tofhir-engine/src/main/scala/io/tofhir/engine/util/FhirClientUtil.scala diff --git a/tofhir-engine/src/main/scala/io/tofhir/engine/data/read/FhirServerDataSourceReader.scala b/tofhir-engine/src/main/scala/io/tofhir/engine/data/read/FhirServerDataSourceReader.scala index 5d5cd1d88..69a605f0d 100644 --- a/tofhir-engine/src/main/scala/io/tofhir/engine/data/read/FhirServerDataSourceReader.scala +++ b/tofhir-engine/src/main/scala/io/tofhir/engine/data/read/FhirServerDataSourceReader.scala @@ -1,5 +1,6 @@ package io.tofhir.engine.data.read +import io.onfhir.client.model.{BasicAuthenticationSettings, BearerTokenAuthorizationSettings, FixedTokenAuthenticationSettings} import io.onfhir.spark.reader.FhirApiReader.OPTIONS import io.tofhir.engine.model._ import org.apache.spark.sql.types.StructType diff --git a/tofhir-engine/src/main/scala/io/tofhir/engine/model/FhirSinkSettings.scala b/tofhir-engine/src/main/scala/io/tofhir/engine/model/FhirSinkSettings.scala index c827394f6..72da25cd4 100644 --- a/tofhir-engine/src/main/scala/io/tofhir/engine/model/FhirSinkSettings.scala +++ b/tofhir-engine/src/main/scala/io/tofhir/engine/model/FhirSinkSettings.scala @@ -2,8 +2,9 @@ package io.tofhir.engine.model import akka.actor.ActorSystem import io.onfhir.client.OnFhirNetworkClient +import io.onfhir.client.model.IFhirRepositorySecuritySettings +import io.onfhir.client.util.FhirClientUtil import io.tofhir.engine.data.write.FileSystemWriter.SinkContentTypes -import io.tofhir.engine.util.FhirClientUtil /** * Common interface for sink settings @@ -62,39 +63,4 @@ case class FhirRepositorySinkSettings(fhirRepoUrl: String, * @return */ def createOnFhirClient(implicit actorSystem: ActorSystem): OnFhirNetworkClient = FhirClientUtil.createOnFhirClient(fhirRepoUrl, securitySettings) -} - -/** - * Interface for security settings - */ -trait IFhirRepositorySecuritySettings - -/** - * Security settings for FHIR API access via bearer token - * - * @param clientId OpenID Client identifier assigned to toFhir - * @param clientSecret OpenID Client secret given to toFhir - * @param requiredScopes List of required scores to write the resources - * @param authzServerTokenEndpoint Authorization servers token endpoint - * @param clientAuthenticationMethod Client authentication method - */ -case class BearerTokenAuthorizationSettings(clientId: String, - clientSecret: String, - requiredScopes: Seq[String], - authzServerTokenEndpoint: String, - clientAuthenticationMethod: String = "client_secret_basic") extends IFhirRepositorySecuritySettings - -/** - * Security settings for FHIR API access via basic authentication - * - * @param username Username for basic authentication - * @param password Password for basic authentication - */ -case class BasicAuthenticationSettings(username: String, password: String) extends IFhirRepositorySecuritySettings - -/** - * Security settings for FHIR API access via fixed token - * - * @param token The fixed token - */ -case class FixedTokenAuthenticationSettings(token: String) extends IFhirRepositorySecuritySettings \ No newline at end of file +} \ No newline at end of file diff --git a/tofhir-engine/src/main/scala/io/tofhir/engine/model/MappingJobSourceSettings.scala b/tofhir-engine/src/main/scala/io/tofhir/engine/model/MappingJobSourceSettings.scala index f3e45e82f..ad5ec0060 100644 --- a/tofhir-engine/src/main/scala/io/tofhir/engine/model/MappingJobSourceSettings.scala +++ b/tofhir-engine/src/main/scala/io/tofhir/engine/model/MappingJobSourceSettings.scala @@ -1,5 +1,6 @@ package io.tofhir.engine.model +import io.onfhir.client.model.IFhirRepositorySecuritySettings import org.json4s.JsonAST.{JString, JValue} diff --git a/tofhir-engine/src/main/scala/io/tofhir/engine/util/FhirClientUtil.scala b/tofhir-engine/src/main/scala/io/tofhir/engine/util/FhirClientUtil.scala deleted file mode 100644 index 49a410f6b..000000000 --- a/tofhir-engine/src/main/scala/io/tofhir/engine/util/FhirClientUtil.scala +++ /dev/null @@ -1,25 +0,0 @@ -package io.tofhir.engine.util - -import akka.actor.ActorSystem -import io.onfhir.client.OnFhirNetworkClient -import io.tofhir.engine.model.{BasicAuthenticationSettings, BearerTokenAuthorizationSettings, FixedTokenAuthenticationSettings, IFhirRepositorySecuritySettings} - -object FhirClientUtil { - /** - * Create an OnFhir client - * - * @param actorSystem - * @return - */ - def createOnFhirClient(fhirRepoUrl: String, securitySettings: Option[IFhirRepositorySecuritySettings] = None)(implicit actorSystem: ActorSystem): OnFhirNetworkClient = { - val client = OnFhirNetworkClient.apply(fhirRepoUrl) - securitySettings - .map { - case BearerTokenAuthorizationSettings(clientId, clientSecret, requiredScopes, authzServerTokenEndpoint, clientAuthenticationMethod) => - client.withOpenIdBearerTokenAuthentication(clientId, clientSecret, requiredScopes, authzServerTokenEndpoint, clientAuthenticationMethod) - case BasicAuthenticationSettings(username, password) => client.withBasicAuthentication(username, password) - case FixedTokenAuthenticationSettings(token) => client.withFixedBasicTokenAuthentication(token) - } - .getOrElse(client) - } -} diff --git a/tofhir-engine/src/main/scala/io/tofhir/engine/util/FhirMappingJobFormatter.scala b/tofhir-engine/src/main/scala/io/tofhir/engine/util/FhirMappingJobFormatter.scala index 8da4193ea..a23fbe8b1 100644 --- a/tofhir-engine/src/main/scala/io/tofhir/engine/util/FhirMappingJobFormatter.scala +++ b/tofhir-engine/src/main/scala/io/tofhir/engine/util/FhirMappingJobFormatter.scala @@ -1,6 +1,7 @@ package io.tofhir.engine.util -import io.tofhir.engine.model.{BasicAuthenticationSettings, BearerTokenAuthorizationSettings, FhirMappingJob, FhirMappingTask, FhirRepositorySinkSettings, FhirServerSource, FhirServerSourceSettings, FileSystemSinkSettings, FileSystemSource, FileSystemSourceSettings, FixedTokenAuthenticationSettings, KafkaSource, KafkaSourceSettings, LocalFhirTerminologyServiceSettings, SQLSchedulingSettings, SchedulingSettings, SqlSource, SqlSourceSettings} +import io.onfhir.client.model.{BasicAuthenticationSettings, BearerTokenAuthorizationSettings, FixedTokenAuthenticationSettings} +import io.tofhir.engine.model.{FhirMappingJob, FhirMappingTask, FhirRepositorySinkSettings, FhirServerSource, FhirServerSourceSettings, FileSystemSinkSettings, FileSystemSource, FileSystemSourceSettings, KafkaSource, KafkaSourceSettings, LocalFhirTerminologyServiceSettings, SQLSchedulingSettings, SchedulingSettings, SqlSource, SqlSourceSettings} import org.json4s.{Formats, MappingException, ShortTypeHints} import org.json4s.jackson.Serialization diff --git a/tofhir-server/src/main/scala/io/tofhir/server/model/ImportSchemaSettings.scala b/tofhir-server/src/main/scala/io/tofhir/server/model/ImportSchemaSettings.scala index 10e59b10b..47d34b31b 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/model/ImportSchemaSettings.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/model/ImportSchemaSettings.scala @@ -1,6 +1,6 @@ package io.tofhir.server.model -import io.tofhir.engine.model.IFhirRepositorySecuritySettings +import io.onfhir.client.model.IFhirRepositorySecuritySettings /** * Represents the settings required for importing a schema from a FHIR server. diff --git a/tofhir-server/src/main/scala/io/tofhir/server/service/SchemaDefinitionService.scala b/tofhir-server/src/main/scala/io/tofhir/server/service/SchemaDefinitionService.scala index 9d3651b66..d1e1b4e52 100644 --- a/tofhir-server/src/main/scala/io/tofhir/server/service/SchemaDefinitionService.scala +++ b/tofhir-server/src/main/scala/io/tofhir/server/service/SchemaDefinitionService.scala @@ -4,6 +4,7 @@ import akka.stream.scaladsl.Source import akka.util.ByteString import com.typesafe.scalalogging.LazyLogging import io.onfhir.api.Resource +import io.onfhir.client.util.FhirClientUtil import io.onfhir.definitions.common.model.SchemaDefinition import io.tofhir.engine.Execution.actorSystem import io.tofhir.engine.config.ToFhirConfig @@ -11,7 +12,7 @@ import io.tofhir.engine.data.read.SourceHandler import io.tofhir.engine.mapping.schema.SchemaConverter import io.tofhir.engine.model.exception.FhirMappingException import io.tofhir.engine.util.redcap.RedCapUtil -import io.tofhir.engine.util.{CsvUtil, FhirClientUtil, FhirVersionUtil} +import io.tofhir.engine.util.{CsvUtil, FhirVersionUtil} import io.tofhir.server.common.model.{BadRequest, ResourceNotFound} import io.tofhir.server.model.{ImportSchemaSettings, InferTask} import io.tofhir.server.repository.mapping.IMappingRepository