From 15911c408d2556849769103fd206fce59799f2cd Mon Sep 17 00:00:00 2001 From: Tuncay Namli Date: Fri, 20 Sep 2024 16:15:25 +0300 Subject: [PATCH 1/7] :sparkles: feat: Now we support versioning in StructureDefinitions (supporting different versions of a profile) and refering the profiles with versioning in capability statement and other parts --- .../scala/io/onfhir/api/util/FHIRUtil.scala | 77 ++++++++++++++++ .../AbstractFhirContentValidator.scala | 2 +- .../api/validation/ProfileRestrictions.scala | 6 +- .../io/onfhir/config/BaseFhirConfig.scala | 41 ++++++--- .../io/onfhir/config/FhirServerConfig.scala | 13 +-- .../io/onfhir/api/util/FHIRUtilTest.scala | 4 + .../onfhir/config/BaseFhirConfigurator.scala | 92 +++++++++++-------- .../config/BaseFhirServerConfigurator.scala | 56 +++++------ .../config/SearchParameterConfigurator.scala | 2 +- .../onfhir/api/model/XmlToJsonConvertor.scala | 2 +- .../api/validation/FHIRApiValidator.scala | 15 +-- .../validation/FHIRResourceValidator.scala | 2 +- .../ValidationOperationHandler.scala | 37 ++++---- .../parsers/StructureDefinitionParser.scala | 8 +- .../validation/ProfileValidationTest.scala | 2 +- .../validation/BaseFhirProfileHandler.scala | 2 +- .../validation/FhirContentValidator.scala | 6 +- .../validation/ReferenceRestrictions.scala | 2 +- .../onfhir/validation/TypeRestriction.scala | 2 +- 19 files changed, 248 insertions(+), 123 deletions(-) diff --git a/onfhir-common/src/main/scala/io/onfhir/api/util/FHIRUtil.scala b/onfhir-common/src/main/scala/io/onfhir/api/util/FHIRUtil.scala index f47b8b45..8d058b50 100644 --- a/onfhir-common/src/main/scala/io/onfhir/api/util/FHIRUtil.scala +++ b/onfhir-common/src/main/scala/io/onfhir/api/util/FHIRUtil.scala @@ -3,6 +3,7 @@ package io.onfhir.api.util import akka.http.scaladsl.model._ import io.onfhir.api._ import io.onfhir.api.model._ +import io.onfhir.api.validation.ProfileRestrictions import io.onfhir.config.OnfhirConfig import io.onfhir.util.JsonFormatter.formats @@ -964,4 +965,80 @@ object FHIRUtil { Math.pow(10, i) * 0.5 } } + + /** + * Find the latest FHIR version among the given versions. If versions are not given in the correct format this returns None indicating it is impossible + * e.g. 2.1, 2.2, 2.3.1, 2.4 --> 2.4 + * @param versions Given version strings + * @return + */ + def findLatestFhirVersion(versions:Seq[String]):Option[String] = { + Try( + versions + .map(v => v.split('.').map(_.toInt)) //Try to parse the versions e.g. 1.2, 2.2.2, if not return None indicating we cannot deduce the latest version + ) + .toOption + .getOrElse(Nil) + .sortWith((v1, v2) => isNewer(v1, v2)) + .headOption + .map(v => v.mkString(".")) + } + + /** + * Check if the first FHIR version is greater than the second + * @param v1 First FHIR version + * @param v2 Second FHIR version + * @return + */ + private def isNewer(v1:Array[Int], v2:Array[Int]):Boolean = { + val result = + v1 + .zip(v2) //Zip the versions, if lengths are not equal remaining elements are dropped + .foldLeft[Option[Boolean]](None) { + case (None,(i1, i2)) if i1 == i2 => None //If both versions are equal, return None indicating not decided yet + case (None, (i1, i2)) => Some(i1 > i2) //If version1 is greater, return true + case (Some(true), _) => Some(true) //If version1 is greater in earlier phase, continue with that + case (Some(false), _) => Some(false) //If version1 is smaller in earlier phase, continue with that + } + + (v1.length, v2.length) match { + //If version 1's length equal or more and the comparison is equal e.g. 2.1.1 vs 2.1 --> assume 2.1.1 is newer + case (l1, l2) if l1 >= l2 => result.getOrElse(true) + //Otherwise e.g. 2.1 vs 2.1.1 --> return false as the second is newer + case _ => result.getOrElse(false) + } + } + + /** + * Find the definition of a mentioned profile + * @param mentionedProfile Mentioned profile URL and optional version + * @param profiles All existing profiles + * @return + */ + def getMentionedProfile(mentionedProfile:(String, Option[String]), profiles: Map[String, Map[String, ProfileRestrictions]]):Option[ProfileRestrictions] = { + profiles + .get(mentionedProfile._1) + .flatMap(foundVersions=> + mentionedProfile._2 + //If a version is mentioned, try to get that + .flatMap(v => + foundVersions.get(v) + ) + //If a version is not mentioned + .orElse( + //If there is only one just return it + if (foundVersions.size == 1) + foundVersions.headOption.map(_._2) + else { + foundVersions + .get("latest") + .orElse( + FHIRUtil + .findLatestFhirVersion(foundVersions.keys.toSeq) + .map(v => foundVersions(v)) + ) + } + ) + ) + } } diff --git a/onfhir-common/src/main/scala/io/onfhir/api/validation/AbstractFhirContentValidator.scala b/onfhir-common/src/main/scala/io/onfhir/api/validation/AbstractFhirContentValidator.scala index cc00ac0f..5153c189 100644 --- a/onfhir-common/src/main/scala/io/onfhir/api/validation/AbstractFhirContentValidator.scala +++ b/onfhir-common/src/main/scala/io/onfhir/api/validation/AbstractFhirContentValidator.scala @@ -21,7 +21,7 @@ abstract class AbstractFhirContentValidator( val terminologyValidator: IFhirTerminologyValidator ) { //Chain of profiles for this profile, where parents are on the right in hierarchy order e.g. MyObservation2 -> MyObservation -> Observation -> DomainResource -> Resource - val rootProfileChain: Seq[ProfileRestrictions] = fhirConfig.findProfileChain(profileUrl) + val rootProfileChain: Seq[ProfileRestrictions] = fhirConfig.findProfileChainByCanonical(profileUrl) //FHIR reference and expected target profiles to check for existence val referencesToCheck = new mutable.ListBuffer[(FhirReference, Set[String])]() diff --git a/onfhir-common/src/main/scala/io/onfhir/api/validation/ProfileRestrictions.scala b/onfhir-common/src/main/scala/io/onfhir/api/validation/ProfileRestrictions.scala index 2b835b23..74f386c3 100644 --- a/onfhir-common/src/main/scala/io/onfhir/api/validation/ProfileRestrictions.scala +++ b/onfhir-common/src/main/scala/io/onfhir/api/validation/ProfileRestrictions.scala @@ -29,8 +29,9 @@ case class ConstraintFailure(errorOrWarningMessage: String, isWarning: Boolean = * A FHIR StructureDefinition (Profile or base definition for resource types or data types) * * @param url URL of the profile + * @param version Version of the profile * @param id Resource id of the profile - * @param baseUrl Base profile that this extends if exist + * @param baseUrl Base profile that this extends if exist (with optional version) * @param resourceType Resource type for the StructureDefinition * @param resourceName Given name of the StructureDefinition resource * @param resourceDescription Description of the StructureDefinition resource @@ -40,8 +41,9 @@ case class ConstraintFailure(errorOrWarningMessage: String, isWarning: Boolean = * @param isAbstract If this is a abstract definition */ case class ProfileRestrictions(url: String, + version:Option[String], id: Option[String], - baseUrl: Option[String], + baseUrl: Option[(String, Option[String])], resourceType: String, resourceName: Option[String], resourceDescription: Option[String], diff --git a/onfhir-common/src/main/scala/io/onfhir/config/BaseFhirConfig.scala b/onfhir-common/src/main/scala/io/onfhir/config/BaseFhirConfig.scala index fee599da..7b75ba45 100644 --- a/onfhir-common/src/main/scala/io/onfhir/config/BaseFhirConfig.scala +++ b/onfhir-common/src/main/scala/io/onfhir/config/BaseFhirConfig.scala @@ -1,6 +1,7 @@ package io.onfhir.config import io.onfhir.api.FHIR_ROOT_URL_FOR_DEFINITIONS +import io.onfhir.api.util.FHIRUtil import io.onfhir.api.validation.{ProfileRestrictions, ValueSetRestrictions} /** * @@ -14,8 +15,8 @@ class BaseFhirConfig(version:String) { */ var fhirVersion: String = _ - /** FHIR Profile definitions including the base profiles (For validation) Profile Url -> Definitions * */ - var profileRestrictions: Map[String, ProfileRestrictions] = _ + /** FHIR Profile definitions including the base profiles (For validation) Profile Url -> Map(version -> Definition) * */ + var profileRestrictions: Map[String, Map[String, ProfileRestrictions]] = _ /** Supported FHIR value set urls with this server (For validation) ValueSet Url -> Map(Version ->Definitions) */ var valueSetRestrictions: Map[String, Map[String, ValueSetRestrictions]] = _ @@ -54,23 +55,36 @@ class BaseFhirConfig(version:String) { * @param profileUrl Profile URL (StructureDefinition.url) * @return */ - def findProfile(profileUrl: String): Option[ProfileRestrictions] = { - profileRestrictions.get(profileUrl) + def findProfile(profileUrl: String, version:Option[String] = None): Option[ProfileRestrictions] = { + FHIRUtil.getMentionedProfile(profileUrl -> version, profileRestrictions) } /** - * Find a chain of parent profiles until the base FHIR specification profile + * Find a chain of parent profiles until the base FHIR specification profile from given url and optional version * * @param profileUrl Profile URL (StructureDefinition.url) + * @param version Version of definition (StructureDefinition.version) * @return Profiles in order of evaluation (inner profile,..., base profile) */ - def findProfileChain(profileUrl: String): Seq[ProfileRestrictions] = { - findProfile(profileUrl) match { + def findProfileChain(profileUrl: String, version:Option[String] = None): Seq[ProfileRestrictions] = { + findProfile(profileUrl, version) match { case None => Nil - case Some(profile) => findChain(profileRestrictions)(profile) + case Some(profile) => findChain(profile) } } + /** + * Find a chain of parent profiles until the base FHIR specification profile from given Canonical reference to profile + * @param profileCanonicalRef Canonical reference + * e.g. http://onfhir.io/StructureDefinition/MyProfile + * e.g. http://onfhir.io/StructureDefinition/MyProfile|2.0 + * @return + */ + def findProfileChainByCanonical(profileCanonicalRef:String):Seq[ProfileRestrictions] = { + val (profileUrl, version) = FHIRUtil.parseCanonicalValue(profileCanonicalRef) + findProfileChain(profileUrl, version) + } + /** * Find target resource/data type of a profile * @param profileUrl Profile URL (StructureDefinition.url) @@ -83,17 +97,16 @@ class BaseFhirConfig(version:String) { /** * Supplementary method for profile chain finding * - * @param restrictions Profile restrictions for each profile - * @param profile Profile URL (StructureDefinition.url) + * @param profile Profile itself * @return */ - private def findChain(restrictions: Map[String, ProfileRestrictions])(profile: ProfileRestrictions): Seq[ProfileRestrictions] = { + private def findChain(profile: ProfileRestrictions): Seq[ProfileRestrictions] = { profile .baseUrl .map(burl => - restrictions - .get(burl) - .fold[Seq[ProfileRestrictions]](Seq(profile))(parent => profile +: findChain(restrictions)(parent)) + findProfile(burl._1, burl._2) + .map(parent => profile +: findChain(parent)) + .getOrElse(Nil) ) .getOrElse(Seq(profile)) } diff --git a/onfhir-common/src/main/scala/io/onfhir/config/FhirServerConfig.scala b/onfhir-common/src/main/scala/io/onfhir/config/FhirServerConfig.scala index 4a5ed609..0d486f58 100644 --- a/onfhir-common/src/main/scala/io/onfhir/config/FhirServerConfig.scala +++ b/onfhir-common/src/main/scala/io/onfhir/config/FhirServerConfig.scala @@ -13,8 +13,8 @@ class FhirServerConfig(version:String) extends BaseFhirConfig(version) { /*** * Dynamic configurations for this instance of FHIR repository */ - /** List of supported resource types and profiles for each resource; resource-type -> Set(profile-url) */ - var supportedProfiles:Map[String, Set[String]] = HashMap() + /** List of supported resource types and profiles for each resource; resource-type -> Map(profile-url -> Set(versions)) */ + var supportedProfiles:Map[String, Map[String, Set[String]]] = HashMap() /** Rest configuration for each Resource*/ var resourceConfigurations:Map[String, ResourceConf] = HashMap() @@ -85,8 +85,10 @@ class FhirServerConfig(version:String) extends BaseFhirConfig(version) { * @param profileUrl Profile URL (StructureDefinition.url) * @return */ - def isProfileSupported(profileUrl:String):Boolean = { - supportedProfiles.flatMap(_._2).exists(_.equals(profileUrl)) + def isProfileSupported(profileUrl:String, version:Option[String] = None):Boolean = { + supportedProfiles.flatMap(_._2).exists(profiles => + profiles._1 == profileUrl && version.forall(v => profiles._2.contains(v)) + ) } /** @@ -149,8 +151,7 @@ class FhirServerConfig(version:String) extends BaseFhirConfig(version) { */ def getSummaryElements(rtype:String):Set[String] = { val cProfile = resourceConfigurations(rtype).profile.getOrElse(s"$FHIR_ROOT_URL_FOR_DEFINITIONS/StructureDefinition/$rtype") - - profileRestrictions(cProfile).summaryElements + findProfile(cProfile).map(_.summaryElements).getOrElse(Set.empty) } } diff --git a/onfhir-common/src/test/scala/io/onfhir/api/util/FHIRUtilTest.scala b/onfhir-common/src/test/scala/io/onfhir/api/util/FHIRUtilTest.scala index d57c2bdd..d2de07d6 100644 --- a/onfhir-common/src/test/scala/io/onfhir/api/util/FHIRUtilTest.scala +++ b/onfhir-common/src/test/scala/io/onfhir/api/util/FHIRUtilTest.scala @@ -82,5 +82,9 @@ class FHIRUtilTest extends Specification { FHIRUtil.getParameterValueByPath(parameters, "match.concept").map(c => (c \ "code").extract[String]).toSet shouldEqual Set("309068002", "309068001") FHIRUtil.getParameterValueByPath(parameters, "match.other.x") shouldEqual Seq(JString("y")) } + + "find latest FHIR version" in { + FHIRUtil.findLatestFhirVersion(Seq("2.1", "2.2", "1.9", "2.2.1")) shouldEqual Some("2.2.1") + } } } diff --git a/onfhir-config/src/main/scala/io/onfhir/config/BaseFhirConfigurator.scala b/onfhir-config/src/main/scala/io/onfhir/config/BaseFhirConfigurator.scala index 57b14b22..fd2b36e6 100644 --- a/onfhir-config/src/main/scala/io/onfhir/config/BaseFhirConfigurator.scala +++ b/onfhir-config/src/main/scala/io/onfhir/config/BaseFhirConfigurator.scala @@ -2,6 +2,7 @@ package io.onfhir.config import io.onfhir.api.FHIR_FOUNDATION_RESOURCES.{FHIR_CODE_SYSTEM, FHIR_STRUCTURE_DEFINITION, FHIR_VALUE_SET} import io.onfhir.api.model.OutcomeIssue +import io.onfhir.api.parsers.IFhirFoundationResourceParser import io.onfhir.api.util.FHIRUtil import io.onfhir.api.validation.{ConstraintKeys, IReferenceResolver, ProfileRestrictions, SimpleReferenceResolver} import io.onfhir.api.{FHIR_ROOT_URL_FOR_DEFINITIONS, Resource} @@ -64,19 +65,15 @@ abstract class BaseFhirConfigurator extends IFhirVersionConfigurator { logger.info("Parsing base FHIR foundation resources (base standard) ...") //Parsing base definitions - val baseResourceProfiles = - baseResourceProfileResources - .map(foundationResourceParser.parseStructureDefinition(_, includeElementMetadata = true)) - .map(p => p.url -> p).toMap - val baseDataTypeProfiles = - baseDataTypeProfileResources - .map(foundationResourceParser.parseStructureDefinition(_, includeElementMetadata = true)) - .map(p => p.url -> p).toMap + val baseResourceProfiles = parseStructureDefinitionsConvertToMap(foundationResourceParser, baseResourceProfileResources) + + val baseDataTypeProfiles = parseStructureDefinitionsConvertToMap(foundationResourceParser, baseDataTypeProfileResources) + val baseProfiles = baseResourceProfiles ++ - baseDataTypeProfiles.filter(_._1.split('/').last.head.isUpper) ++ - baseOtherProfileResources.map(foundationResourceParser.parseStructureDefinition(_, includeElementMetadata = true)).map(p => p.url -> p).toMap ++ - baseExtensionProfileResources.map(foundationResourceParser.parseStructureDefinition(_, includeElementMetadata = true)).map(p => p.url -> p).toMap + baseDataTypeProfiles.filter(_._1.split('/').last.head.isUpper) ++ //Get only complex types + parseStructureDefinitionsConvertToMap(foundationResourceParser, baseOtherProfileResources) ++ + parseStructureDefinitionsConvertToMap(foundationResourceParser, baseExtensionProfileResources) //Initialize fhir config with base profiles and value sets to prepare for validation fhirConfig.profileRestrictions = baseProfiles @@ -87,7 +84,8 @@ abstract class BaseFhirConfigurator extends IFhirVersionConfigurator { validateGivenInfrastructureResources(fhirConfig, FHIR_CODE_SYSTEM, codeSystemResources) logger.info("Parsing given FHIR foundation resources ...") //Parsing the profiles and value sets into our compact form - val profiles = profileResources.map(foundationResourceParser.parseStructureDefinition(_, includeElementMetadata = true)).map(p => p.url -> p).toMap + val profiles = parseStructureDefinitionsConvertToMap(foundationResourceParser, profileResources) + //Parse all as bundle val valueSets = foundationResourceParser.parseValueSetAndCodeSystems(valueSetResources ++ codeSystemResources ++ baseValueSetsAndCodeSystems) @@ -101,6 +99,19 @@ abstract class BaseFhirConfigurator extends IFhirVersionConfigurator { fhirConfig } + /** + * Parse StructureDefinition resources and make them Map of Map (url and version) + * @param foundationResourceParser Resource parser + * @param resources StructureDefinition resources + * @return + */ + protected def parseStructureDefinitionsConvertToMap(foundationResourceParser:IFhirFoundationResourceParser, resources:Seq[Resource], includeElementMetadata:Boolean = true):Map[String, Map[String, ProfileRestrictions]] = { + resources + .map(foundationResourceParser.parseStructureDefinition(_, includeElementMetadata = includeElementMetadata)) + .map(s => (s.url, s.version, s)) + .groupBy(_._1) + .map(g => g._1 -> g._2.map(s => s._2.getOrElse("latest") -> s._3).toMap) + } /** * Validate and handle profile configurations @@ -111,11 +122,15 @@ abstract class BaseFhirConfigurator extends IFhirVersionConfigurator { * @return */ private def validateAndConfigureProfiles(fhirConfig: BaseFhirConfig, - profiles: Map[String, ProfileRestrictions], - baseProfiles: Map[String, ProfileRestrictions]): BaseFhirConfig = { + profiles: Map[String, Map[String, ProfileRestrictions]], + baseProfiles: Map[String, Map[String, ProfileRestrictions]]): BaseFhirConfig = { //Check if all mentioned profiles within the given profiles also exist in profile set (Profile set is closed) - val allProfilesAndExtensionsMentionedInSomewhere = findMentionedProfiles(fhirConfig, profiles.values.toSeq) - val profileDefinitionsNotGiven = allProfilesAndExtensionsMentionedInSomewhere.filter(p => !profiles.contains(p) && !baseProfiles.contains(p)).toSeq + val allProfilesAndExtensionsMentionedInSomewhere = findMentionedProfiles(fhirConfig, profiles.values.flatMap(_.values).toSeq) + //Find those ones that the definitions are not given + val profileDefinitionsNotGiven = + allProfilesAndExtensionsMentionedInSomewhere + .filter(p => FHIRUtil.getMentionedProfile(p, profiles ++ baseProfiles).isEmpty) + .toSeq if (profileDefinitionsNotGiven.nonEmpty) throw new InitializationException(s"Missing StructureDefinition in profile configurations for the referred profiles (${profileDefinitionsNotGiven.mkString(",")}) within the given profiles (e.g. as base profile 'StructureDefinition.baseDefinition', target profile for an element StructureDefinition.differential.element.type.profile or reference StructureDefinition.differential.element.type.targetProfile) ! All mentioned profiles should be given for configuration of the application!") @@ -131,27 +146,30 @@ abstract class BaseFhirConfigurator extends IFhirVersionConfigurator { * @param profiles All profile restrictions * @return */ - protected def findMentionedProfiles(fhirConfig: BaseFhirConfig, profiles: Seq[ProfileRestrictions]): Set[String] = { - profiles.flatMap(p => { - val isBaseProfile = p.url.startsWith(FHIR_ROOT_URL_FOR_DEFINITIONS) - p.elementRestrictions.map(_._2) - .flatMap(e => - e.restrictions.get(ConstraintKeys.DATATYPE).toSeq.map(_.asInstanceOf[TypeRestriction]) - .flatMap(_.dataTypesAndProfiles.flatMap(dtp => dtp._2 match { - case Nil => - if (isBaseProfile || fhirConfig.FHIR_COMPLEX_TYPES.contains(dtp._1)) - Seq(s"$FHIR_ROOT_URL_FOR_DEFINITIONS/StructureDefinition/${dtp._1}") - else - Nil - case oth => oth - }).toSet) ++ - e.restrictions.get(ConstraintKeys.REFERENCE_TARGET).toSeq.map(_.asInstanceOf[ReferenceRestrictions]).flatMap(_.targetProfiles).toSet) ++ - p.baseUrl.toSeq - }).filterNot(p => { - val ns = s"$FHIR_ROOT_URL_FOR_DEFINITIONS/StructureDefinition/" - val arr = p.split(ns) - arr.length == 2 && Character.isLowerCase(arr(1).head) - }).toSet + protected def findMentionedProfiles(fhirConfig: BaseFhirConfig, profiles: Seq[ProfileRestrictions]): Set[(String, Option[String])] = { + profiles + .flatMap(p => { + val isBaseProfile = p.url.startsWith(FHIR_ROOT_URL_FOR_DEFINITIONS) + p.elementRestrictions.map(_._2) + .flatMap(e => + e.restrictions.get(ConstraintKeys.DATATYPE).toSeq.map(_.asInstanceOf[TypeRestriction]) + .flatMap(_.dataTypesAndProfiles.flatMap(dtp => dtp._2 match { + case Nil => + if (isBaseProfile || fhirConfig.FHIR_COMPLEX_TYPES.contains(dtp._1)) + Seq(s"$FHIR_ROOT_URL_FOR_DEFINITIONS/StructureDefinition/${dtp._1}") + else + Nil + case oth => oth + }).toSet) ++ + e.restrictions.get(ConstraintKeys.REFERENCE_TARGET).toSeq.map(_.asInstanceOf[ReferenceRestrictions]).flatMap(_.targetProfiles).toSet + ).map(FHIRUtil.parseCanonicalValue) ++ + p.baseUrl.toSeq + }) + .filterNot(p => { + val ns = s"$FHIR_ROOT_URL_FOR_DEFINITIONS/StructureDefinition/" + p._1.startsWith(ns) && Character.isLowerCase(p._1.drop(ns.length).head) + }) + .toSet } /** diff --git a/onfhir-config/src/main/scala/io/onfhir/config/BaseFhirServerConfigurator.scala b/onfhir-config/src/main/scala/io/onfhir/config/BaseFhirServerConfigurator.scala index 9b835379..96800d61 100644 --- a/onfhir-config/src/main/scala/io/onfhir/config/BaseFhirServerConfigurator.scala +++ b/onfhir-config/src/main/scala/io/onfhir/config/BaseFhirServerConfigurator.scala @@ -85,13 +85,15 @@ abstract class BaseFhirServerConfigurator extends BaseFhirConfigurator with IFhi logger.info("Parsing base FHIR foundation resources (base standard) ...") //Parsing base definitions - val baseResourceProfiles = baseResourceProfileResources.map(foundationResourceParser.parseStructureDefinition).map(p => p.url -> p).toMap - val baseDataTypeProfiles = baseDataTypeProfileResources.map(foundationResourceParser.parseStructureDefinition).map(p => p.url -> p).toMap + val baseResourceProfiles = parseStructureDefinitionsConvertToMap(foundationResourceParser, baseResourceProfileResources, includeElementMetadata = false) + val baseDataTypeProfiles = parseStructureDefinitionsConvertToMap(foundationResourceParser, baseDataTypeProfileResources, includeElementMetadata = false) + val baseProfiles = baseResourceProfiles ++ baseDataTypeProfiles.filter(_._1.split('/').last.head.isUpper) ++ - baseOtherProfileResources.map(foundationResourceParser.parseStructureDefinition).map(p => p.url -> p).toMap ++ - baseExtensionProfileResources.map(foundationResourceParser.parseStructureDefinition).map(p => p.url -> p).toMap + parseStructureDefinitionsConvertToMap(foundationResourceParser, baseOtherProfileResources, includeElementMetadata = false) ++ + parseStructureDefinitionsConvertToMap(foundationResourceParser, baseExtensionProfileResources, includeElementMetadata = false) + val baseSearchParameters = baseSearchParameterResources.map(foundationResourceParser.parseSearchParameter).map(s => s.url -> s).toMap val baseOperationDefinitions = baseOperationDefinitionResources.map(foundationResourceParser.parseOperationDefinition).map(p => p.url -> p).toMap val baseCompartmentDefinitions = baseCompartmentDefinitionResources.map(foundationResourceParser.parseCompartmentDefinition).map(c => c.url -> c).toMap @@ -113,7 +115,7 @@ abstract class BaseFhirServerConfigurator extends BaseFhirConfigurator with IFhi logger.info("Parsing given FHIR foundation resources ...") //Parsing the Conformance statement into our compact form val conformance = foundationResourceParser.parseCapabilityStatement(conformanceResource) - val profiles = profileResources.map(foundationResourceParser.parseStructureDefinition).map(p => p.url -> p).toMap + val profiles = parseStructureDefinitionsConvertToMap(foundationResourceParser, profileResources, includeElementMetadata = false) val searchParameters = searchParameterResources.map(foundationResourceParser.parseSearchParameter).map(s => s.url -> s).toMap val operationDefs = operationDefResources.map(opDef => foundationResourceParser.parseOperationDefinition(opDef)).map(o => o.url -> o).toMap val compartments = compartmentDefResources.map(foundationResourceParser.parseCompartmentDefinition).map(c => c.url -> c).toMap @@ -125,7 +127,7 @@ abstract class BaseFhirServerConfigurator extends BaseFhirConfigurator with IFhi fhirConfig = validateAndConfigureProfiles(fhirConfig, conformance, profiles, baseProfiles) logger.info("Configuring supported FHIR search parameters for supported resources ...") - fhirConfig = validateAndConfigureSearchParameters(fhirConfig, conformance, searchParameters, baseSearchParameters, baseProfiles ++ profiles) + fhirConfig = validateAndConfigureSearchParameters(fhirConfig, conformance, searchParameters, baseSearchParameters) logger.info("Configuring supported FHIR operations ...") fhirConfig = validateAndConfigureOperations(fhirConfig, conformance, operationDefs, baseOperationDefinitions, fhirOperationImplms) @@ -280,39 +282,41 @@ abstract class BaseFhirServerConfigurator extends BaseFhirConfigurator with IFhi * @param baseProfiles Base profiles given in standard bundle * @return */ - protected def validateAndConfigureProfiles(fhirConfig: FhirServerConfig, conformance:FHIRCapabilityStatement, profiles:Map[String, ProfileRestrictions], baseProfiles:Map[String, ProfileRestrictions]):FhirServerConfig = { + protected def validateAndConfigureProfiles(fhirConfig: FhirServerConfig, conformance:FHIRCapabilityStatement, profiles:Map[String, Map[String, ProfileRestrictions]], baseProfiles:Map[String, Map[String, ProfileRestrictions]]):FhirServerConfig = { //Check if all base profiles mentioned in Conformance are given in profile configurations - var profileDefinitionsNotGiven = conformance.restResourceConf.flatMap(_.profile).filter(p => !profiles.contains(p) && !baseProfiles.contains(p)) + var profileDefinitionsNotGiven = + conformance.restResourceConf.flatMap(_.profile).map(FHIRUtil.parseCanonicalValue).filter(p => FHIRUtil.getMentionedProfile(p, profiles ++ baseProfiles).isEmpty) + if(profileDefinitionsNotGiven.nonEmpty) - throw new InitializationException(s"Missing StructureDefinition in profile configurations for some of the base profiles (${profileDefinitionsNotGiven.mkString(",")}) declared in CapabilityStatement (CapabilityStatement.rest.resource.profile)! ") + throw new InitializationException(s"Missing StructureDefinition in profile configurations for some of the base profiles (${profileDefinitionsNotGiven.map(p => s"${p._1}${p._2.map(v => s"|$v").getOrElse("")}").mkString(",")}) declared in CapabilityStatement (CapabilityStatement.rest.resource.profile)! ") //Check if all supported profiles mentioned in Conformance are given in profile configurations - profileDefinitionsNotGiven = conformance.restResourceConf.flatMap(_.supportedProfiles.toSeq).filter(p => !profiles.contains(p) && !baseProfiles.contains(p)) + profileDefinitionsNotGiven = conformance.restResourceConf.flatMap(_.supportedProfiles.toSeq).map(FHIRUtil.parseCanonicalValue).filter(p => FHIRUtil.getMentionedProfile(p, profiles ++ baseProfiles).isEmpty) if(profileDefinitionsNotGiven.nonEmpty) - throw new InitializationException(s"Missing StructureDefinition in profile configurations for some of the supported profiles (${profileDefinitionsNotGiven.mkString(",")}) declared in CapabilityStatement (CapabilityStatement.rest.resource.supportedProfile)! ") + throw new InitializationException(s"Missing StructureDefinition in profile configurations for some of the supported profiles (${profileDefinitionsNotGiven.map(p => s"${p._1}${p._2.map(v => s"|$v").getOrElse("")}").mkString(",")}) declared in CapabilityStatement (CapabilityStatement.rest.resource.supportedProfile)! ") //Get the URLs of all used base profiles val baseProfilesUrlsUsed = - (conformance.restResourceConf.flatMap(_.profile).filter(baseProfiles.contains) ++ - conformance.restResourceConf.flatMap(_.supportedProfiles.toSeq).filter(baseProfiles.contains)).toSet + (conformance.restResourceConf.flatMap(_.profile).map(FHIRUtil.parseCanonicalValue).filter(p => FHIRUtil.getMentionedProfile(p, baseProfiles).nonEmpty) ++ + conformance.restResourceConf.flatMap(_.supportedProfiles.toSeq).map(FHIRUtil.parseCanonicalValue).filter(p => FHIRUtil.getMentionedProfile(p, baseProfiles).nonEmpty)).toSet //Check if all mentioned profiles within the given profiles also exist in profile set (Profile set is closed) - var allProfilesAndExtensionsMentionedInSomewhere = findMentionedProfiles(fhirConfig, profiles.values.toSeq) - profileDefinitionsNotGiven = allProfilesAndExtensionsMentionedInSomewhere.filter(p => !profiles.contains(p) && !baseProfiles.contains(p)).toSeq + var allProfilesAndExtensionsMentionedInSomewhere = findMentionedProfiles(fhirConfig, profiles.values.flatMap(_.values).toSeq) + profileDefinitionsNotGiven = allProfilesAndExtensionsMentionedInSomewhere.filter(p => FHIRUtil.getMentionedProfile(p, profiles ++ baseProfiles).isEmpty).toSeq if(profileDefinitionsNotGiven.nonEmpty) - throw new InitializationException(s"Missing StructureDefinition in o profile configurations for the referred profiles (${profileDefinitionsNotGiven.mkString(",")}) within the given profiles (e.g. as base profile 'StructureDefinition.baseDefinition', target profile for an element StructureDefinition.differential.element.type.profile or reference StructureDefinition.differential.element.type.targetProfile) ! All mentioned profiles should be given for validation!") + throw new InitializationException(s"Missing StructureDefinition in o profile configurations for the referred profiles (${profileDefinitionsNotGiven.map(p => s"${p._1}${p._2.map(v => s"|$v").getOrElse("")}").mkString(",")}) within the given profiles (e.g. as base profile 'StructureDefinition.baseDefinition', target profile for an element StructureDefinition.differential.element.type.profile or reference StructureDefinition.differential.element.type.targetProfile) ! All mentioned profiles should be given for validation!") allProfilesAndExtensionsMentionedInSomewhere = - allProfilesAndExtensionsMentionedInSomewhere.diff(profiles.keySet) ++ - baseProfilesUrlsUsed ++ + allProfilesAndExtensionsMentionedInSomewhere.filter(p => FHIRUtil.getMentionedProfile(p, profiles).isEmpty) ++ //Mentioned profiles that are not given in configured profile set + baseProfilesUrlsUsed ++ //Base FHIR standart definitions //Base profiles used in FHIR interactions Set( - s"$FHIR_ROOT_URL_FOR_DEFINITIONS/StructureDefinition/OperationOutcome", - s"$FHIR_ROOT_URL_FOR_DEFINITIONS/StructureDefinition/Bundle", - s"$FHIR_ROOT_URL_FOR_DEFINITIONS/StructureDefinition/Parameters", + s"$FHIR_ROOT_URL_FOR_DEFINITIONS/StructureDefinition/OperationOutcome" -> None, + s"$FHIR_ROOT_URL_FOR_DEFINITIONS/StructureDefinition/Bundle" -> None, + s"$FHIR_ROOT_URL_FOR_DEFINITIONS/StructureDefinition/Parameters" -> None, ) - fhirConfig.supportedProfiles = conformance.restResourceConf.map(restConf => restConf.resource -> restConf.supportedProfiles).toMap + fhirConfig.supportedProfiles = conformance.restResourceConf.map(restConf => restConf.resource -> restConf.supportedProfiles.map(FHIRUtil.parseCanonicalValue).groupBy(_._1).map(g => g._1 -> g._2.flatMap(_._2))).toMap fhirConfig.resourceConfigurations = conformance.restResourceConf.map(restConf => restConf.resource -> restConf).toMap fhirConfig.profileRestrictions = @@ -328,9 +332,9 @@ abstract class BaseFhirServerConfigurator extends BaseFhirConfigurator with IFhi * @param baseProfiles All base profiles provided in the standard * @return */ - private def findClosureForBaseProfiles(fhirConfig: FhirServerConfig, mentionedBaseProfileUrls:Set[String], baseProfiles:Map[String, ProfileRestrictions]):Map[String, ProfileRestrictions] = { - val mentionedBaseProfiles = mentionedBaseProfileUrls.map(url => url -> baseProfiles.apply(url)).toMap - val deepMentionedBaseProfiles = findMentionedProfiles(fhirConfig, mentionedBaseProfiles.values.toSeq) + private def findClosureForBaseProfiles(fhirConfig: FhirServerConfig, mentionedBaseProfileUrls:Set[(String, Option[String])], baseProfiles:Map[String, Map[String, ProfileRestrictions]]):Map[String, Map[String, ProfileRestrictions]] = { + val mentionedBaseProfiles = mentionedBaseProfileUrls.map(p => p._1 -> Map(p._2.getOrElse("latest") -> FHIRUtil.getMentionedProfile(p, baseProfiles).get)).toMap + val deepMentionedBaseProfiles = findMentionedProfiles(fhirConfig, mentionedBaseProfiles.values.flatMap(_.values).toSeq) val newUrls = deepMentionedBaseProfiles.diff(mentionedBaseProfileUrls) if(newUrls.nonEmpty) findClosureForBaseProfiles(fhirConfig, mentionedBaseProfileUrls ++ newUrls, baseProfiles) @@ -347,7 +351,7 @@ abstract class BaseFhirServerConfigurator extends BaseFhirConfigurator with IFhi * @param allProfiles All profiles both base and given * @return */ - protected def validateAndConfigureSearchParameters(fhirConfig: FhirServerConfig, conformance:FHIRCapabilityStatement, searchParameters:Map[String, FHIRSearchParameter], baseSearchParameters:Map[String, FHIRSearchParameter], allProfiles:Map[String, ProfileRestrictions]) :FhirServerConfig = { + protected def validateAndConfigureSearchParameters(fhirConfig: FhirServerConfig, conformance:FHIRCapabilityStatement, searchParameters:Map[String, FHIRSearchParameter], baseSearchParameters:Map[String, FHIRSearchParameter]) :FhirServerConfig = { //Check if for all search parameters mentioned in the Conformance, a SearchParameter definition exist in base standard or given configuration val resourcesWithMissingSearchParameterDefs = conformance.restResourceConf diff --git a/onfhir-config/src/main/scala/io/onfhir/config/SearchParameterConfigurator.scala b/onfhir-config/src/main/scala/io/onfhir/config/SearchParameterConfigurator.scala index 3dcd286f..58deb5aa 100644 --- a/onfhir-config/src/main/scala/io/onfhir/config/SearchParameterConfigurator.scala +++ b/onfhir-config/src/main/scala/io/onfhir/config/SearchParameterConfigurator.scala @@ -30,7 +30,7 @@ class SearchParameterConfigurator( // Find profile definition for base Resource type val baseProfileChain = fhirConfig.getBaseProfileChain(rtype) //Find profile chain for base profile specific for resource type - val profileChain = rtypeBaseProfile.map(url => fhirConfig.findProfileChain(url)).getOrElse(baseProfileChain) + val profileChain = rtypeBaseProfile.map(url => fhirConfig.findProfileChainByCanonical(url)).getOrElse(baseProfileChain) /** diff --git a/onfhir-core/src/main/scala/io/onfhir/api/model/XmlToJsonConvertor.scala b/onfhir-core/src/main/scala/io/onfhir/api/model/XmlToJsonConvertor.scala index a92e7477..b89ce5d6 100644 --- a/onfhir-core/src/main/scala/io/onfhir/api/model/XmlToJsonConvertor.scala +++ b/onfhir-core/src/main/scala/io/onfhir/api/model/XmlToJsonConvertor.scala @@ -36,7 +36,7 @@ class XmlToJsonConvertor(fhirConfig: FhirServerConfig) extends BaseFhirProfileHa throw new UnprocessableEntityException(Seq(OutcomeIssue(FHIRResponse.SEVERITY_CODES.ERROR, FHIRResponse.OUTCOME_CODES.INVALID, None, Some(s"Invalid resource type ${parsedXml.label} in XML format!"), Seq("resourceType")))) val baseProfile: ProfileRestrictions = fhirConfig.getBaseProfile(parsedXml.label) - val baseProfileChain = fhirConfig.findProfileChain(baseProfile.url) + val baseProfileChain = fhirConfig.findProfileChain(baseProfile.url, baseProfile.version) val result = ("resourceType" -> parsedXml.label) ~ convertNodeGroupToJson(parsedXml, baseProfileChain) diff --git a/onfhir-core/src/main/scala/io/onfhir/api/validation/FHIRApiValidator.scala b/onfhir-core/src/main/scala/io/onfhir/api/validation/FHIRApiValidator.scala index 8a081b0b..5925932b 100644 --- a/onfhir-core/src/main/scala/io/onfhir/api/validation/FHIRApiValidator.scala +++ b/onfhir-core/src/main/scala/io/onfhir/api/validation/FHIRApiValidator.scala @@ -191,14 +191,15 @@ object FHIRApiValidator { */ def validateResourceType(resource:Resource, _rtype:String):Seq[OutcomeIssue] = { validateResourceTypeMatching(resource, _rtype) - + /** This is not needed //Base profile for resource - val profile = fhirConfig.resourceConfigurations.get(_rtype).flatMap(_.profile) - //All supported profiles for resource - val supportedProfiles = fhirConfig.supportedProfiles.getOrElse(_rtype, Set.empty) + val profile = fhirConfig.resourceConfigurations.get(_rtype).flatMap(_.profile.map(FHIRUtil.parseCanonicalValue)) + //All supported profiles for resource type + val supportedProfiles = fhirConfig.supportedProfiles.getOrElse(_rtype, Map.empty) + //If a base profile is defined for resource, then we expect resource contains some profile in meta (base profile or sub profiles) - if(profile.isDefined && !profile.get.startsWith(FHIR_ROOT_URL_FOR_DEFINITIONS)){ - val resourceProfiles = FHIRUtil.extractProfilesFromBson(resource) + if(profile.exists(_._1.startsWith(FHIR_ROOT_URL_FOR_DEFINITIONS))){ + val resourceProfiles = FHIRUtil.extractProfilesFromBson(resource).map(FHIRUtil.parseCanonicalValue) if(resourceProfiles.intersect(supportedProfiles ++ Set(profile.get)).isEmpty) throw new BadRequestException(Seq( OutcomeIssue( @@ -213,7 +214,7 @@ object FHIRApiValidator { Seq(".meta.profile") ) )) - } + }*/ Nil } diff --git a/onfhir-core/src/main/scala/io/onfhir/api/validation/FHIRResourceValidator.scala b/onfhir-core/src/main/scala/io/onfhir/api/validation/FHIRResourceValidator.scala index 607b90a3..e7efd095 100644 --- a/onfhir-core/src/main/scala/io/onfhir/api/validation/FHIRResourceValidator.scala +++ b/onfhir-core/src/main/scala/io/onfhir/api/validation/FHIRResourceValidator.scala @@ -59,7 +59,7 @@ class FHIRResourceValidator(fhirConfigurationManager: IFhirConfigurationManager) //Known profiles among them val knownProfiles = profilesClaimedToConform.intersect(supportedProfiles) //We only validate against the base profile and known profiles - val profileChains = (Set(baseProfile) ++ knownProfiles).map(p => p -> fhirConfigurationManager.fhirConfig.findProfileChain(p)) + val profileChains = (Set(baseProfile) ++ knownProfiles).map(p => p -> fhirConfigurationManager.fhirConfig.findProfileChainByCanonical(p)) //Find inner profile urls in all profile chain val allInnerProfiles = profileChains.flatMap(_._2.drop(1).map(_.url)) //Only validate against the one that does not exist in inner profiles (because this chain already includes others) diff --git a/onfhir-operations/src/main/scala/io/onfhir/operation/ValidationOperationHandler.scala b/onfhir-operations/src/main/scala/io/onfhir/operation/ValidationOperationHandler.scala index 31a89640..c17c156e 100644 --- a/onfhir-operations/src/main/scala/io/onfhir/operation/ValidationOperationHandler.scala +++ b/onfhir-operations/src/main/scala/io/onfhir/operation/ValidationOperationHandler.scala @@ -107,26 +107,29 @@ class ValidationOperationHandler(fhirConfigurationManager:IFhirConfigurationMana Nil)) ) ) - case Some(resource) => { + case Some(resource) => //Validate if resource type matches FHIRApiValidator.validateResourceTypeMatching(resource, resourceType) - val resultResource = if(profileOption.isDefined) { - //Check if we support profile, if not return not supported - if(!fhirConfigurationManager.fhirConfig.isProfileSupported(profileOption.get)) - throw new MethodNotAllowedException(Seq( - OutcomeIssue(FHIRResponse.SEVERITY_CODES.ERROR, - FHIRResponse.OUTCOME_CODES.NOT_SUPPORTED, - None, - Some(s"Given profile is not supported in this server. See our Conformance statement from ${OnfhirConfig.fhirRootUrl}/metadata for all supported profiles ..."), - Nil))) - //else Set the profile into the resource - FHIRUtil.setProfile(resource, profileOption.get) - } else resource + val resultResource = + profileOption + .map(FHIRUtil.parseCanonicalValue) match { + case Some(url -> version) if !fhirConfigurationManager.fhirConfig.isProfileSupported(url, version) => + throw new MethodNotAllowedException(Seq( + OutcomeIssue(FHIRResponse.SEVERITY_CODES.ERROR, + FHIRResponse.OUTCOME_CODES.NOT_SUPPORTED, + None, + Some(s"Given profile is not supported in this server. See our Conformance statement from ${OnfhirConfig.fhirRootUrl}/metadata for all supported profiles ..."), + Nil))) + case Some(_) => + //else Set the profile into the resource + FHIRUtil.setProfile(resource, profileOption.get) + case None => + resource + } validateContent(resultResource, resourceType, validationMode) map { issues => - //Not error response, but we should return OperationOutcome so we use this - FHIRResponse.errorResponse(StatusCodes.OK, issues) - } - } + //Not error response, but we should return OperationOutcome so we use this + FHIRResponse.errorResponse(StatusCodes.OK, issues) + } } } diff --git a/onfhir-r4/src/main/scala/io/onfhir/r4/parsers/StructureDefinitionParser.scala b/onfhir-r4/src/main/scala/io/onfhir/r4/parsers/StructureDefinitionParser.scala index 2e1b8384..ce8326e8 100644 --- a/onfhir-r4/src/main/scala/io/onfhir/r4/parsers/StructureDefinitionParser.scala +++ b/onfhir-r4/src/main/scala/io/onfhir/r4/parsers/StructureDefinitionParser.scala @@ -26,6 +26,7 @@ class StructureDefinitionParser(fhirComplexTypes: Set[String], fhirPrimitiveType if (rtype.apply(0).isLower) { ProfileRestrictions( url = FHIRUtil.extractValueOption[String](structureDef, "url").get, + version = FHIRUtil.extractValueOption[String](structureDef, "version"), id = FHIRUtil.extractValueOption[String](structureDef, "id"), baseUrl = None, resourceType = rtype, @@ -55,14 +56,15 @@ class StructureDefinitionParser(fhirComplexTypes: Set[String], fhirPrimitiveType //Parse element definitions (without establishing child relationship) val elemDefs = elementDefs - .map(parseElementDef(_, rtype, if (profileUrl.startsWith(FHIR_ROOT_URL_FOR_DEFINITIONS)) None else Some(profileUrl), //Parse the element definitions + .map(parseElementDef(_, rtype, if (profileUrl.startsWith(FHIR_ROOT_URL_FOR_DEFINITIONS + "/StructureDefinition")) None else Some(profileUrl), //Parse the element definitions includeElementMetadata)) ProfileRestrictions( - url = FHIRUtil.extractValueOption[String](structureDef, "url").get, + url = profileUrl, + version = FHIRUtil.extractValueOption[String](structureDef, "version"), id = FHIRUtil.extractValueOption[String](structureDef, "id"), - baseUrl = FHIRUtil.extractValueOption[String](structureDef, "baseDefinition"), + baseUrl = FHIRUtil.extractValueOption[String](structureDef, "baseDefinition").map(url => FHIRUtil.parseCanonicalValue(url)), resourceType = rtype, resourceName = FHIRUtil.extractValueOption[String](structureDef, "name"), resourceDescription = FHIRUtil.extractValueOption[String](structureDef, "description"), diff --git a/onfhir-server-r4/src/test/scala/io/onfhir/validation/ProfileValidationTest.scala b/onfhir-server-r4/src/test/scala/io/onfhir/validation/ProfileValidationTest.scala index d128a480..5f0ec769 100644 --- a/onfhir-server-r4/src/test/scala/io/onfhir/validation/ProfileValidationTest.scala +++ b/onfhir-server-r4/src/test/scala/io/onfhir/validation/ProfileValidationTest.scala @@ -69,7 +69,7 @@ class ProfileValidationTest extends Specification { ).map(sdParser.parseProfile) - fhirConfig.profileRestrictions = (resourceProfiles ++ dataTypeProfiles ++ otherProfiles ++ extensions ++ extraProfiles).map(p => p.url -> p).toMap + fhirConfig.profileRestrictions = (resourceProfiles ++ dataTypeProfiles ++ otherProfiles ++ extensions ++ extraProfiles).map(p => p.url -> Map("latest" -> p)).toMap fhirConfig.valueSetRestrictions = new TerminologyParser().parseValueSetBundle(valueSetsOrCodeSystems) //Make reference validation policy as enforced for DiagnosticReport diff --git a/onfhir-validation/src/main/scala/io/onfhir/validation/BaseFhirProfileHandler.scala b/onfhir-validation/src/main/scala/io/onfhir/validation/BaseFhirProfileHandler.scala index 960e9dbe..41b6d64c 100644 --- a/onfhir-validation/src/main/scala/io/onfhir/validation/BaseFhirProfileHandler.scala +++ b/onfhir-validation/src/main/scala/io/onfhir/validation/BaseFhirProfileHandler.scala @@ -177,7 +177,7 @@ abstract class BaseFhirProfileHandler(val fhirConfig: FhirServerConfig) { import scala.util.control.Breaks._ breakable { for (i <- multipleProfiles._3.indices) { - fhirConfig.findProfileChain(multipleProfiles._3.apply(i)) match { + fhirConfig.findProfileChainByCanonical(multipleProfiles._3.apply(i)) match { case Nil => //Nothing case pc: Seq[ProfileRestrictions] if pc.nonEmpty => diff --git a/onfhir-validation/src/main/scala/io/onfhir/validation/FhirContentValidator.scala b/onfhir-validation/src/main/scala/io/onfhir/validation/FhirContentValidator.scala index 82d2c897..83fa7918 100644 --- a/onfhir-validation/src/main/scala/io/onfhir/validation/FhirContentValidator.scala +++ b/onfhir-validation/src/main/scala/io/onfhir/validation/FhirContentValidator.scala @@ -504,7 +504,7 @@ class FhirContentValidator( //Find profile chain for each profile val profileChains = profileUrls - .map(fhirConfig.findProfileChain).sortWith((c1, c2) => c1.size >= c2.size) + .map(url => fhirConfig.findProfileChainByCanonical(url)).sortWith((c1, c2) => c1.size >= c2.size) //Get all urls var allUrls = profileChains.flatMap(_.map(_.url)).toSet val totalNumUrls = allUrls.size @@ -823,10 +823,10 @@ class FhirContentValidator( afterResolvingPathItems match { //If there is no path, then just return the targeted profile chain case Nil => - Right(fhirConfig.findProfileChain(referencedProfile)) + Right(fhirConfig.findProfileChainByCanonical(referencedProfile)) case _ => //Otherwise Load the profile chain, and find the definition path for it - val subElementsOfReferenced = fhirConfig.findProfileChain(referencedProfile).map(_.elementRestrictions) + val subElementsOfReferenced = fhirConfig.findProfileChainByCanonical(referencedProfile).map(_.elementRestrictions) val (suffixElementDefPath, suffixElementActualPath) = findDefinitionAndActualPath(afterResolvingPathItems, subElementsOfReferenced) Left( diff --git a/onfhir-validation/src/main/scala/io/onfhir/validation/ReferenceRestrictions.scala b/onfhir-validation/src/main/scala/io/onfhir/validation/ReferenceRestrictions.scala index ac15856a..bb6f8f17 100644 --- a/onfhir-validation/src/main/scala/io/onfhir/validation/ReferenceRestrictions.scala +++ b/onfhir-validation/src/main/scala/io/onfhir/validation/ReferenceRestrictions.scala @@ -45,7 +45,7 @@ case class ReferenceRestrictions(targetProfiles:Seq[String], versioning:Option[B if(tp == "http://hl7.org/fhir/StructureDefinition/Resource") "Resource" -> tp else { - val dt = fhirContentValidator.findResourceType(fhirContentValidator.fhirConfig.findProfileChain(tp)).get + val dt = fhirContentValidator.findResourceType(fhirContentValidator.fhirConfig.findProfileChainByCanonical(tp)).get dt -> tp } }) diff --git a/onfhir-validation/src/main/scala/io/onfhir/validation/TypeRestriction.scala b/onfhir-validation/src/main/scala/io/onfhir/validation/TypeRestriction.scala index dd350b25..2f139f3b 100644 --- a/onfhir-validation/src/main/scala/io/onfhir/validation/TypeRestriction.scala +++ b/onfhir-validation/src/main/scala/io/onfhir/validation/TypeRestriction.scala @@ -47,7 +47,7 @@ case class TypeRestriction(dataTypesAndProfiles:Seq[(String, Seq[String])]) exte ) //It should match with one of the profiles allProfiles.exists(profile => - !Await.result(fhirContentValidator.validateComplexContentAgainstProfile(fhirContentValidator.fhirConfig.findProfileChain(profile),jobj, None), 1 minutes) + !Await.result(fhirContentValidator.validateComplexContentAgainstProfile(fhirContentValidator.fhirConfig.findProfileChainByCanonical(profile),jobj, None), 1 minutes) .exists(_.severity == FHIRResponse.SEVERITY_CODES.ERROR) ) //Primitive From 6d75d3d923fe325e20400cd5888e979528e12651 Mon Sep 17 00:00:00 2001 From: Tuncay Namli Date: Fri, 20 Sep 2024 16:15:25 +0300 Subject: [PATCH 2/7] :sparkles: feat: Now we support versioning in StructureDefinitions (supporting different versions of a profile) and refering the profiles with versioning in capability statement and other parts --- .../scala/io/onfhir/api/util/FHIRUtil.scala | 77 ++++++++++++++++ .../AbstractFhirContentValidator.scala | 2 +- .../api/validation/ProfileRestrictions.scala | 6 +- .../io/onfhir/config/BaseFhirConfig.scala | 41 ++++++--- .../io/onfhir/config/FhirServerConfig.scala | 13 +-- .../io/onfhir/api/util/FHIRUtilTest.scala | 4 + .../onfhir/config/BaseFhirConfigurator.scala | 92 +++++++++++-------- .../config/BaseFhirServerConfigurator.scala | 56 +++++------ .../config/SearchParameterConfigurator.scala | 2 +- .../onfhir/api/model/XmlToJsonConvertor.scala | 2 +- .../api/validation/FHIRApiValidator.scala | 15 +-- .../validation/FHIRResourceValidator.scala | 2 +- .../ValidationOperationHandler.scala | 37 ++++---- .../parsers/StructureDefinitionParser.scala | 8 +- .../validation/ProfileValidationTest.scala | 2 +- .../validation/BaseFhirProfileHandler.scala | 2 +- .../validation/FhirContentValidator.scala | 6 +- .../validation/ReferenceRestrictions.scala | 2 +- .../onfhir/validation/TypeRestriction.scala | 2 +- 19 files changed, 248 insertions(+), 123 deletions(-) diff --git a/onfhir-common/src/main/scala/io/onfhir/api/util/FHIRUtil.scala b/onfhir-common/src/main/scala/io/onfhir/api/util/FHIRUtil.scala index f47b8b45..8d058b50 100644 --- a/onfhir-common/src/main/scala/io/onfhir/api/util/FHIRUtil.scala +++ b/onfhir-common/src/main/scala/io/onfhir/api/util/FHIRUtil.scala @@ -3,6 +3,7 @@ package io.onfhir.api.util import akka.http.scaladsl.model._ import io.onfhir.api._ import io.onfhir.api.model._ +import io.onfhir.api.validation.ProfileRestrictions import io.onfhir.config.OnfhirConfig import io.onfhir.util.JsonFormatter.formats @@ -964,4 +965,80 @@ object FHIRUtil { Math.pow(10, i) * 0.5 } } + + /** + * Find the latest FHIR version among the given versions. If versions are not given in the correct format this returns None indicating it is impossible + * e.g. 2.1, 2.2, 2.3.1, 2.4 --> 2.4 + * @param versions Given version strings + * @return + */ + def findLatestFhirVersion(versions:Seq[String]):Option[String] = { + Try( + versions + .map(v => v.split('.').map(_.toInt)) //Try to parse the versions e.g. 1.2, 2.2.2, if not return None indicating we cannot deduce the latest version + ) + .toOption + .getOrElse(Nil) + .sortWith((v1, v2) => isNewer(v1, v2)) + .headOption + .map(v => v.mkString(".")) + } + + /** + * Check if the first FHIR version is greater than the second + * @param v1 First FHIR version + * @param v2 Second FHIR version + * @return + */ + private def isNewer(v1:Array[Int], v2:Array[Int]):Boolean = { + val result = + v1 + .zip(v2) //Zip the versions, if lengths are not equal remaining elements are dropped + .foldLeft[Option[Boolean]](None) { + case (None,(i1, i2)) if i1 == i2 => None //If both versions are equal, return None indicating not decided yet + case (None, (i1, i2)) => Some(i1 > i2) //If version1 is greater, return true + case (Some(true), _) => Some(true) //If version1 is greater in earlier phase, continue with that + case (Some(false), _) => Some(false) //If version1 is smaller in earlier phase, continue with that + } + + (v1.length, v2.length) match { + //If version 1's length equal or more and the comparison is equal e.g. 2.1.1 vs 2.1 --> assume 2.1.1 is newer + case (l1, l2) if l1 >= l2 => result.getOrElse(true) + //Otherwise e.g. 2.1 vs 2.1.1 --> return false as the second is newer + case _ => result.getOrElse(false) + } + } + + /** + * Find the definition of a mentioned profile + * @param mentionedProfile Mentioned profile URL and optional version + * @param profiles All existing profiles + * @return + */ + def getMentionedProfile(mentionedProfile:(String, Option[String]), profiles: Map[String, Map[String, ProfileRestrictions]]):Option[ProfileRestrictions] = { + profiles + .get(mentionedProfile._1) + .flatMap(foundVersions=> + mentionedProfile._2 + //If a version is mentioned, try to get that + .flatMap(v => + foundVersions.get(v) + ) + //If a version is not mentioned + .orElse( + //If there is only one just return it + if (foundVersions.size == 1) + foundVersions.headOption.map(_._2) + else { + foundVersions + .get("latest") + .orElse( + FHIRUtil + .findLatestFhirVersion(foundVersions.keys.toSeq) + .map(v => foundVersions(v)) + ) + } + ) + ) + } } diff --git a/onfhir-common/src/main/scala/io/onfhir/api/validation/AbstractFhirContentValidator.scala b/onfhir-common/src/main/scala/io/onfhir/api/validation/AbstractFhirContentValidator.scala index cc00ac0f..5153c189 100644 --- a/onfhir-common/src/main/scala/io/onfhir/api/validation/AbstractFhirContentValidator.scala +++ b/onfhir-common/src/main/scala/io/onfhir/api/validation/AbstractFhirContentValidator.scala @@ -21,7 +21,7 @@ abstract class AbstractFhirContentValidator( val terminologyValidator: IFhirTerminologyValidator ) { //Chain of profiles for this profile, where parents are on the right in hierarchy order e.g. MyObservation2 -> MyObservation -> Observation -> DomainResource -> Resource - val rootProfileChain: Seq[ProfileRestrictions] = fhirConfig.findProfileChain(profileUrl) + val rootProfileChain: Seq[ProfileRestrictions] = fhirConfig.findProfileChainByCanonical(profileUrl) //FHIR reference and expected target profiles to check for existence val referencesToCheck = new mutable.ListBuffer[(FhirReference, Set[String])]() diff --git a/onfhir-common/src/main/scala/io/onfhir/api/validation/ProfileRestrictions.scala b/onfhir-common/src/main/scala/io/onfhir/api/validation/ProfileRestrictions.scala index 2b835b23..74f386c3 100644 --- a/onfhir-common/src/main/scala/io/onfhir/api/validation/ProfileRestrictions.scala +++ b/onfhir-common/src/main/scala/io/onfhir/api/validation/ProfileRestrictions.scala @@ -29,8 +29,9 @@ case class ConstraintFailure(errorOrWarningMessage: String, isWarning: Boolean = * A FHIR StructureDefinition (Profile or base definition for resource types or data types) * * @param url URL of the profile + * @param version Version of the profile * @param id Resource id of the profile - * @param baseUrl Base profile that this extends if exist + * @param baseUrl Base profile that this extends if exist (with optional version) * @param resourceType Resource type for the StructureDefinition * @param resourceName Given name of the StructureDefinition resource * @param resourceDescription Description of the StructureDefinition resource @@ -40,8 +41,9 @@ case class ConstraintFailure(errorOrWarningMessage: String, isWarning: Boolean = * @param isAbstract If this is a abstract definition */ case class ProfileRestrictions(url: String, + version:Option[String], id: Option[String], - baseUrl: Option[String], + baseUrl: Option[(String, Option[String])], resourceType: String, resourceName: Option[String], resourceDescription: Option[String], diff --git a/onfhir-common/src/main/scala/io/onfhir/config/BaseFhirConfig.scala b/onfhir-common/src/main/scala/io/onfhir/config/BaseFhirConfig.scala index fee599da..7b75ba45 100644 --- a/onfhir-common/src/main/scala/io/onfhir/config/BaseFhirConfig.scala +++ b/onfhir-common/src/main/scala/io/onfhir/config/BaseFhirConfig.scala @@ -1,6 +1,7 @@ package io.onfhir.config import io.onfhir.api.FHIR_ROOT_URL_FOR_DEFINITIONS +import io.onfhir.api.util.FHIRUtil import io.onfhir.api.validation.{ProfileRestrictions, ValueSetRestrictions} /** * @@ -14,8 +15,8 @@ class BaseFhirConfig(version:String) { */ var fhirVersion: String = _ - /** FHIR Profile definitions including the base profiles (For validation) Profile Url -> Definitions * */ - var profileRestrictions: Map[String, ProfileRestrictions] = _ + /** FHIR Profile definitions including the base profiles (For validation) Profile Url -> Map(version -> Definition) * */ + var profileRestrictions: Map[String, Map[String, ProfileRestrictions]] = _ /** Supported FHIR value set urls with this server (For validation) ValueSet Url -> Map(Version ->Definitions) */ var valueSetRestrictions: Map[String, Map[String, ValueSetRestrictions]] = _ @@ -54,23 +55,36 @@ class BaseFhirConfig(version:String) { * @param profileUrl Profile URL (StructureDefinition.url) * @return */ - def findProfile(profileUrl: String): Option[ProfileRestrictions] = { - profileRestrictions.get(profileUrl) + def findProfile(profileUrl: String, version:Option[String] = None): Option[ProfileRestrictions] = { + FHIRUtil.getMentionedProfile(profileUrl -> version, profileRestrictions) } /** - * Find a chain of parent profiles until the base FHIR specification profile + * Find a chain of parent profiles until the base FHIR specification profile from given url and optional version * * @param profileUrl Profile URL (StructureDefinition.url) + * @param version Version of definition (StructureDefinition.version) * @return Profiles in order of evaluation (inner profile,..., base profile) */ - def findProfileChain(profileUrl: String): Seq[ProfileRestrictions] = { - findProfile(profileUrl) match { + def findProfileChain(profileUrl: String, version:Option[String] = None): Seq[ProfileRestrictions] = { + findProfile(profileUrl, version) match { case None => Nil - case Some(profile) => findChain(profileRestrictions)(profile) + case Some(profile) => findChain(profile) } } + /** + * Find a chain of parent profiles until the base FHIR specification profile from given Canonical reference to profile + * @param profileCanonicalRef Canonical reference + * e.g. http://onfhir.io/StructureDefinition/MyProfile + * e.g. http://onfhir.io/StructureDefinition/MyProfile|2.0 + * @return + */ + def findProfileChainByCanonical(profileCanonicalRef:String):Seq[ProfileRestrictions] = { + val (profileUrl, version) = FHIRUtil.parseCanonicalValue(profileCanonicalRef) + findProfileChain(profileUrl, version) + } + /** * Find target resource/data type of a profile * @param profileUrl Profile URL (StructureDefinition.url) @@ -83,17 +97,16 @@ class BaseFhirConfig(version:String) { /** * Supplementary method for profile chain finding * - * @param restrictions Profile restrictions for each profile - * @param profile Profile URL (StructureDefinition.url) + * @param profile Profile itself * @return */ - private def findChain(restrictions: Map[String, ProfileRestrictions])(profile: ProfileRestrictions): Seq[ProfileRestrictions] = { + private def findChain(profile: ProfileRestrictions): Seq[ProfileRestrictions] = { profile .baseUrl .map(burl => - restrictions - .get(burl) - .fold[Seq[ProfileRestrictions]](Seq(profile))(parent => profile +: findChain(restrictions)(parent)) + findProfile(burl._1, burl._2) + .map(parent => profile +: findChain(parent)) + .getOrElse(Nil) ) .getOrElse(Seq(profile)) } diff --git a/onfhir-common/src/main/scala/io/onfhir/config/FhirServerConfig.scala b/onfhir-common/src/main/scala/io/onfhir/config/FhirServerConfig.scala index 4a5ed609..0d486f58 100644 --- a/onfhir-common/src/main/scala/io/onfhir/config/FhirServerConfig.scala +++ b/onfhir-common/src/main/scala/io/onfhir/config/FhirServerConfig.scala @@ -13,8 +13,8 @@ class FhirServerConfig(version:String) extends BaseFhirConfig(version) { /*** * Dynamic configurations for this instance of FHIR repository */ - /** List of supported resource types and profiles for each resource; resource-type -> Set(profile-url) */ - var supportedProfiles:Map[String, Set[String]] = HashMap() + /** List of supported resource types and profiles for each resource; resource-type -> Map(profile-url -> Set(versions)) */ + var supportedProfiles:Map[String, Map[String, Set[String]]] = HashMap() /** Rest configuration for each Resource*/ var resourceConfigurations:Map[String, ResourceConf] = HashMap() @@ -85,8 +85,10 @@ class FhirServerConfig(version:String) extends BaseFhirConfig(version) { * @param profileUrl Profile URL (StructureDefinition.url) * @return */ - def isProfileSupported(profileUrl:String):Boolean = { - supportedProfiles.flatMap(_._2).exists(_.equals(profileUrl)) + def isProfileSupported(profileUrl:String, version:Option[String] = None):Boolean = { + supportedProfiles.flatMap(_._2).exists(profiles => + profiles._1 == profileUrl && version.forall(v => profiles._2.contains(v)) + ) } /** @@ -149,8 +151,7 @@ class FhirServerConfig(version:String) extends BaseFhirConfig(version) { */ def getSummaryElements(rtype:String):Set[String] = { val cProfile = resourceConfigurations(rtype).profile.getOrElse(s"$FHIR_ROOT_URL_FOR_DEFINITIONS/StructureDefinition/$rtype") - - profileRestrictions(cProfile).summaryElements + findProfile(cProfile).map(_.summaryElements).getOrElse(Set.empty) } } diff --git a/onfhir-common/src/test/scala/io/onfhir/api/util/FHIRUtilTest.scala b/onfhir-common/src/test/scala/io/onfhir/api/util/FHIRUtilTest.scala index d57c2bdd..d2de07d6 100644 --- a/onfhir-common/src/test/scala/io/onfhir/api/util/FHIRUtilTest.scala +++ b/onfhir-common/src/test/scala/io/onfhir/api/util/FHIRUtilTest.scala @@ -82,5 +82,9 @@ class FHIRUtilTest extends Specification { FHIRUtil.getParameterValueByPath(parameters, "match.concept").map(c => (c \ "code").extract[String]).toSet shouldEqual Set("309068002", "309068001") FHIRUtil.getParameterValueByPath(parameters, "match.other.x") shouldEqual Seq(JString("y")) } + + "find latest FHIR version" in { + FHIRUtil.findLatestFhirVersion(Seq("2.1", "2.2", "1.9", "2.2.1")) shouldEqual Some("2.2.1") + } } } diff --git a/onfhir-config/src/main/scala/io/onfhir/config/BaseFhirConfigurator.scala b/onfhir-config/src/main/scala/io/onfhir/config/BaseFhirConfigurator.scala index 57b14b22..fd2b36e6 100644 --- a/onfhir-config/src/main/scala/io/onfhir/config/BaseFhirConfigurator.scala +++ b/onfhir-config/src/main/scala/io/onfhir/config/BaseFhirConfigurator.scala @@ -2,6 +2,7 @@ package io.onfhir.config import io.onfhir.api.FHIR_FOUNDATION_RESOURCES.{FHIR_CODE_SYSTEM, FHIR_STRUCTURE_DEFINITION, FHIR_VALUE_SET} import io.onfhir.api.model.OutcomeIssue +import io.onfhir.api.parsers.IFhirFoundationResourceParser import io.onfhir.api.util.FHIRUtil import io.onfhir.api.validation.{ConstraintKeys, IReferenceResolver, ProfileRestrictions, SimpleReferenceResolver} import io.onfhir.api.{FHIR_ROOT_URL_FOR_DEFINITIONS, Resource} @@ -64,19 +65,15 @@ abstract class BaseFhirConfigurator extends IFhirVersionConfigurator { logger.info("Parsing base FHIR foundation resources (base standard) ...") //Parsing base definitions - val baseResourceProfiles = - baseResourceProfileResources - .map(foundationResourceParser.parseStructureDefinition(_, includeElementMetadata = true)) - .map(p => p.url -> p).toMap - val baseDataTypeProfiles = - baseDataTypeProfileResources - .map(foundationResourceParser.parseStructureDefinition(_, includeElementMetadata = true)) - .map(p => p.url -> p).toMap + val baseResourceProfiles = parseStructureDefinitionsConvertToMap(foundationResourceParser, baseResourceProfileResources) + + val baseDataTypeProfiles = parseStructureDefinitionsConvertToMap(foundationResourceParser, baseDataTypeProfileResources) + val baseProfiles = baseResourceProfiles ++ - baseDataTypeProfiles.filter(_._1.split('/').last.head.isUpper) ++ - baseOtherProfileResources.map(foundationResourceParser.parseStructureDefinition(_, includeElementMetadata = true)).map(p => p.url -> p).toMap ++ - baseExtensionProfileResources.map(foundationResourceParser.parseStructureDefinition(_, includeElementMetadata = true)).map(p => p.url -> p).toMap + baseDataTypeProfiles.filter(_._1.split('/').last.head.isUpper) ++ //Get only complex types + parseStructureDefinitionsConvertToMap(foundationResourceParser, baseOtherProfileResources) ++ + parseStructureDefinitionsConvertToMap(foundationResourceParser, baseExtensionProfileResources) //Initialize fhir config with base profiles and value sets to prepare for validation fhirConfig.profileRestrictions = baseProfiles @@ -87,7 +84,8 @@ abstract class BaseFhirConfigurator extends IFhirVersionConfigurator { validateGivenInfrastructureResources(fhirConfig, FHIR_CODE_SYSTEM, codeSystemResources) logger.info("Parsing given FHIR foundation resources ...") //Parsing the profiles and value sets into our compact form - val profiles = profileResources.map(foundationResourceParser.parseStructureDefinition(_, includeElementMetadata = true)).map(p => p.url -> p).toMap + val profiles = parseStructureDefinitionsConvertToMap(foundationResourceParser, profileResources) + //Parse all as bundle val valueSets = foundationResourceParser.parseValueSetAndCodeSystems(valueSetResources ++ codeSystemResources ++ baseValueSetsAndCodeSystems) @@ -101,6 +99,19 @@ abstract class BaseFhirConfigurator extends IFhirVersionConfigurator { fhirConfig } + /** + * Parse StructureDefinition resources and make them Map of Map (url and version) + * @param foundationResourceParser Resource parser + * @param resources StructureDefinition resources + * @return + */ + protected def parseStructureDefinitionsConvertToMap(foundationResourceParser:IFhirFoundationResourceParser, resources:Seq[Resource], includeElementMetadata:Boolean = true):Map[String, Map[String, ProfileRestrictions]] = { + resources + .map(foundationResourceParser.parseStructureDefinition(_, includeElementMetadata = includeElementMetadata)) + .map(s => (s.url, s.version, s)) + .groupBy(_._1) + .map(g => g._1 -> g._2.map(s => s._2.getOrElse("latest") -> s._3).toMap) + } /** * Validate and handle profile configurations @@ -111,11 +122,15 @@ abstract class BaseFhirConfigurator extends IFhirVersionConfigurator { * @return */ private def validateAndConfigureProfiles(fhirConfig: BaseFhirConfig, - profiles: Map[String, ProfileRestrictions], - baseProfiles: Map[String, ProfileRestrictions]): BaseFhirConfig = { + profiles: Map[String, Map[String, ProfileRestrictions]], + baseProfiles: Map[String, Map[String, ProfileRestrictions]]): BaseFhirConfig = { //Check if all mentioned profiles within the given profiles also exist in profile set (Profile set is closed) - val allProfilesAndExtensionsMentionedInSomewhere = findMentionedProfiles(fhirConfig, profiles.values.toSeq) - val profileDefinitionsNotGiven = allProfilesAndExtensionsMentionedInSomewhere.filter(p => !profiles.contains(p) && !baseProfiles.contains(p)).toSeq + val allProfilesAndExtensionsMentionedInSomewhere = findMentionedProfiles(fhirConfig, profiles.values.flatMap(_.values).toSeq) + //Find those ones that the definitions are not given + val profileDefinitionsNotGiven = + allProfilesAndExtensionsMentionedInSomewhere + .filter(p => FHIRUtil.getMentionedProfile(p, profiles ++ baseProfiles).isEmpty) + .toSeq if (profileDefinitionsNotGiven.nonEmpty) throw new InitializationException(s"Missing StructureDefinition in profile configurations for the referred profiles (${profileDefinitionsNotGiven.mkString(",")}) within the given profiles (e.g. as base profile 'StructureDefinition.baseDefinition', target profile for an element StructureDefinition.differential.element.type.profile or reference StructureDefinition.differential.element.type.targetProfile) ! All mentioned profiles should be given for configuration of the application!") @@ -131,27 +146,30 @@ abstract class BaseFhirConfigurator extends IFhirVersionConfigurator { * @param profiles All profile restrictions * @return */ - protected def findMentionedProfiles(fhirConfig: BaseFhirConfig, profiles: Seq[ProfileRestrictions]): Set[String] = { - profiles.flatMap(p => { - val isBaseProfile = p.url.startsWith(FHIR_ROOT_URL_FOR_DEFINITIONS) - p.elementRestrictions.map(_._2) - .flatMap(e => - e.restrictions.get(ConstraintKeys.DATATYPE).toSeq.map(_.asInstanceOf[TypeRestriction]) - .flatMap(_.dataTypesAndProfiles.flatMap(dtp => dtp._2 match { - case Nil => - if (isBaseProfile || fhirConfig.FHIR_COMPLEX_TYPES.contains(dtp._1)) - Seq(s"$FHIR_ROOT_URL_FOR_DEFINITIONS/StructureDefinition/${dtp._1}") - else - Nil - case oth => oth - }).toSet) ++ - e.restrictions.get(ConstraintKeys.REFERENCE_TARGET).toSeq.map(_.asInstanceOf[ReferenceRestrictions]).flatMap(_.targetProfiles).toSet) ++ - p.baseUrl.toSeq - }).filterNot(p => { - val ns = s"$FHIR_ROOT_URL_FOR_DEFINITIONS/StructureDefinition/" - val arr = p.split(ns) - arr.length == 2 && Character.isLowerCase(arr(1).head) - }).toSet + protected def findMentionedProfiles(fhirConfig: BaseFhirConfig, profiles: Seq[ProfileRestrictions]): Set[(String, Option[String])] = { + profiles + .flatMap(p => { + val isBaseProfile = p.url.startsWith(FHIR_ROOT_URL_FOR_DEFINITIONS) + p.elementRestrictions.map(_._2) + .flatMap(e => + e.restrictions.get(ConstraintKeys.DATATYPE).toSeq.map(_.asInstanceOf[TypeRestriction]) + .flatMap(_.dataTypesAndProfiles.flatMap(dtp => dtp._2 match { + case Nil => + if (isBaseProfile || fhirConfig.FHIR_COMPLEX_TYPES.contains(dtp._1)) + Seq(s"$FHIR_ROOT_URL_FOR_DEFINITIONS/StructureDefinition/${dtp._1}") + else + Nil + case oth => oth + }).toSet) ++ + e.restrictions.get(ConstraintKeys.REFERENCE_TARGET).toSeq.map(_.asInstanceOf[ReferenceRestrictions]).flatMap(_.targetProfiles).toSet + ).map(FHIRUtil.parseCanonicalValue) ++ + p.baseUrl.toSeq + }) + .filterNot(p => { + val ns = s"$FHIR_ROOT_URL_FOR_DEFINITIONS/StructureDefinition/" + p._1.startsWith(ns) && Character.isLowerCase(p._1.drop(ns.length).head) + }) + .toSet } /** diff --git a/onfhir-config/src/main/scala/io/onfhir/config/BaseFhirServerConfigurator.scala b/onfhir-config/src/main/scala/io/onfhir/config/BaseFhirServerConfigurator.scala index 9b835379..96800d61 100644 --- a/onfhir-config/src/main/scala/io/onfhir/config/BaseFhirServerConfigurator.scala +++ b/onfhir-config/src/main/scala/io/onfhir/config/BaseFhirServerConfigurator.scala @@ -85,13 +85,15 @@ abstract class BaseFhirServerConfigurator extends BaseFhirConfigurator with IFhi logger.info("Parsing base FHIR foundation resources (base standard) ...") //Parsing base definitions - val baseResourceProfiles = baseResourceProfileResources.map(foundationResourceParser.parseStructureDefinition).map(p => p.url -> p).toMap - val baseDataTypeProfiles = baseDataTypeProfileResources.map(foundationResourceParser.parseStructureDefinition).map(p => p.url -> p).toMap + val baseResourceProfiles = parseStructureDefinitionsConvertToMap(foundationResourceParser, baseResourceProfileResources, includeElementMetadata = false) + val baseDataTypeProfiles = parseStructureDefinitionsConvertToMap(foundationResourceParser, baseDataTypeProfileResources, includeElementMetadata = false) + val baseProfiles = baseResourceProfiles ++ baseDataTypeProfiles.filter(_._1.split('/').last.head.isUpper) ++ - baseOtherProfileResources.map(foundationResourceParser.parseStructureDefinition).map(p => p.url -> p).toMap ++ - baseExtensionProfileResources.map(foundationResourceParser.parseStructureDefinition).map(p => p.url -> p).toMap + parseStructureDefinitionsConvertToMap(foundationResourceParser, baseOtherProfileResources, includeElementMetadata = false) ++ + parseStructureDefinitionsConvertToMap(foundationResourceParser, baseExtensionProfileResources, includeElementMetadata = false) + val baseSearchParameters = baseSearchParameterResources.map(foundationResourceParser.parseSearchParameter).map(s => s.url -> s).toMap val baseOperationDefinitions = baseOperationDefinitionResources.map(foundationResourceParser.parseOperationDefinition).map(p => p.url -> p).toMap val baseCompartmentDefinitions = baseCompartmentDefinitionResources.map(foundationResourceParser.parseCompartmentDefinition).map(c => c.url -> c).toMap @@ -113,7 +115,7 @@ abstract class BaseFhirServerConfigurator extends BaseFhirConfigurator with IFhi logger.info("Parsing given FHIR foundation resources ...") //Parsing the Conformance statement into our compact form val conformance = foundationResourceParser.parseCapabilityStatement(conformanceResource) - val profiles = profileResources.map(foundationResourceParser.parseStructureDefinition).map(p => p.url -> p).toMap + val profiles = parseStructureDefinitionsConvertToMap(foundationResourceParser, profileResources, includeElementMetadata = false) val searchParameters = searchParameterResources.map(foundationResourceParser.parseSearchParameter).map(s => s.url -> s).toMap val operationDefs = operationDefResources.map(opDef => foundationResourceParser.parseOperationDefinition(opDef)).map(o => o.url -> o).toMap val compartments = compartmentDefResources.map(foundationResourceParser.parseCompartmentDefinition).map(c => c.url -> c).toMap @@ -125,7 +127,7 @@ abstract class BaseFhirServerConfigurator extends BaseFhirConfigurator with IFhi fhirConfig = validateAndConfigureProfiles(fhirConfig, conformance, profiles, baseProfiles) logger.info("Configuring supported FHIR search parameters for supported resources ...") - fhirConfig = validateAndConfigureSearchParameters(fhirConfig, conformance, searchParameters, baseSearchParameters, baseProfiles ++ profiles) + fhirConfig = validateAndConfigureSearchParameters(fhirConfig, conformance, searchParameters, baseSearchParameters) logger.info("Configuring supported FHIR operations ...") fhirConfig = validateAndConfigureOperations(fhirConfig, conformance, operationDefs, baseOperationDefinitions, fhirOperationImplms) @@ -280,39 +282,41 @@ abstract class BaseFhirServerConfigurator extends BaseFhirConfigurator with IFhi * @param baseProfiles Base profiles given in standard bundle * @return */ - protected def validateAndConfigureProfiles(fhirConfig: FhirServerConfig, conformance:FHIRCapabilityStatement, profiles:Map[String, ProfileRestrictions], baseProfiles:Map[String, ProfileRestrictions]):FhirServerConfig = { + protected def validateAndConfigureProfiles(fhirConfig: FhirServerConfig, conformance:FHIRCapabilityStatement, profiles:Map[String, Map[String, ProfileRestrictions]], baseProfiles:Map[String, Map[String, ProfileRestrictions]]):FhirServerConfig = { //Check if all base profiles mentioned in Conformance are given in profile configurations - var profileDefinitionsNotGiven = conformance.restResourceConf.flatMap(_.profile).filter(p => !profiles.contains(p) && !baseProfiles.contains(p)) + var profileDefinitionsNotGiven = + conformance.restResourceConf.flatMap(_.profile).map(FHIRUtil.parseCanonicalValue).filter(p => FHIRUtil.getMentionedProfile(p, profiles ++ baseProfiles).isEmpty) + if(profileDefinitionsNotGiven.nonEmpty) - throw new InitializationException(s"Missing StructureDefinition in profile configurations for some of the base profiles (${profileDefinitionsNotGiven.mkString(",")}) declared in CapabilityStatement (CapabilityStatement.rest.resource.profile)! ") + throw new InitializationException(s"Missing StructureDefinition in profile configurations for some of the base profiles (${profileDefinitionsNotGiven.map(p => s"${p._1}${p._2.map(v => s"|$v").getOrElse("")}").mkString(",")}) declared in CapabilityStatement (CapabilityStatement.rest.resource.profile)! ") //Check if all supported profiles mentioned in Conformance are given in profile configurations - profileDefinitionsNotGiven = conformance.restResourceConf.flatMap(_.supportedProfiles.toSeq).filter(p => !profiles.contains(p) && !baseProfiles.contains(p)) + profileDefinitionsNotGiven = conformance.restResourceConf.flatMap(_.supportedProfiles.toSeq).map(FHIRUtil.parseCanonicalValue).filter(p => FHIRUtil.getMentionedProfile(p, profiles ++ baseProfiles).isEmpty) if(profileDefinitionsNotGiven.nonEmpty) - throw new InitializationException(s"Missing StructureDefinition in profile configurations for some of the supported profiles (${profileDefinitionsNotGiven.mkString(",")}) declared in CapabilityStatement (CapabilityStatement.rest.resource.supportedProfile)! ") + throw new InitializationException(s"Missing StructureDefinition in profile configurations for some of the supported profiles (${profileDefinitionsNotGiven.map(p => s"${p._1}${p._2.map(v => s"|$v").getOrElse("")}").mkString(",")}) declared in CapabilityStatement (CapabilityStatement.rest.resource.supportedProfile)! ") //Get the URLs of all used base profiles val baseProfilesUrlsUsed = - (conformance.restResourceConf.flatMap(_.profile).filter(baseProfiles.contains) ++ - conformance.restResourceConf.flatMap(_.supportedProfiles.toSeq).filter(baseProfiles.contains)).toSet + (conformance.restResourceConf.flatMap(_.profile).map(FHIRUtil.parseCanonicalValue).filter(p => FHIRUtil.getMentionedProfile(p, baseProfiles).nonEmpty) ++ + conformance.restResourceConf.flatMap(_.supportedProfiles.toSeq).map(FHIRUtil.parseCanonicalValue).filter(p => FHIRUtil.getMentionedProfile(p, baseProfiles).nonEmpty)).toSet //Check if all mentioned profiles within the given profiles also exist in profile set (Profile set is closed) - var allProfilesAndExtensionsMentionedInSomewhere = findMentionedProfiles(fhirConfig, profiles.values.toSeq) - profileDefinitionsNotGiven = allProfilesAndExtensionsMentionedInSomewhere.filter(p => !profiles.contains(p) && !baseProfiles.contains(p)).toSeq + var allProfilesAndExtensionsMentionedInSomewhere = findMentionedProfiles(fhirConfig, profiles.values.flatMap(_.values).toSeq) + profileDefinitionsNotGiven = allProfilesAndExtensionsMentionedInSomewhere.filter(p => FHIRUtil.getMentionedProfile(p, profiles ++ baseProfiles).isEmpty).toSeq if(profileDefinitionsNotGiven.nonEmpty) - throw new InitializationException(s"Missing StructureDefinition in o profile configurations for the referred profiles (${profileDefinitionsNotGiven.mkString(",")}) within the given profiles (e.g. as base profile 'StructureDefinition.baseDefinition', target profile for an element StructureDefinition.differential.element.type.profile or reference StructureDefinition.differential.element.type.targetProfile) ! All mentioned profiles should be given for validation!") + throw new InitializationException(s"Missing StructureDefinition in o profile configurations for the referred profiles (${profileDefinitionsNotGiven.map(p => s"${p._1}${p._2.map(v => s"|$v").getOrElse("")}").mkString(",")}) within the given profiles (e.g. as base profile 'StructureDefinition.baseDefinition', target profile for an element StructureDefinition.differential.element.type.profile or reference StructureDefinition.differential.element.type.targetProfile) ! All mentioned profiles should be given for validation!") allProfilesAndExtensionsMentionedInSomewhere = - allProfilesAndExtensionsMentionedInSomewhere.diff(profiles.keySet) ++ - baseProfilesUrlsUsed ++ + allProfilesAndExtensionsMentionedInSomewhere.filter(p => FHIRUtil.getMentionedProfile(p, profiles).isEmpty) ++ //Mentioned profiles that are not given in configured profile set + baseProfilesUrlsUsed ++ //Base FHIR standart definitions //Base profiles used in FHIR interactions Set( - s"$FHIR_ROOT_URL_FOR_DEFINITIONS/StructureDefinition/OperationOutcome", - s"$FHIR_ROOT_URL_FOR_DEFINITIONS/StructureDefinition/Bundle", - s"$FHIR_ROOT_URL_FOR_DEFINITIONS/StructureDefinition/Parameters", + s"$FHIR_ROOT_URL_FOR_DEFINITIONS/StructureDefinition/OperationOutcome" -> None, + s"$FHIR_ROOT_URL_FOR_DEFINITIONS/StructureDefinition/Bundle" -> None, + s"$FHIR_ROOT_URL_FOR_DEFINITIONS/StructureDefinition/Parameters" -> None, ) - fhirConfig.supportedProfiles = conformance.restResourceConf.map(restConf => restConf.resource -> restConf.supportedProfiles).toMap + fhirConfig.supportedProfiles = conformance.restResourceConf.map(restConf => restConf.resource -> restConf.supportedProfiles.map(FHIRUtil.parseCanonicalValue).groupBy(_._1).map(g => g._1 -> g._2.flatMap(_._2))).toMap fhirConfig.resourceConfigurations = conformance.restResourceConf.map(restConf => restConf.resource -> restConf).toMap fhirConfig.profileRestrictions = @@ -328,9 +332,9 @@ abstract class BaseFhirServerConfigurator extends BaseFhirConfigurator with IFhi * @param baseProfiles All base profiles provided in the standard * @return */ - private def findClosureForBaseProfiles(fhirConfig: FhirServerConfig, mentionedBaseProfileUrls:Set[String], baseProfiles:Map[String, ProfileRestrictions]):Map[String, ProfileRestrictions] = { - val mentionedBaseProfiles = mentionedBaseProfileUrls.map(url => url -> baseProfiles.apply(url)).toMap - val deepMentionedBaseProfiles = findMentionedProfiles(fhirConfig, mentionedBaseProfiles.values.toSeq) + private def findClosureForBaseProfiles(fhirConfig: FhirServerConfig, mentionedBaseProfileUrls:Set[(String, Option[String])], baseProfiles:Map[String, Map[String, ProfileRestrictions]]):Map[String, Map[String, ProfileRestrictions]] = { + val mentionedBaseProfiles = mentionedBaseProfileUrls.map(p => p._1 -> Map(p._2.getOrElse("latest") -> FHIRUtil.getMentionedProfile(p, baseProfiles).get)).toMap + val deepMentionedBaseProfiles = findMentionedProfiles(fhirConfig, mentionedBaseProfiles.values.flatMap(_.values).toSeq) val newUrls = deepMentionedBaseProfiles.diff(mentionedBaseProfileUrls) if(newUrls.nonEmpty) findClosureForBaseProfiles(fhirConfig, mentionedBaseProfileUrls ++ newUrls, baseProfiles) @@ -347,7 +351,7 @@ abstract class BaseFhirServerConfigurator extends BaseFhirConfigurator with IFhi * @param allProfiles All profiles both base and given * @return */ - protected def validateAndConfigureSearchParameters(fhirConfig: FhirServerConfig, conformance:FHIRCapabilityStatement, searchParameters:Map[String, FHIRSearchParameter], baseSearchParameters:Map[String, FHIRSearchParameter], allProfiles:Map[String, ProfileRestrictions]) :FhirServerConfig = { + protected def validateAndConfigureSearchParameters(fhirConfig: FhirServerConfig, conformance:FHIRCapabilityStatement, searchParameters:Map[String, FHIRSearchParameter], baseSearchParameters:Map[String, FHIRSearchParameter]) :FhirServerConfig = { //Check if for all search parameters mentioned in the Conformance, a SearchParameter definition exist in base standard or given configuration val resourcesWithMissingSearchParameterDefs = conformance.restResourceConf diff --git a/onfhir-config/src/main/scala/io/onfhir/config/SearchParameterConfigurator.scala b/onfhir-config/src/main/scala/io/onfhir/config/SearchParameterConfigurator.scala index 3dcd286f..58deb5aa 100644 --- a/onfhir-config/src/main/scala/io/onfhir/config/SearchParameterConfigurator.scala +++ b/onfhir-config/src/main/scala/io/onfhir/config/SearchParameterConfigurator.scala @@ -30,7 +30,7 @@ class SearchParameterConfigurator( // Find profile definition for base Resource type val baseProfileChain = fhirConfig.getBaseProfileChain(rtype) //Find profile chain for base profile specific for resource type - val profileChain = rtypeBaseProfile.map(url => fhirConfig.findProfileChain(url)).getOrElse(baseProfileChain) + val profileChain = rtypeBaseProfile.map(url => fhirConfig.findProfileChainByCanonical(url)).getOrElse(baseProfileChain) /** diff --git a/onfhir-core/src/main/scala/io/onfhir/api/model/XmlToJsonConvertor.scala b/onfhir-core/src/main/scala/io/onfhir/api/model/XmlToJsonConvertor.scala index a92e7477..b89ce5d6 100644 --- a/onfhir-core/src/main/scala/io/onfhir/api/model/XmlToJsonConvertor.scala +++ b/onfhir-core/src/main/scala/io/onfhir/api/model/XmlToJsonConvertor.scala @@ -36,7 +36,7 @@ class XmlToJsonConvertor(fhirConfig: FhirServerConfig) extends BaseFhirProfileHa throw new UnprocessableEntityException(Seq(OutcomeIssue(FHIRResponse.SEVERITY_CODES.ERROR, FHIRResponse.OUTCOME_CODES.INVALID, None, Some(s"Invalid resource type ${parsedXml.label} in XML format!"), Seq("resourceType")))) val baseProfile: ProfileRestrictions = fhirConfig.getBaseProfile(parsedXml.label) - val baseProfileChain = fhirConfig.findProfileChain(baseProfile.url) + val baseProfileChain = fhirConfig.findProfileChain(baseProfile.url, baseProfile.version) val result = ("resourceType" -> parsedXml.label) ~ convertNodeGroupToJson(parsedXml, baseProfileChain) diff --git a/onfhir-core/src/main/scala/io/onfhir/api/validation/FHIRApiValidator.scala b/onfhir-core/src/main/scala/io/onfhir/api/validation/FHIRApiValidator.scala index 8a081b0b..5925932b 100644 --- a/onfhir-core/src/main/scala/io/onfhir/api/validation/FHIRApiValidator.scala +++ b/onfhir-core/src/main/scala/io/onfhir/api/validation/FHIRApiValidator.scala @@ -191,14 +191,15 @@ object FHIRApiValidator { */ def validateResourceType(resource:Resource, _rtype:String):Seq[OutcomeIssue] = { validateResourceTypeMatching(resource, _rtype) - + /** This is not needed //Base profile for resource - val profile = fhirConfig.resourceConfigurations.get(_rtype).flatMap(_.profile) - //All supported profiles for resource - val supportedProfiles = fhirConfig.supportedProfiles.getOrElse(_rtype, Set.empty) + val profile = fhirConfig.resourceConfigurations.get(_rtype).flatMap(_.profile.map(FHIRUtil.parseCanonicalValue)) + //All supported profiles for resource type + val supportedProfiles = fhirConfig.supportedProfiles.getOrElse(_rtype, Map.empty) + //If a base profile is defined for resource, then we expect resource contains some profile in meta (base profile or sub profiles) - if(profile.isDefined && !profile.get.startsWith(FHIR_ROOT_URL_FOR_DEFINITIONS)){ - val resourceProfiles = FHIRUtil.extractProfilesFromBson(resource) + if(profile.exists(_._1.startsWith(FHIR_ROOT_URL_FOR_DEFINITIONS))){ + val resourceProfiles = FHIRUtil.extractProfilesFromBson(resource).map(FHIRUtil.parseCanonicalValue) if(resourceProfiles.intersect(supportedProfiles ++ Set(profile.get)).isEmpty) throw new BadRequestException(Seq( OutcomeIssue( @@ -213,7 +214,7 @@ object FHIRApiValidator { Seq(".meta.profile") ) )) - } + }*/ Nil } diff --git a/onfhir-core/src/main/scala/io/onfhir/api/validation/FHIRResourceValidator.scala b/onfhir-core/src/main/scala/io/onfhir/api/validation/FHIRResourceValidator.scala index 607b90a3..e7efd095 100644 --- a/onfhir-core/src/main/scala/io/onfhir/api/validation/FHIRResourceValidator.scala +++ b/onfhir-core/src/main/scala/io/onfhir/api/validation/FHIRResourceValidator.scala @@ -59,7 +59,7 @@ class FHIRResourceValidator(fhirConfigurationManager: IFhirConfigurationManager) //Known profiles among them val knownProfiles = profilesClaimedToConform.intersect(supportedProfiles) //We only validate against the base profile and known profiles - val profileChains = (Set(baseProfile) ++ knownProfiles).map(p => p -> fhirConfigurationManager.fhirConfig.findProfileChain(p)) + val profileChains = (Set(baseProfile) ++ knownProfiles).map(p => p -> fhirConfigurationManager.fhirConfig.findProfileChainByCanonical(p)) //Find inner profile urls in all profile chain val allInnerProfiles = profileChains.flatMap(_._2.drop(1).map(_.url)) //Only validate against the one that does not exist in inner profiles (because this chain already includes others) diff --git a/onfhir-operations/src/main/scala/io/onfhir/operation/ValidationOperationHandler.scala b/onfhir-operations/src/main/scala/io/onfhir/operation/ValidationOperationHandler.scala index 31a89640..c17c156e 100644 --- a/onfhir-operations/src/main/scala/io/onfhir/operation/ValidationOperationHandler.scala +++ b/onfhir-operations/src/main/scala/io/onfhir/operation/ValidationOperationHandler.scala @@ -107,26 +107,29 @@ class ValidationOperationHandler(fhirConfigurationManager:IFhirConfigurationMana Nil)) ) ) - case Some(resource) => { + case Some(resource) => //Validate if resource type matches FHIRApiValidator.validateResourceTypeMatching(resource, resourceType) - val resultResource = if(profileOption.isDefined) { - //Check if we support profile, if not return not supported - if(!fhirConfigurationManager.fhirConfig.isProfileSupported(profileOption.get)) - throw new MethodNotAllowedException(Seq( - OutcomeIssue(FHIRResponse.SEVERITY_CODES.ERROR, - FHIRResponse.OUTCOME_CODES.NOT_SUPPORTED, - None, - Some(s"Given profile is not supported in this server. See our Conformance statement from ${OnfhirConfig.fhirRootUrl}/metadata for all supported profiles ..."), - Nil))) - //else Set the profile into the resource - FHIRUtil.setProfile(resource, profileOption.get) - } else resource + val resultResource = + profileOption + .map(FHIRUtil.parseCanonicalValue) match { + case Some(url -> version) if !fhirConfigurationManager.fhirConfig.isProfileSupported(url, version) => + throw new MethodNotAllowedException(Seq( + OutcomeIssue(FHIRResponse.SEVERITY_CODES.ERROR, + FHIRResponse.OUTCOME_CODES.NOT_SUPPORTED, + None, + Some(s"Given profile is not supported in this server. See our Conformance statement from ${OnfhirConfig.fhirRootUrl}/metadata for all supported profiles ..."), + Nil))) + case Some(_) => + //else Set the profile into the resource + FHIRUtil.setProfile(resource, profileOption.get) + case None => + resource + } validateContent(resultResource, resourceType, validationMode) map { issues => - //Not error response, but we should return OperationOutcome so we use this - FHIRResponse.errorResponse(StatusCodes.OK, issues) - } - } + //Not error response, but we should return OperationOutcome so we use this + FHIRResponse.errorResponse(StatusCodes.OK, issues) + } } } diff --git a/onfhir-r4/src/main/scala/io/onfhir/r4/parsers/StructureDefinitionParser.scala b/onfhir-r4/src/main/scala/io/onfhir/r4/parsers/StructureDefinitionParser.scala index 2e1b8384..ce8326e8 100644 --- a/onfhir-r4/src/main/scala/io/onfhir/r4/parsers/StructureDefinitionParser.scala +++ b/onfhir-r4/src/main/scala/io/onfhir/r4/parsers/StructureDefinitionParser.scala @@ -26,6 +26,7 @@ class StructureDefinitionParser(fhirComplexTypes: Set[String], fhirPrimitiveType if (rtype.apply(0).isLower) { ProfileRestrictions( url = FHIRUtil.extractValueOption[String](structureDef, "url").get, + version = FHIRUtil.extractValueOption[String](structureDef, "version"), id = FHIRUtil.extractValueOption[String](structureDef, "id"), baseUrl = None, resourceType = rtype, @@ -55,14 +56,15 @@ class StructureDefinitionParser(fhirComplexTypes: Set[String], fhirPrimitiveType //Parse element definitions (without establishing child relationship) val elemDefs = elementDefs - .map(parseElementDef(_, rtype, if (profileUrl.startsWith(FHIR_ROOT_URL_FOR_DEFINITIONS)) None else Some(profileUrl), //Parse the element definitions + .map(parseElementDef(_, rtype, if (profileUrl.startsWith(FHIR_ROOT_URL_FOR_DEFINITIONS + "/StructureDefinition")) None else Some(profileUrl), //Parse the element definitions includeElementMetadata)) ProfileRestrictions( - url = FHIRUtil.extractValueOption[String](structureDef, "url").get, + url = profileUrl, + version = FHIRUtil.extractValueOption[String](structureDef, "version"), id = FHIRUtil.extractValueOption[String](structureDef, "id"), - baseUrl = FHIRUtil.extractValueOption[String](structureDef, "baseDefinition"), + baseUrl = FHIRUtil.extractValueOption[String](structureDef, "baseDefinition").map(url => FHIRUtil.parseCanonicalValue(url)), resourceType = rtype, resourceName = FHIRUtil.extractValueOption[String](structureDef, "name"), resourceDescription = FHIRUtil.extractValueOption[String](structureDef, "description"), diff --git a/onfhir-server-r4/src/test/scala/io/onfhir/validation/ProfileValidationTest.scala b/onfhir-server-r4/src/test/scala/io/onfhir/validation/ProfileValidationTest.scala index d128a480..5f0ec769 100644 --- a/onfhir-server-r4/src/test/scala/io/onfhir/validation/ProfileValidationTest.scala +++ b/onfhir-server-r4/src/test/scala/io/onfhir/validation/ProfileValidationTest.scala @@ -69,7 +69,7 @@ class ProfileValidationTest extends Specification { ).map(sdParser.parseProfile) - fhirConfig.profileRestrictions = (resourceProfiles ++ dataTypeProfiles ++ otherProfiles ++ extensions ++ extraProfiles).map(p => p.url -> p).toMap + fhirConfig.profileRestrictions = (resourceProfiles ++ dataTypeProfiles ++ otherProfiles ++ extensions ++ extraProfiles).map(p => p.url -> Map("latest" -> p)).toMap fhirConfig.valueSetRestrictions = new TerminologyParser().parseValueSetBundle(valueSetsOrCodeSystems) //Make reference validation policy as enforced for DiagnosticReport diff --git a/onfhir-validation/src/main/scala/io/onfhir/validation/BaseFhirProfileHandler.scala b/onfhir-validation/src/main/scala/io/onfhir/validation/BaseFhirProfileHandler.scala index 960e9dbe..41b6d64c 100644 --- a/onfhir-validation/src/main/scala/io/onfhir/validation/BaseFhirProfileHandler.scala +++ b/onfhir-validation/src/main/scala/io/onfhir/validation/BaseFhirProfileHandler.scala @@ -177,7 +177,7 @@ abstract class BaseFhirProfileHandler(val fhirConfig: FhirServerConfig) { import scala.util.control.Breaks._ breakable { for (i <- multipleProfiles._3.indices) { - fhirConfig.findProfileChain(multipleProfiles._3.apply(i)) match { + fhirConfig.findProfileChainByCanonical(multipleProfiles._3.apply(i)) match { case Nil => //Nothing case pc: Seq[ProfileRestrictions] if pc.nonEmpty => diff --git a/onfhir-validation/src/main/scala/io/onfhir/validation/FhirContentValidator.scala b/onfhir-validation/src/main/scala/io/onfhir/validation/FhirContentValidator.scala index 92ed405b..cc06a74c 100644 --- a/onfhir-validation/src/main/scala/io/onfhir/validation/FhirContentValidator.scala +++ b/onfhir-validation/src/main/scala/io/onfhir/validation/FhirContentValidator.scala @@ -504,7 +504,7 @@ class FhirContentValidator( //Find profile chain for each profile val profileChains = profileUrls - .map(fhirConfig.findProfileChain).sortWith((c1, c2) => c1.size >= c2.size) + .map(url => fhirConfig.findProfileChainByCanonical(url)).sortWith((c1, c2) => c1.size >= c2.size) //Get all urls var allUrls = profileChains.flatMap(_.map(_.url)).toSet val totalNumUrls = allUrls.size @@ -823,10 +823,10 @@ class FhirContentValidator( afterResolvingPathItems match { //If there is no path, then just return the targeted profile chain case Nil => - Right(fhirConfig.findProfileChain(referencedProfile)) + Right(fhirConfig.findProfileChainByCanonical(referencedProfile)) case _ => //Otherwise Load the profile chain, and find the definition path for it - val subElementsOfReferenced = fhirConfig.findProfileChain(referencedProfile).map(_.elementRestrictions) + val subElementsOfReferenced = fhirConfig.findProfileChainByCanonical(referencedProfile).map(_.elementRestrictions) val (suffixElementDefPath, suffixElementActualPath) = findDefinitionAndActualPath(afterResolvingPathItems, subElementsOfReferenced) Left( diff --git a/onfhir-validation/src/main/scala/io/onfhir/validation/ReferenceRestrictions.scala b/onfhir-validation/src/main/scala/io/onfhir/validation/ReferenceRestrictions.scala index ac15856a..bb6f8f17 100644 --- a/onfhir-validation/src/main/scala/io/onfhir/validation/ReferenceRestrictions.scala +++ b/onfhir-validation/src/main/scala/io/onfhir/validation/ReferenceRestrictions.scala @@ -45,7 +45,7 @@ case class ReferenceRestrictions(targetProfiles:Seq[String], versioning:Option[B if(tp == "http://hl7.org/fhir/StructureDefinition/Resource") "Resource" -> tp else { - val dt = fhirContentValidator.findResourceType(fhirContentValidator.fhirConfig.findProfileChain(tp)).get + val dt = fhirContentValidator.findResourceType(fhirContentValidator.fhirConfig.findProfileChainByCanonical(tp)).get dt -> tp } }) diff --git a/onfhir-validation/src/main/scala/io/onfhir/validation/TypeRestriction.scala b/onfhir-validation/src/main/scala/io/onfhir/validation/TypeRestriction.scala index dd350b25..2f139f3b 100644 --- a/onfhir-validation/src/main/scala/io/onfhir/validation/TypeRestriction.scala +++ b/onfhir-validation/src/main/scala/io/onfhir/validation/TypeRestriction.scala @@ -47,7 +47,7 @@ case class TypeRestriction(dataTypesAndProfiles:Seq[(String, Seq[String])]) exte ) //It should match with one of the profiles allProfiles.exists(profile => - !Await.result(fhirContentValidator.validateComplexContentAgainstProfile(fhirContentValidator.fhirConfig.findProfileChain(profile),jobj, None), 1 minutes) + !Await.result(fhirContentValidator.validateComplexContentAgainstProfile(fhirContentValidator.fhirConfig.findProfileChainByCanonical(profile),jobj, None), 1 minutes) .exists(_.severity == FHIRResponse.SEVERITY_CODES.ERROR) ) //Primitive From cf01ba1d111029e9b4a8451de5057da52fa20dae Mon Sep 17 00:00:00 2001 From: "A. Anil Sinaci" Date: Mon, 23 Sep 2024 09:09:05 +0300 Subject: [PATCH 3/7] :loud_sound: Add a warning in case more than one StructureDefinitions are parsed which do not have version. --- .../io/onfhir/config/BaseFhirConfigurator.scala | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/onfhir-config/src/main/scala/io/onfhir/config/BaseFhirConfigurator.scala b/onfhir-config/src/main/scala/io/onfhir/config/BaseFhirConfigurator.scala index fd2b36e6..aa321752 100644 --- a/onfhir-config/src/main/scala/io/onfhir/config/BaseFhirConfigurator.scala +++ b/onfhir-config/src/main/scala/io/onfhir/config/BaseFhirConfigurator.scala @@ -109,8 +109,18 @@ abstract class BaseFhirConfigurator extends IFhirVersionConfigurator { resources .map(foundationResourceParser.parseStructureDefinition(_, includeElementMetadata = includeElementMetadata)) .map(s => (s.url, s.version, s)) - .groupBy(_._1) - .map(g => g._1 -> g._2.map(s => s._2.getOrElse("latest") -> s._3).toMap) + .groupBy(_._1) // group by the URL + .map { case (url, defs) => + val withoutVersion = defs.filter(_._2.isEmpty) // Find all StructureDefinitions without a version + + // If there are multiple StructureDefinitions without a version, log a warning + if (withoutVersion.size > 1) { + logger.warn(s"Multiple StructureDefinitions without a version for URL: $url. Only the last one will be used.") + } + + // Convert to the desired map structure: url -> (version -> StructureDefinition) + url -> defs.map(s => s._2.getOrElse("latest") -> s._3).toMap + } } /** From 1078df17ab558f01c1da24df874022c8c330614e Mon Sep 17 00:00:00 2001 From: "A. Anil Sinaci" Date: Mon, 23 Sep 2024 10:13:10 +0300 Subject: [PATCH 4/7] :loud_sound: Organize logs for better inspection. --- .../src/main/scala/io/onfhir/db/MongoDBInitializer.scala | 3 +-- .../main/scala/io/onfhir/db/OnFhirBsonTransformer.scala | 7 +++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/onfhir-core/src/main/scala/io/onfhir/db/MongoDBInitializer.scala b/onfhir-core/src/main/scala/io/onfhir/db/MongoDBInitializer.scala index 4085ea91..5b39d776 100644 --- a/onfhir-core/src/main/scala/io/onfhir/db/MongoDBInitializer.scala +++ b/onfhir-core/src/main/scala/io/onfhir/db/MongoDBInitializer.scala @@ -197,8 +197,7 @@ class MongoDBInitializer(resourceManager: ResourceManager) extends BaseDBInitial } ).recoverWith { case e => - logger.error(s"Problem in storing $resourceType !!!") - logger.error(e.getMessage, e) + logger.error(s"Problem in storing $resourceType !!!", e) throw new InitializationException(s"Problem in storing $resourceType !!!") } Await.result(job, 60 seconds) diff --git a/onfhir-core/src/main/scala/io/onfhir/db/OnFhirBsonTransformer.scala b/onfhir-core/src/main/scala/io/onfhir/db/OnFhirBsonTransformer.scala index aa7681b9..97ba2680 100644 --- a/onfhir-core/src/main/scala/io/onfhir/db/OnFhirBsonTransformer.scala +++ b/onfhir-core/src/main/scala/io/onfhir/db/OnFhirBsonTransformer.scala @@ -227,9 +227,8 @@ object OnFhirBsonTransformer{ */ def dateToISODate(string:String) : BsonValue = { val timePrecision = string.count(_ == ':') - // Conversion condtions + // Conversion conditions if ((string.contains('Z') && timePrecision == 2) || timePrecision == 3) { - if(!string.contains('.')) // ..HH:MM:SSZ|..HH:MM:SS+03:30 (All time indexes are converted to 0) BsonDateTime(dateTimeWSecFormat.parse(string).getTime) @@ -237,13 +236,13 @@ object OnFhirBsonTransformer{ // ..HH:MM:SS.SSSZ|..HH:MM:SS.SSS+03:30 (All time indexes are converted to 0) BsonDateTime(dateTimeWMiliFormat.parse(string).getTime) } else if (string.contains('Z') || string.contains('+') || string.count(_ == '-') == 3) { - // ..HH:MMZ|..HH:MM+..|Y-M-DTHH:MM-.. (Parsing automatically appends seconds, all time inedexes are converted to 0) + // ..HH:MMZ|..HH:MM+..|Y-M-DTHH:MM-.. (Parsing automatically appends seconds, all time indexes are converted to 0) BsonDateTime(dateTimeFormat.parse(string).getTime) } else if (!string.contains('Z') && timePrecision == 1) { // ..HH:MM (Parsing automatically appends seconds) BsonDateTime(dateTimeFormat.parse(string + "Z").getTime) } else { - throw new IllegalArgumentException + throw new IllegalArgumentException(s"Cannot convert the date to Mongo ISO date: $string") } } From 0a225c855654e719af523941056e7ee9c124ec70 Mon Sep 17 00:00:00 2001 From: Tuncay Namli Date: Mon, 23 Sep 2024 10:25:35 +0300 Subject: [PATCH 5/7] :bugs: fix: Fix the regex for FHIR dateTime for OnFhirBsonTransformer --- .../src/main/scala/io/onfhir/db/OnFhirBsonTransformer.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onfhir-core/src/main/scala/io/onfhir/db/OnFhirBsonTransformer.scala b/onfhir-core/src/main/scala/io/onfhir/db/OnFhirBsonTransformer.scala index 97ba2680..2aa1934a 100644 --- a/onfhir-core/src/main/scala/io/onfhir/db/OnFhirBsonTransformer.scala +++ b/onfhir-core/src/main/scala/io/onfhir/db/OnFhirBsonTransformer.scala @@ -21,7 +21,7 @@ object OnFhirBsonTransformer{ private val dateTimeWSecFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ssXXX") private val dateTimeFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mmXXX") // DateTime regular expression - private val dateTimeRegex = """-?[1-2]{1}[0|1|8|9][0-9]{2}(-(0[1-9]|1[0-2])(-(0[0-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?(\.[0-9]+)?(Z|(\+|-|\s)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))?)?)?)?$""".r + private val dateTimeRegex = """-?[1-2]{1}[0|1|8|9][0-9]{2}(-(0[1-9]|1[0-2])(-(0[0-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?(\.[0-9]+)?(Z|(\+|-|\s)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?$""".r /** * An implicit object that extends org.mongodb.scala.bson.BsonTransformer From 51d554eb8eb7d6cbfc4c081331a6f7447a154862 Mon Sep 17 00:00:00 2001 From: Tuncay Namli Date: Mon, 23 Sep 2024 11:14:56 +0300 Subject: [PATCH 6/7] :bugs: fix: Fix the bug in cardinality finding which causes problem in XmlConvertor --- .../validation/BaseFhirProfileHandler.scala | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/onfhir-validation/src/main/scala/io/onfhir/validation/BaseFhirProfileHandler.scala b/onfhir-validation/src/main/scala/io/onfhir/validation/BaseFhirProfileHandler.scala index 41b6d64c..dc7a238d 100644 --- a/onfhir-validation/src/main/scala/io/onfhir/validation/BaseFhirProfileHandler.scala +++ b/onfhir-validation/src/main/scala/io/onfhir/validation/BaseFhirProfileHandler.scala @@ -203,8 +203,7 @@ abstract class BaseFhirProfileHandler(val fhirConfig: FhirServerConfig) { */ def findPathCardinality(path:String, profiles:Seq[ProfileRestrictions]):Boolean = { //Search in the base profile defined in the standard for resource type - findElementRestrictionForPath(path, profiles.flatMap(_.elementRestrictions)) - match { + findElementRestrictionForPath(path, profiles.flatMap(_.elementRestrictions)) match { case Some(er) => er._2.restrictions.get(ConstraintKeys.ARRAY).exists(_.asInstanceOf[ArrayRestriction].isArray) //If such a path not exist case None => @@ -215,6 +214,7 @@ abstract class BaseFhirProfileHandler(val fhirConfig: FhirServerConfig) { } } + /** * Check if path targets an array or not for given resource type according to the base profile specified for that resoure type * @param path @@ -236,14 +236,24 @@ abstract class BaseFhirProfileHandler(val fhirConfig: FhirServerConfig) { //Split the path accordingly val pathToDataType = pathParts.slice(0, pathParts.length-i).mkString(".") + //val pathToDataType = pathParts.slice(0, i).mkString(".") val pathAfterDataType = pathParts.slice(pathParts.length-i, pathParts.length).mkString(".") + //val pathAfterDataType = pathParts.slice(i, pathParts.length).mkString(".") findTargetTypeOfPath(pathToDataType, profiles) .map(_._2) match { case Nil if i < pathParts.length - 1 => findPathCardinalityInSubpaths(pathParts, profiles, i+1) case foundTypes if foundTypes.nonEmpty=> val baseProfileChainForDataType = fhirConfig.getBaseProfileChain(foundTypes.head) - findPathCardinality(pathAfterDataType, baseProfileChainForDataType) + findElementRestrictionForPath(pathAfterDataType, baseProfileChainForDataType.flatMap(_.elementRestrictions)) match { + //If this element is not defined, continue with other splits + case None if i < pathParts.length - 1 => findPathCardinalityInSubpaths(pathParts, profiles, i+1) + case None => + logger.warn(s"Problem while identifying cardinality of path ${pathParts.mkString(".")} in profiles ${profiles.map(_.url).mkString(", ")}!") + false + //If there is element restrictions, get the array restriction + case Some(er) => er._2.restrictions.get(ConstraintKeys.ARRAY).exists(_.asInstanceOf[ArrayRestriction].isArray) + } case _ => logger.warn(s"Problem while identifying cardinality of path ${pathParts.mkString(".")} in profiles ${profiles.map(_.url).mkString(", ")}!") false From 836b003264d123799c6039f37dee0d3e227dff38 Mon Sep 17 00:00:00 2001 From: Tuncay Namli Date: Tue, 24 Sep 2024 15:06:48 +0300 Subject: [PATCH 7/7] :bugs: fix: Fix composite search that are on a path where the last item for common path is not an array --- .../config/SearchParameterConfigurator.scala | 21 +++++++++++++++++-- .../io/onfhir/db/ResourceQueryBuilder.scala | 18 ++++++++++++---- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/onfhir-config/src/main/scala/io/onfhir/config/SearchParameterConfigurator.scala b/onfhir-config/src/main/scala/io/onfhir/config/SearchParameterConfigurator.scala index 58deb5aa..51471c1f 100644 --- a/onfhir-config/src/main/scala/io/onfhir/config/SearchParameterConfigurator.scala +++ b/onfhir-config/src/main/scala/io/onfhir/config/SearchParameterConfigurator.scala @@ -83,12 +83,29 @@ class SearchParameterConfigurator( //Handle Paths that are over some extensions val (extensionPaths, extensionTargetTypes) = getPathsForExtensions(par) + //If extension is defined on simple type, the path should be converted due to usage of _ as prefix to the extensions on simple type + //e.g. extension on Patient.address.line --> path should be Patient.address._line + val convertedExtensionPaths = + extensionPaths.map { + case (p, r) => + val pathParts = p.split('.') + val pathToExtended = pathParts.takeWhile(_ != "extension[i]") + + findTargetTypeOfPath(pathToExtended.mkString(".").replace("[i]", ""), profileChain) match { + case Seq((_, targetType, _, _)) if fhirConfig.FHIR_PRIMITIVE_TYPES.contains(targetType) => + val convertedPath = ((pathToExtended.dropRight(1) :+ ("_"+pathToExtended.last)) ++ pathParts.drop(pathToExtended.length)).mkString(".") + convertedPath -> r + case _ => + p -> r + } + } + //Handle other paths val nonExtensionPathDetails = par.filterNot(_._1.contains("extension[i]")) //Find out the target types for paths or target type for references val (nonExtensionPaths, nonExtensionTargetTypes, restrictions, targetReferencedProfiles) = transformPathsAndExtractTargetTypes(searchParameterDef.ptype, nonExtensionPathDetails) - val finalPaths = extensionPaths.map(_._1) ++ nonExtensionPaths + val finalPaths = convertedExtensionPaths.map(_._1) ++ nonExtensionPaths val finalTargetTypes = extensionTargetTypes ++ nonExtensionTargetTypes if (finalPaths.isEmpty || finalTargetTypes.isEmpty) { @@ -99,7 +116,7 @@ class SearchParameterConfigurator( searchParameterDef, finalPaths, //Alternative Paths for the search parameter finalTargetTypes, //Target type for each path - extensionPaths.map(_._2) ++ restrictions, //Set of restrictions for each path + convertedExtensionPaths.map(_._2) ++ restrictions, //Set of restrictions for each path targetReferencedProfiles //If reference set of target profiles for each path ) ) diff --git a/onfhir-core/src/main/scala/io/onfhir/db/ResourceQueryBuilder.scala b/onfhir-core/src/main/scala/io/onfhir/db/ResourceQueryBuilder.scala index 97dc164e..5b74fd85 100644 --- a/onfhir-core/src/main/scala/io/onfhir/db/ResourceQueryBuilder.scala +++ b/onfhir-core/src/main/scala/io/onfhir/db/ResourceQueryBuilder.scala @@ -134,6 +134,15 @@ object ResourceQueryBuilder { val queries = valuesArr.flatMap(value => //For each common alternative path commonPathsAndTargetTypes.map { case (commonPath, targetType) => + //Find the path to the last array field + val finalCommonPath = + //If the path goes over an array element, but the last path item of common path is not array, find this last array item and make it common path (mongodb $elemMatch works that way) + if(!commonPath.endsWith("[i]") && commonPath.contains("[i]")){ + val i = commonPath.split(".").indexWhere(_.endsWith("[i]")) + commonPath.slice(0, i+1).mkString(".") + } else + commonPath + //Construct query for each composite val queriesForEachCombParam = compositeParams.zipWithIndex.map { case (compParamName, i) => @@ -153,10 +162,10 @@ object ResourceQueryBuilder { case _ => compParamConf .extractElementPathsAndTargetTypes(withArrayIndicators = true) - .filter(p => p._1.startsWith(commonPath)) + .filter(p => p._1.startsWith(finalCommonPath)) .map(p => //Get rid of from common path - (p._1.drop(commonPath.length) match { + (p._1.drop(finalCommonPath.length) match { case startWithDot if startWithDot.headOption.contains('.') => startWithDot.tail case oth => oth }) -> p._2 @@ -164,7 +173,7 @@ object ResourceQueryBuilder { } //Construct query for this param val queriesForCombParam = - subpathsAfterCommonPathAndTargetTypes.toSeq.map { + subpathsAfterCommonPathAndTargetTypes.map { case (path, spTargetType) => //Run query for each path SearchUtil @@ -174,7 +183,8 @@ object ResourceQueryBuilder { } //Queries on all combined components should hold val mainQuery = and(queriesForEachCombParam:_*) - if(commonPath.endsWith("[i]")) + + if(finalCommonPath.endsWith("[i]")) elemMatch(FHIRUtil.normalizeElementPath(commonPath), mainQuery) else mainQuery