diff --git a/onfhir-client/src/main/scala/io/onfhir/client/OnFhirNetworkClient.scala b/onfhir-client/src/main/scala/io/onfhir/client/OnFhirNetworkClient.scala index 0369e841..1102e1b4 100644 --- a/onfhir-client/src/main/scala/io/onfhir/client/OnFhirNetworkClient.scala +++ b/onfhir-client/src/main/scala/io/onfhir/client/OnFhirNetworkClient.scala @@ -87,7 +87,7 @@ case class OnFhirNetworkClient(serverBaseUrl:String, interceptors:Seq[IHttpReque nextPageParams.find { case (pn, pv) => // Check if the parameter is either "_page" or "_skip" - (pn.contentEquals("_page") || pn.contentEquals("_skip") || pn.contentEquals("_searchafter")) && + (pn.contentEquals("_page") || pn.contentEquals("_skip") || pn.contentEquals("_searchafter")) || // Ensure that either the parameter does not exist in the previous request, // or it has a different value compared to the "next" link's parameter (!previousPageParams.contains(pn) || previousPageParams(pn).toSet != pv.toSet) diff --git a/onfhir-common/src/main/scala/io/onfhir/api/api.scala b/onfhir-common/src/main/scala/io/onfhir/api/api.scala index 177775c9..b46212e1 100644 --- a/onfhir-common/src/main/scala/io/onfhir/api/api.scala +++ b/onfhir-common/src/main/scala/io/onfhir/api/api.scala @@ -405,6 +405,7 @@ package object api { val ABOVE = ":above" val BELOW = ":below" val TEXT = ":text" + val CODE_TEXT = ":code-text" val NOT = ":not" val TYPE = ":type" val IDENTIFIER = ":identifier" diff --git a/onfhir-common/src/main/scala/io/onfhir/api/parsers/FHIRSearchParameterValueParser.scala b/onfhir-common/src/main/scala/io/onfhir/api/parsers/FHIRSearchParameterValueParser.scala index 421eed64..46253a9d 100644 --- a/onfhir-common/src/main/scala/io/onfhir/api/parsers/FHIRSearchParameterValueParser.scala +++ b/onfhir-common/src/main/scala/io/onfhir/api/parsers/FHIRSearchParameterValueParser.scala @@ -260,9 +260,10 @@ class FHIRSearchParameterValueParser(fhirConfig: FhirServerConfig) { .getOrElse(Set.empty) possibleTargets.size match { case 1 => possibleTargets.head - case _ => throw new InvalidParameterException(s"Invalid usage of chained search '$nameExpr', need type discriminator for parameter '$pname'! Syntax is erroneous, please see https://build.fhir.org/search.html#chaining !") + case _ => + throw new InvalidParameterException(s"Invalid usage of chained search '$nameExpr', need type discriminator for parameter '$pname'! Syntax is erroneous, please see https://build.fhir.org/search.html#chaining !") } - case 2 => chains.last.last + case 2 => c.last case _ => throw new InvalidParameterException(s"Invalid usage of chained search '$nameExpr'! Syntax is erroneous, please see https://build.fhir.org/search.html#chaining !") } (lastTempRType, pname) diff --git a/onfhir-common/src/main/scala/io/onfhir/config/ResourceConf.scala b/onfhir-common/src/main/scala/io/onfhir/config/ResourceConf.scala index 11a73a7b..ec697db3 100644 --- a/onfhir-common/src/main/scala/io/onfhir/config/ResourceConf.scala +++ b/onfhir-common/src/main/scala/io/onfhir/config/ResourceConf.scala @@ -22,18 +22,18 @@ import io.onfhir.api.model.InternalEntity * @param referencePolicies How this resource type uses FHIR references i.e. literal | logical | resolves | enforced | local */ case class ResourceConf(resource:String, - profile:Option[String], - supportedProfiles:Set[String], - interactions:Set[String], - searchParams:Set[String], - versioning:String, - readHistory:Boolean, - updateCreate:Boolean, - conditionalCreate:Boolean, - conditionalRead:String, - conditionalUpdate:Boolean, - conditionalDelete:String, - searchInclude:Set[String], - searchRevInclude:Set[String], + profile:Option[String] = None, + supportedProfiles:Set[String] = Set.empty, + interactions:Set[String] =Set.empty, + searchParams:Set[String] = Set.empty, + versioning:String = "no-version", + readHistory:Boolean = false, + updateCreate:Boolean = false, + conditionalCreate:Boolean = false, + conditionalRead:String = "not-supported", + conditionalUpdate:Boolean = false, + conditionalDelete:String = "not-supported", + searchInclude:Set[String] = Set.empty, + searchRevInclude:Set[String] = Set.empty, referencePolicies:Set[String] = Set.empty[String] ) extends InternalEntity diff --git a/onfhir-core/src/main/resources/application.conf b/onfhir-core/src/main/resources/application.conf index 5e316abb..0a6304e4 100644 --- a/onfhir-core/src/main/resources/application.conf +++ b/onfhir-core/src/main/resources/application.conf @@ -85,7 +85,7 @@ fhir { search-total = "accurate" # Default pagination mechanism # 'page' --> Page based pagination e.g. _count=50&_page=4 - # 'offset' --> Offset based pagination e.g. _count=500&_searchafter=65156168498 + # 'offset' --> Offset/Cursor based pagination (cursor is MongoDB _id of resource) e.g. _count=500&_searchafter=65156168498 pagination = "page" # Default value for [CapabilityStatement|Conformance].rest.resource.readHistory when not present in CapabilityStatenent for the resource type # Indicates whether server can return past versions for FHIR vRead interaction. See https://www.hl7.org/fhir/capabilitystatement-definitions.html#CapabilityStatement.rest.resource.readHistory diff --git a/onfhir-core/src/main/scala/io/onfhir/db/AggregationUtil.scala b/onfhir-core/src/main/scala/io/onfhir/db/AggregationUtil.scala index 68dc3fcb..cd65c7f8 100644 --- a/onfhir-core/src/main/scala/io/onfhir/db/AggregationUtil.scala +++ b/onfhir-core/src/main/scala/io/onfhir/db/AggregationUtil.scala @@ -1,5 +1,7 @@ package io.onfhir.db +import org.mongodb.scala.bson.collection.immutable.Document +import org.mongodb.scala.bson.conversions.Bson import org.mongodb.scala.bson.{BsonArray, BsonDocument, BsonInt32, BsonString, BsonValue} object AggregationUtil { @@ -124,4 +126,73 @@ object AggregationUtil { ) ) } + + /** + * Construct Mongodb $lookup phase expression with both fields and pipeline + * @param col Collection name to join + * @param localFieldPath Path to the local field + * @param foreignFieldPath Path to the foreign field + * @param pipeline Pipeline to execute (at least one should be provided) + * @param as The results name + * @return + */ + def constructLookupPhaseExpression(col:String, localFieldPath:String, foreignFieldPath:String, pipeline:Seq[BsonDocument], as:String):BsonDocument = { + BsonDocument( + "$lookup" -> BsonDocument.apply( + "from" -> BsonString(col), + "localField" -> BsonString(localFieldPath), + "foreignField" -> BsonString(foreignFieldPath), + "pipeline" -> BsonArray.fromIterable(pipeline), + "as" -> BsonString(as) + ) + ) + } + + /** + * Construct Mongodb $lookup phase expression with both fields and pipeline and let + * + * @param col Collection name to join + * @param localFieldPath Path to the local field + * @param foreignFieldPath Path to the foreign field + * @param letVariablePathMap Variable name -> path e.g. rid --> id, rtype --> resourceType + * @param pipeline Pipeline to execute (at least one should be provided) + * @param as The results name + * @return + */ + def constructLookupPhaseExpression(col: String, localFieldPath: String, foreignFieldPath: String, letVariablePathMap:Map[String, String], pipeline: Seq[BsonDocument], as: String): BsonDocument = { + BsonDocument( + "$lookup" -> BsonDocument.apply( + "from" -> BsonString(col), + "localField" -> BsonString(localFieldPath), + "foreignField" -> BsonString(foreignFieldPath), + "let" -> BsonDocument(letVariablePathMap.map(v => v._1 -> BsonString("$"+v._2))), + "pipeline" -> BsonArray.fromIterable(pipeline), + "as" -> BsonString(as) + ) + ) + } + + /** + * Construct the Mongodb expression for checking if given array field size is larger than given value + * @param field Field name + * @param size Size + * @return + */ + def constructGreaterThanSizeExpression(field:String, size:Int):BsonDocument = { + BsonDocument(field -> BsonDocument("$gt" -> BsonDocument("$size" -> BsonInt32(size)))) + } + + /** + * + * @param field1 + * @param field2 + * @return + */ + def constructAggEqual(field1:BsonValue, field2:BsonValue):BsonDocument = { + BsonDocument("$eq" -> BsonArray(field1,field2)) + } + + def constructAggNotEqual(field1:BsonValue, field2:BsonValue): BsonDocument = { + BsonDocument("$ne" -> BsonArray(field1,field2)) + } } diff --git a/onfhir-core/src/main/scala/io/onfhir/db/DateQueryBuilder.scala b/onfhir-core/src/main/scala/io/onfhir/db/DateQueryBuilder.scala new file mode 100644 index 00000000..6a1e70ac --- /dev/null +++ b/onfhir-core/src/main/scala/io/onfhir/db/DateQueryBuilder.scala @@ -0,0 +1,298 @@ +package io.onfhir.db + +import io.onfhir.api.{FHIR_COMMON_FIELDS, FHIR_DATA_TYPES, FHIR_EXTRA_FIELDS, FHIR_PREFIXES_MODIFIERS} +import io.onfhir.api.util.FHIRUtil +import io.onfhir.exception.InternalServerException +import io.onfhir.util.DateTimeUtil +import org.mongodb.scala.bson.{BsonDateTime, BsonValue} +import org.mongodb.scala.bson.conversions.Bson +import org.mongodb.scala.model.Filters + +/** + * Utility class to construct MongoDB queries for FHIR date type search parameters + */ +object DateQueryBuilder extends IFhirQueryBuilder { + + /** + * Construct query for date queries + * @param prefixAndValues + * @param path + * @param targetType + * @return + */ + def getQuery(prefixAndValues: Seq[(String, String)], path: String, targetType: String): Bson = { + //Split the parts + val (elemMatchPath, queryPath) = FHIRUtil.splitElementPathIntoElemMatchAndQueryPaths(path) + orQueries( + prefixAndValues.map { + case (prefix, value) => + // '+' in time zone field is replaced with ' ' by the server + val dateValue = value.replace(" ", "+") + // Construct main query on date object + val mainQuery = + targetType match { + case FHIR_DATA_TYPES.DATE | + FHIR_DATA_TYPES.DATETIME | + FHIR_DATA_TYPES.INSTANT => + getQueryForTimePoint(queryPath.getOrElse(""), dateValue, prefix) + case FHIR_DATA_TYPES.PERIOD => + getQueryForPeriod(queryPath.getOrElse(""), dateValue, prefix, isTiming = false) + case FHIR_DATA_TYPES.TIMING => + //TODO Handle event case better by special query on array (should match for all elements, not or) + Filters.or( + getQueryForTiming(queryPath.getOrElse(""), dateValue, prefix), + getQueryForPeriod(queryPath.getOrElse(""), dateValue, prefix, isTiming = true) + ) + case other => + throw new InternalServerException(s"Unknown target element type $other !!!") + } + + //If an array exist, use elemMatch otherwise return the query + getFinalQuery(elemMatchPath, mainQuery) + } + ) + } + + /** + * Handles prefix for date values(implicit range) for date parameters. + * For further information about using prefixes with range values + * please refer to prefix table's third column in page; + * https://www.hl7.org/fhir/search.html#prefix + * + * @param path absolute path of the parameter + * @param value value of the parameter + * @param prefix prefix of the parameter + * @return BsonDocument for the query + */ + def getQueryForTimePoint(path: String, value: String, prefix: String): Bson = { + // Populate Implicit ranges(e.g. 2010-10-10 represents the range 2010-10-10T00:00Z/2010-10-10T23:59ZZ) + val implicitRanges = DateTimeUtil.populateImplicitDateTimeRanges(value) + // Generate the implicit range paths(i.e. the paths created by the server) + val rangePaths = (FHIRUtil.mergeElementPath(path, FHIR_EXTRA_FIELDS.TIME_RANGE_START), FHIRUtil.mergeElementPath(path, FHIR_EXTRA_FIELDS.TIME_RANGE_END)) + + //If it is a datetime or instant base query + if (value.contains("T")) { + //We handle this specially as onfhir store this in millisecond precision + if (path == "meta.lastUpdated") + getQueryForDateTime(FHIRUtil.mergeElementPath(path, FHIR_EXTRA_FIELDS.TIME_TIMESTAMP), prefix, implicitRanges) + else + // Build dateTime query on date time and period query on implicit ranges and combine them. + Filters.or( + getQueryForDateTime(FHIRUtil.mergeElementPath(path, FHIR_EXTRA_FIELDS.TIME_TIMESTAMP), prefix, implicitRanges), + getQueryForPeriodRange(rangePaths, prefix, implicitRanges) + ) + } else { + if (path == "meta.lastUpdated") + getQueryForDate(FHIRUtil.mergeElementPath(path, FHIR_EXTRA_FIELDS.TIME_DATE), prefix, implicitRanges) + else + //If it is year, year-month, or date query + //Query over the sub date field + Filters.or(getQueryForDate(FHIRUtil.mergeElementPath(path, FHIR_EXTRA_FIELDS.TIME_DATE), prefix, implicitRanges), getQueryForPeriodRange(rangePaths, prefix, implicitRanges)) + } + } + + /** + * Handle prefixes for period parameters. For further information + * about the prefixes with range values please refer to prefix table's + * third column in page; https://www.hl7.org/fhir/search.html#prefix + * + * @param path absolute path of the parameter + * @param value value of the parameter + * @param prefix prefix of the parameter + * @param isTiming determines if the field is timing + * @return BsonDocument for the query + */ + def getQueryForPeriod(path: String, value: String, prefix: String, isTiming: Boolean): Bson = { + // Generate period fields + val periodPath = if (isTiming) FHIRUtil.mergeElementPath(path, s"${FHIR_COMMON_FIELDS.REPEAT}.${FHIR_COMMON_FIELDS.BOUNDS_PERIOD}") else path + val periodRanges = ( + FHIRUtil.mergeElementPath(periodPath, s"${FHIR_COMMON_FIELDS.START}.${FHIR_EXTRA_FIELDS.TIME_TIMESTAMP}"), + FHIRUtil.mergeElementPath(periodPath, s"${FHIR_COMMON_FIELDS.END}.${FHIR_EXTRA_FIELDS.TIME_TIMESTAMP}") + ) + // Populate implicit date ranges(e.g. 2010 represents the range 2010-01-01/2010-12-31) + //val implicitDate = DateTimeUtil.populateImplicitDateRanges(value) + // Populate implicit date time ranges(i.e. same process with the time ranges) + val implicitDateTime = DateTimeUtil.populateImplicitDateTimeRanges(value) + // Generate queries for both date and date time ranges + getQueryForPeriodRange(periodRanges, prefix, implicitDateTime) + //or(periodQueryBuilder(periodRanges, prefix, implicitDate), periodQueryBuilder(periodRanges, prefix, implicitDateTime)) + } + + /** + * Special processing for Timing.event; all elements should satisfy the query + * + * @param path absolute path of the parameter + * @param value value of the parameter + * @param prefix prefix of the parameter + * @return + */ + def getQueryForTiming(path: String, value: String, prefix: String): Bson = { + // Populate Implicit ranges(e.g. 2010-10-10 represents the range 2010-10-10T00:00Z/2010-10-10T23:59ZZ) + val implicitRanges = DateTimeUtil.populateImplicitDateTimeRanges(value) + // Convert implicit range to dateTime objects(inputted values have already been converted to dataTime format) + var (floor, ceil) = (OnFhirBsonTransformer.dateToISODate(implicitRanges._1), OnFhirBsonTransformer.dateToISODate(implicitRanges._2)) + + val subpath = if (value.contains("T")) FHIR_EXTRA_FIELDS.TIME_TIMESTAMP else FHIR_EXTRA_FIELDS.TIME_DATE + val oppositeQuery = prefix match { + case FHIR_PREFIXES_MODIFIERS.BLANK_EQUAL | FHIR_PREFIXES_MODIFIERS.EQUAL => Filters.or(Filters.lt(subpath, floor), Filters.gt(subpath, ceil)) + case FHIR_PREFIXES_MODIFIERS.GREATER_THAN | FHIR_PREFIXES_MODIFIERS.GREATER_THAN_M => Filters.or(Filters.lt(subpath, floor), Filters.and(Filters.gte(subpath, floor), Filters.lt(subpath, ceil)), Filters.equal(path, floor)) + case FHIR_PREFIXES_MODIFIERS.LESS_THAN | FHIR_PREFIXES_MODIFIERS.LESS_THAN_M => Filters.or(Filters.gt(subpath, ceil), Filters.and(Filters.gte(subpath, floor), Filters.lt(subpath, ceil)), Filters.equal(subpath, floor)) + case FHIR_PREFIXES_MODIFIERS.GREATER_THAN_EQUAL => Filters.lt(subpath, floor) + case FHIR_PREFIXES_MODIFIERS.LESS_THAN_EQUAL => Filters.gt(subpath, ceil) + case FHIR_PREFIXES_MODIFIERS.NOT_EQUAL => Filters.or(Filters.lt(subpath, floor), Filters.gt(subpath, ceil)) + case FHIR_PREFIXES_MODIFIERS.STARTS_AFTER => Filters.or(Filters.lt(subpath, ceil), Filters.equal(subpath, ceil)) + case FHIR_PREFIXES_MODIFIERS.ENDS_BEFORE => Filters.or(Filters.gt(subpath, floor), Filters.equal(subpath, floor)) + case FHIR_PREFIXES_MODIFIERS.APPROXIMATE => + if (ceil == floor) + Filters.or(Filters.lt(subpath, floor), Filters.gt(subpath, ceil)) + else { + val delta: Long = ((ceil.asInstanceOf[BsonDateTime].getValue - floor.asInstanceOf[BsonDateTime].getValue) * 0.1).toLong + ceil = BsonDateTime.apply(ceil.asInstanceOf[BsonDateTime].getValue + delta) + floor = BsonDateTime.apply(floor.asInstanceOf[BsonDateTime].getValue - delta) + Filters.or(Filters.lt(subpath, floor), Filters.gt(subpath, ceil)) + } + } + + val (fieldStart, fieldEnd) = (FHIR_EXTRA_FIELDS.TIME_RANGE_START, FHIR_EXTRA_FIELDS.TIME_RANGE_END) + val oppositeQuery2 = prefix match { + case FHIR_PREFIXES_MODIFIERS.BLANK_EQUAL | FHIR_PREFIXES_MODIFIERS.EQUAL => Filters.or(Filters.lt(fieldStart, floor), Filters.gt(fieldEnd, ceil)) + case FHIR_PREFIXES_MODIFIERS.GREATER_THAN | FHIR_PREFIXES_MODIFIERS.GREATER_THAN_M => Filters.lte(fieldEnd, ceil) + case FHIR_PREFIXES_MODIFIERS.LESS_THAN | FHIR_PREFIXES_MODIFIERS.LESS_THAN_M => Filters.gte(fieldStart, floor) + case FHIR_PREFIXES_MODIFIERS.GREATER_THAN_EQUAL => Filters.or(Filters.lte(fieldEnd, ceil), Filters.lt(fieldStart, floor), Filters.gt(fieldEnd, ceil)) + case FHIR_PREFIXES_MODIFIERS.LESS_THAN_EQUAL => Filters.or(Filters.gte(fieldStart, floor), Filters.lt(fieldStart, floor), Filters.gt(fieldEnd, ceil)) + case FHIR_PREFIXES_MODIFIERS.NOT_EQUAL => Filters.or(Filters.lt(fieldStart, floor), Filters.gt(fieldEnd, ceil)) + case FHIR_PREFIXES_MODIFIERS.STARTS_AFTER => Filters.lte(fieldStart, ceil) + case FHIR_PREFIXES_MODIFIERS.ENDS_BEFORE => Filters.gte(fieldEnd, floor) + case FHIR_PREFIXES_MODIFIERS.APPROXIMATE => + if (ceil == floor) + Filters.or(Filters.lt(fieldStart, floor), Filters.gt(fieldEnd, ceil)) + else { + val delta: Long = ((ceil.asInstanceOf[BsonDateTime].getValue - floor.asInstanceOf[BsonDateTime].getValue) * 0.1).toLong + ceil = BsonDateTime.apply(ceil.asInstanceOf[BsonDateTime].getValue + delta) + floor = BsonDateTime.apply(floor.asInstanceOf[BsonDateTime].getValue - delta) + Filters.or(Filters.lt(fieldStart, floor), Filters.gt(fieldEnd, ceil)) + } + } + + Filters.and( + Filters.exists(FHIRUtil.mergeElementPath(path, "event")), + if (prefix == FHIR_PREFIXES_MODIFIERS.NOT_EQUAL) + Filters.elemMatch(FHIRUtil.mergeElementPath(path, "event"), Filters.or(oppositeQuery, oppositeQuery2)) + else + Filters.nor(Filters.elemMatch(FHIRUtil.mergeElementPath(path, "event"), Filters.or(oppositeQuery, oppositeQuery2))) + ) + } + + /** + * Query builders for dateTime type searches e.g. ge2012-10-15T10:00:00Z + * + * @param path path to the target value + * @param prefix prefix of the date + * @param valueRange value of lower and upper boundaries + * @return BsonDocument for the target query + */ + private def getQueryForDateTime(path: String, prefix: String, valueRange: (String, String)): Bson = { + // Convert implicit range to dateTime objects(inputted values have already been converted to dataTime format) + var (floor, ceil) = (OnFhirBsonTransformer.dateToISODate(valueRange._1), OnFhirBsonTransformer.dateToISODate(valueRange._2)) + prefix match { + case FHIR_PREFIXES_MODIFIERS.BLANK_EQUAL | FHIR_PREFIXES_MODIFIERS.EQUAL => Filters.or(Filters.and(Filters.gte(path, floor), Filters.lt(path, ceil)), Filters.equal(path, floor)) + case FHIR_PREFIXES_MODIFIERS.GREATER_THAN | FHIR_PREFIXES_MODIFIERS.GREATER_THAN_M => Filters.gt(path, ceil) + case FHIR_PREFIXES_MODIFIERS.LESS_THAN | FHIR_PREFIXES_MODIFIERS.LESS_THAN_M => Filters.lt(path, floor) + case FHIR_PREFIXES_MODIFIERS.GREATER_THAN_EQUAL => Filters.or(getQueryForDateTime(path, FHIR_PREFIXES_MODIFIERS.EQUAL, valueRange), getQueryForDateTime(path, FHIR_PREFIXES_MODIFIERS.GREATER_THAN, valueRange)) + case FHIR_PREFIXES_MODIFIERS.LESS_THAN_EQUAL => Filters.or(getQueryForDateTime(path, FHIR_PREFIXES_MODIFIERS.EQUAL, valueRange), getQueryForDateTime(path, FHIR_PREFIXES_MODIFIERS.LESS_THAN, valueRange)) + case FHIR_PREFIXES_MODIFIERS.NOT_EQUAL => Filters.or(Filters.lt(path, floor), Filters.gt(path, ceil)) + case FHIR_PREFIXES_MODIFIERS.STARTS_AFTER => Filters.gt(path, ceil) + //or(dateTimeQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.NOT_EQUAL, valueRange), dateTimeQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.LESS_THAN, valueRange)) + case FHIR_PREFIXES_MODIFIERS.ENDS_BEFORE => Filters.lt(path, floor) + //or(dateTimeQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.NOT_EQUAL, valueRange), dateTimeQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.GREATER_THAN, valueRange)) + case FHIR_PREFIXES_MODIFIERS.APPROXIMATE => + if (ceil == floor) + Filters.or(Filters.and(Filters.gte(path, floor), Filters.lt(path, ceil)), Filters.equal(path, floor)) + else { + val delta: Long = ((ceil.asInstanceOf[BsonDateTime].getValue - floor.asInstanceOf[BsonDateTime].getValue) * 0.1).toLong + ceil = BsonDateTime.apply(ceil.asInstanceOf[BsonDateTime].getValue + delta) + floor = BsonDateTime.apply(floor.asInstanceOf[BsonDateTime].getValue - delta) + Filters.or(Filters.and(Filters.gte(path, floor), Filters.lt(path, ceil)), Filters.equal(path, floor)) + } + } + } + + /** + * Query builders for period type searches + * + * @param path path to the lower and upper boundaries + * @param prefix prefix for comparison + * @param valueRange value of lower and upper boundaries + * @return BsonDocument for the target query + */ + private def getQueryForPeriodRange(path: (String, String), prefix: String, valueRange: (String, String)): Bson = { + val isoDate: (BsonValue, BsonValue) = (OnFhirBsonTransformer.dateToISODate(valueRange._1), OnFhirBsonTransformer.dateToISODate(valueRange._2)) + + // Initiliaze start and end fields of the ranges + val (fieldStart, fieldEnd) = path + // Implicit date range + var (floor, ceil) = isoDate + // BsonDocuments that represent the nonexistence of boundary values + val fieldEndNotExist = Filters.and(Filters.exists(fieldStart, exists = true), Filters.exists(fieldEnd, exists = false)) + val fieldStartNotExist = Filters.and(Filters.exists(fieldStart, exists = false), Filters.exists(fieldEnd, exists = true)) + + // Prefix matching and query generation + prefix match { + case FHIR_PREFIXES_MODIFIERS.BLANK_EQUAL | FHIR_PREFIXES_MODIFIERS.EQUAL => Filters.and(Filters.gte(fieldStart, floor), Filters.lte(fieldEnd, ceil)) + case FHIR_PREFIXES_MODIFIERS.GREATER_THAN | FHIR_PREFIXES_MODIFIERS.GREATER_THAN_M => Filters.or(Filters.gt(fieldEnd, ceil), fieldEndNotExist) + case FHIR_PREFIXES_MODIFIERS.LESS_THAN | FHIR_PREFIXES_MODIFIERS.LESS_THAN_M => Filters.or(Filters.lt(fieldStart, floor), fieldStartNotExist) + case FHIR_PREFIXES_MODIFIERS.GREATER_THAN_EQUAL => Filters.or(getQueryForPeriodRange(path, FHIR_PREFIXES_MODIFIERS.EQUAL, valueRange), getQueryForPeriodRange(path, FHIR_PREFIXES_MODIFIERS.GREATER_THAN, valueRange)) + case FHIR_PREFIXES_MODIFIERS.LESS_THAN_EQUAL => Filters.or(getQueryForPeriodRange(path, FHIR_PREFIXES_MODIFIERS.EQUAL, valueRange), getQueryForPeriodRange(path, FHIR_PREFIXES_MODIFIERS.LESS_THAN, valueRange)) + case FHIR_PREFIXES_MODIFIERS.NOT_EQUAL => Filters.or(getQueryForPeriodRange(path, FHIR_PREFIXES_MODIFIERS.LESS_THAN, valueRange), getQueryForPeriodRange(path, FHIR_PREFIXES_MODIFIERS.GREATER_THAN, valueRange)) + case FHIR_PREFIXES_MODIFIERS.STARTS_AFTER => Filters.gt(fieldStart, ceil) + //or(periodQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.NOT_EQUAL, valueRange), periodQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.LESS_THAN, valueRange)) + case FHIR_PREFIXES_MODIFIERS.ENDS_BEFORE => Filters.lt(fieldEnd, floor) + //or(periodQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.NOT_EQUAL, valueRange), periodQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.GREATER_THAN, valueRange)) + case FHIR_PREFIXES_MODIFIERS.APPROXIMATE => + if (ceil == floor) + Filters.and(Filters.gte(fieldStart, floor), Filters.lte(fieldEnd, ceil)) + else { + val delta: Long = ((ceil.asInstanceOf[BsonDateTime].getValue - floor.asInstanceOf[BsonDateTime].getValue) * 0.1).toLong + ceil = BsonDateTime.apply(ceil.asInstanceOf[BsonDateTime].getValue + delta) + floor = BsonDateTime.apply(floor.asInstanceOf[BsonDateTime].getValue - delta) + Filters.and(Filters.gte(fieldStart, floor), Filters.lte(fieldEnd, ceil)) + } + } + } + + + /** + * Query builders for date type searches e.g. ge2012-10-15 or eq2012-05 + * + * @param path path to the target value + * @param prefix prefix of the date + * @param valueRange value of lower and upper boundaries + * @return BsonDocument for the target query + */ + private def getQueryForDate(path: String, prefix: String, valueRange: (String, String)): Bson = { + // Convert implicit range to dateTime objects(inputted values have already been converted to dataTime format) + var (floor, ceil) = (OnFhirBsonTransformer.dateToISODate(valueRange._1), OnFhirBsonTransformer.dateToISODate(valueRange._2)) + prefix match { + case FHIR_PREFIXES_MODIFIERS.BLANK_EQUAL | FHIR_PREFIXES_MODIFIERS.EQUAL => Filters.or(Filters.and(Filters.gte(path, floor), Filters.lt(path, ceil)), Filters.equal(path, floor)) + case FHIR_PREFIXES_MODIFIERS.GREATER_THAN | FHIR_PREFIXES_MODIFIERS.GREATER_THAN_M => Filters.gt(path, ceil) + case FHIR_PREFIXES_MODIFIERS.LESS_THAN | FHIR_PREFIXES_MODIFIERS.LESS_THAN_M => Filters.lt(path, floor) + case FHIR_PREFIXES_MODIFIERS.GREATER_THAN_EQUAL => Filters.gte(path, floor) + case FHIR_PREFIXES_MODIFIERS.LESS_THAN_EQUAL => Filters.lte(path, ceil) + case FHIR_PREFIXES_MODIFIERS.NOT_EQUAL => Filters.or(Filters.lt(path, floor), Filters.gt(path, ceil)) + case FHIR_PREFIXES_MODIFIERS.STARTS_AFTER => Filters.gt(path, ceil) + //or(dateTimeQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.NOT_EQUAL, valueRange), dateTimeQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.LESS_THAN, valueRange)) + case FHIR_PREFIXES_MODIFIERS.ENDS_BEFORE => Filters.lt(path, floor) + //or(dateTimeQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.NOT_EQUAL, valueRange), dateTimeQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.GREATER_THAN, valueRange)) + case FHIR_PREFIXES_MODIFIERS.APPROXIMATE => + if (ceil == floor) + Filters.or(Filters.and(Filters.gte(path, floor), Filters.lt(path, ceil)), Filters.equal(path, floor)) + else { + val delta: Long = ((ceil.asInstanceOf[BsonDateTime].getValue - floor.asInstanceOf[BsonDateTime].getValue) * 0.1).toLong + ceil = BsonDateTime.apply(ceil.asInstanceOf[BsonDateTime].getValue + delta) + floor = BsonDateTime.apply(floor.asInstanceOf[BsonDateTime].getValue - delta) + Filters.or(Filters.and(Filters.gte(path, floor), Filters.lt(path, ceil)), Filters.equal(path, floor)) + } + } + } + +} diff --git a/onfhir-core/src/main/scala/io/onfhir/db/DocumentManager.scala b/onfhir-core/src/main/scala/io/onfhir/db/DocumentManager.scala index c824373a..bee34822 100644 --- a/onfhir-core/src/main/scala/io/onfhir/db/DocumentManager.scala +++ b/onfhir-core/src/main/scala/io/onfhir/db/DocumentManager.scala @@ -163,15 +163,19 @@ object DocumentManager { */ def searchDocuments(rtype:String, filter:Option[Bson], + aggFilteringStages:Seq[Bson], count:Int = -1, page:Int= 1, sortingPaths:Seq[(String, Int, Seq[String])] = Seq.empty, includingOrExcludingFields:Option[(Boolean, Set[String])] = None, excludeExtraFields:Boolean = false)(implicit transactionSession: Option[TransactionSession] = None):Future[Seq[Document]] = { - //If we have alternative paths for a search parameter, use search by aggregation - if(sortingPaths.exists(_._3.length > 1)){ - searchDocumentsByAggregation(rtype, filter, count, page, sortingPaths, includingOrExcludingFields, excludeExtraFields) + val needAggregationPipeline = + sortingPaths.exists(_._3.length > 1) || //If we have alternative paths for a search parameter, use search by aggregation + aggFilteringStages.nonEmpty + + if(needAggregationPipeline){ + searchDocumentsByAggregation(rtype, filter,aggFilteringStages, count, page, sortingPaths, includingOrExcludingFields, excludeExtraFields) } //Otherwise run a normal MongoDB find query else { @@ -218,6 +222,7 @@ object DocumentManager { */ def searchDocumentsWithOffset(rtype: String, filter: Option[Bson], + filteringStages:Seq[Bson], count: Int = -1, offset:Option[(Seq[String],Boolean)], sortingPaths: Seq[(String, Int, Seq[String])] = Seq.empty, @@ -236,42 +241,50 @@ object DocumentManager { finalFilter = filter.map(f => and(f,of)).orElse(Some(of)) ) - //If we have alternative paths for a search parameter, use search by aggregation + //We don't allow sorting for cursor based pagination if (sortingPaths.nonEmpty) { throw new NotImplementedError("Sorting is not implemented for pagination with offset!") } else { - //Construct query - var query = transactionSession match { - case None => if (finalFilter.isDefined) MongoDB.getCollection(rtype).find(finalFilter.get) else MongoDB.getCollection(rtype).find() - case Some(ts) => if (finalFilter.isDefined) MongoDB.getCollection(rtype).find(ts.dbSession, finalFilter.get) else MongoDB.getCollection(rtype).find(ts.dbSession) + val resultsFuture = { + //If there are + if (filteringStages.nonEmpty) { + searchDocumentsByAggregationForOffsetBasedPagination(rtype, finalFilter, filteringStages, count, includingOrExcludingFields, excludeExtraFields) + } else { + //Construct query + var query = transactionSession match { + case None => if (finalFilter.isDefined) MongoDB.getCollection(rtype).find(finalFilter.get) else MongoDB.getCollection(rtype).find() + case Some(ts) => if (finalFilter.isDefined) MongoDB.getCollection(rtype).find(ts.dbSession, finalFilter.get) else MongoDB.getCollection(rtype).find(ts.dbSession) + } + //Sort on id + query = + query + .sort(ascending(FHIR_COMMON_FIELDS.MONGO_ID)) + .limit(count) + + //Handle projection + query = handleProjection(query, includingOrExcludingFields, excludeExtraFields, exceptMongoId = true) + + //Execute the query + query + .toFuture() + } } - //Sort on id - query = - query - .sort(if(offset.forall(_._2)) ascending(FHIR_COMMON_FIELDS.MONGO_ID) else descending(FHIR_COMMON_FIELDS.MONGO_ID)) - .limit(count) - - //Handle projection - query = handleProjection(query, includingOrExcludingFields, excludeExtraFields, exceptMongoId = true) - //Execute the query - query - .toFuture() + resultsFuture .map(docs => { - var finalDocs = if(offset.forall(_._2)) docs else docs.reverse + var finalDocs = if (offset.forall(_._2)) docs else docs.reverse val offsetBefore = finalDocs.headOption.map(d => d.getObjectId(FHIR_COMMON_FIELDS.MONGO_ID).toString).toSeq val offsetAfter = finalDocs.lastOption.map(d => d.getObjectId(FHIR_COMMON_FIELDS.MONGO_ID).toString).toSeq - finalDocs = if(excludeExtraFields) finalDocs.map(d => d.filter(_._1 == FHIR_COMMON_FIELDS.MONGO_ID)) else finalDocs + finalDocs = if (excludeExtraFields) finalDocs.map(d => d.filter(_._1 == FHIR_COMMON_FIELDS.MONGO_ID)) else finalDocs (offsetBefore, offsetAfter, finalDocs) - }) } } /** * Searches and finds document(s) according to given query, pagination, sorting parameters on multiple resource types - * @param filters Mongo Filter constructed for each resource type + * @param filters Mongo Filter and filtering aggregation phases constructed for each resource type * @param count Limit for # of resources to be returned * @param page Page number * @param sortingPaths Sorting parameters and sorting direction (negative: descending, positive: ascending) for each resource type @@ -282,7 +295,7 @@ object DocumentManager { * @return */ def searchDocumentsFromMultipleCollection( - filters:Map[String, Option[Bson]], + filters:Map[String, (Option[Bson], Seq[Bson])], count:Int = -1, page:Int= 1, sortingPaths:Map[String, Seq[(String, Int, Seq[String])]] = Map.empty, @@ -290,19 +303,20 @@ object DocumentManager { excludeExtraFields:Boolean = false)(implicit transactionSession: Option[TransactionSession] = None):Future[Seq[Document]] = { //Internal method to construct aggregation pipeline for each resource type - def constructAggQueryForResourceType(filter:Option[Bson], sortingPaths:Seq[(String, Int, Seq[String])], includingOrExcludingFields:Option[(Boolean, Set[String])]):ListBuffer[Bson] = { + def constructAggQueryForResourceType(filter:Option[Bson], filteringStages:Seq[Bson], sortingPaths:Seq[(String, Int, Seq[String])], includingOrExcludingFields:Option[(Boolean, Set[String])]):ListBuffer[Bson] = { val aggregations = new ListBuffer[Bson] //First, append the match query if(filter.isDefined) aggregations.append(Aggregates.`match`(filter.get)) + //Add the filtering stages + filteringStages.foreach(s => aggregations.append(s)) if(sortingPaths.nonEmpty) { //Then add common sorting field for sort parameters that has multiple alternative paths aggregations.appendAll(sortingPaths.map(sp => addFieldAggregationForParamWithAlternativeSorting(sp))) } //Handle projections (summary and extra fields) - if(includingOrExcludingFields.isDefined || excludeExtraFields) - aggregations.append(handleProjectionForAggregationSearch(includingOrExcludingFields, excludeExtraFields, Set.empty)) + handleProjectionForAggregationSearch(includingOrExcludingFields, excludeExtraFields, Set.empty).foreach(prj => aggregations.append(prj)) aggregations } @@ -310,7 +324,7 @@ object DocumentManager { //Construct an aggregation pipeline for each resource type val aggregatesForEachResourceType = filters - .map(f => f._1 -> constructAggQueryForResourceType(f._2, sortingPaths(f._1), includingOrExcludingFields(f._1))) + .map(f => f._1 -> constructAggQueryForResourceType(f._2._1, f._2._2, sortingPaths(f._1), includingOrExcludingFields(f._1))) //We will start from the first resource type val firstAggregation = aggregatesForEachResourceType.head @@ -349,6 +363,117 @@ object DocumentManager { } } + + /** + * Searching documents by aggregation pipeline to handle some complex sorting + * + * @param rtype type of the resource + * @param filter query filter for desired resource + * @param aggPhasesForFiltering Further aggregation phases for filtering (handling of chaining, reverse chaining or special params like _list) + * @param count limit for # of resources to be returned + * @param page page number + * @param sortingPaths Sorting parameters and sorting direction (negative: descending, positive: ascending) + * @param includingOrExcludingFields List of to be specifically included or excluded fields in the resulting document, if not given; all document included (true-> include, false -> exclude) + * @param excludeExtraFields If true exclude extra fields related with version control from the document + * @return Two sequence of documents (matched, includes); First the matched documents for the main query, Second included resources + */ + def searchDocumentsByAggregation(rtype: String, + filter: Option[Bson], + aggPhasesForFiltering:Seq[Bson], + count: Int = -1, + page: Int = 1, + sortingPaths: Seq[(String, Int, Seq[String])] = Seq.empty, + includingOrExcludingFields: Option[(Boolean, Set[String])] = None, + excludeExtraFields: Boolean = false)(implicit transactionSession: Option[TransactionSession] = None): Future[Seq[Document]] = { + val aggregations = new ListBuffer[Bson] + + //First, append the match query + if (filter.isDefined) + aggregations.append(Aggregates.`match`(filter.get)) + + //Add the phases + aggPhasesForFiltering.foreach(p => aggregations.append(p)) + + + //Identify the sorting params that has alternative paths + val paramsWithAlternativeSorting = + sortingPaths + .filter(_._3.length > 1) //Those that have multiple paths + + //Then add common sorting field for sort parameters that has multiple alternative paths + aggregations.appendAll(paramsWithAlternativeSorting.map(sp => addFieldAggregationForParamWithAlternativeSorting(sp))) + + //Add sorting aggregations + val sorts = + (sortingPaths :+ ("_id", 1, Seq("_id"))) //Finally sort on MongoDB _id to ensure uniqueness for pagination + .map(sp => sp._3.length match { + //For single alternative path, sort it + case 1 => + if (sp._2 > 0) ascending(sp._3.head) else descending(sp._3.head) + //For multiple alternatives, sort against the added field which is based on parameter name + case _ => + if (sp._2 > 0) ascending(s"__sort_${sp._1}") else descending(s"__sort_${sp._1}") + }) + if (sorts.nonEmpty) + aggregations.append(Aggregates.sort(Sorts.orderBy(sorts: _*))) + + + //Handle paging parameters + if (count != -1) { + aggregations.append(Aggregates.skip((page - 1) * count)) + aggregations.append(Aggregates.limit(count)) + } + + //Handle projections + val extraSortingFieldsToExclude = paramsWithAlternativeSorting.map(sp => s"__sort_${sp._1}") + handleProjectionForAggregationSearch(includingOrExcludingFields, excludeExtraFields, extraSortingFieldsToExclude.toSet) + .foreach(prj => aggregations.append(prj)) + + transactionSession match { + case None => MongoDB.getCollection(rtype).aggregate(aggregations.toSeq).toFuture() + case Some(ts) => MongoDB.getCollection(rtype).aggregate(ts.dbSession, aggregations.toSeq).toFuture() + } + } + + /** + * Search by aggregation pipeline for offset based pagination + * + * @param rtype type of the resource + * @param filter query filter for desired resource + * @param aggPhasesForFiltering Further aggregation phases for filtering (handling of chaining, reverse chaining or special params like _list) + * @param count limit for # of resources to be returned + * @param includingOrExcludingFields List of to be specifically included or excluded fields in the resulting document, if not given; all document included (true-> include, false -> exclude) + * @param excludeExtraFields If true exclude extra fields related with version control from the document + * @param transactionSession + * @return + */ + def searchDocumentsByAggregationForOffsetBasedPagination(rtype: String, + filter: Option[Bson], + aggPhasesForFiltering: Seq[Bson], + count: Int = -1, + includingOrExcludingFields: Option[(Boolean, Set[String])] = None, + excludeExtraFields: Boolean = false)(implicit transactionSession: Option[TransactionSession] = None): Future[Seq[Document]] = { + val aggregations = new ListBuffer[Bson] + + //First, append the match query + if (filter.isDefined) + aggregations.append(Aggregates.`match`(filter.get)) + + //Add the phases + aggPhasesForFiltering.foreach(p => aggregations.append(p)) + + if(count != -1) { + aggregations.append(Aggregates.sort(Sorts.ascending("_id"))) + aggregations.append(Aggregates.limit(count)) + } + + transactionSession match { + case None => MongoDB.getCollection(rtype).aggregate(aggregations.toSeq).toFuture() + case Some(ts) => MongoDB.getCollection(rtype).aggregate(ts.dbSession, aggregations.toSeq).toFuture() + } + } + +/* /** * Searching documents by aggregation pipeline to handle some complex sorting * @param rtype type of the resource @@ -429,7 +554,7 @@ object DocumentManager { case None => MongoDB.getCollection(rtype).aggregate(aggregations.toSeq).toFuture() case Some(ts) => MongoDB.getCollection(rtype).aggregate(ts.dbSession, aggregations.toSeq).toFuture() } - } + }*/ /** @@ -475,25 +600,27 @@ object DocumentManager { * @param addedFields All added fields for aggregation * @return */ - private def handleProjectionForAggregationSearch(includingOrExcludingFields:Option[(Boolean, Set[String])], excludeExtraFields:Boolean, addedFields:Set[String]):Bson = { + private def handleProjectionForAggregationSearch(includingOrExcludingFields:Option[(Boolean, Set[String])], excludeExtraFields:Boolean, addedFields:Set[String]):Option[Bson] = { includingOrExcludingFields match { //Nothing given, so we include all normal FHIR elements case None => //If we exclude the extra fields, exclude them if(excludeExtraFields) - Aggregates.project(exclude((ONFHIR_EXTRA_FIELDS ++ addedFields).toSeq:_*)) + Some(Aggregates.project(exclude((ONFHIR_EXTRA_FIELDS ++ addedFields).toSeq:_*))) + else if(addedFields.nonEmpty) + Some(Aggregates.project(exclude(addedFields.toSeq: _*))) else - Aggregates.project(exclude(addedFields.toSeq: _*)) + None //No need for projection //Specific inclusion case Some((true, fields)) => //If we don't exclude the extra fields, include them to final inclusion set val finalIncludes = if(excludeExtraFields) fields ++ FHIR_MANDATORY_ELEMENTS else fields ++ FHIR_MANDATORY_ELEMENTS ++ ONFHIR_EXTRA_FIELDS - Aggregates.project(include(finalIncludes.toSeq :_*)) + Some(Aggregates.project(include(finalIncludes.toSeq :_*))) //Specific exclusion case Some((false, fields)) => val finalExcludes = if(excludeExtraFields) fields ++ ONFHIR_EXTRA_FIELDS else fields - Aggregates.project(exclude((finalExcludes ++ addedFields).toSeq :_*)) + Some(Aggregates.project(exclude((finalExcludes ++ addedFields).toSeq :_*))) } } /** @@ -536,11 +663,14 @@ object DocumentManager { * @param query Mongo query * @return */ - def countDocuments(rtype:String, query:Option[Bson])(implicit transactionSession: Option[TransactionSession] = None): Future[Long] = { - getCount( - rtype, - query - ) + def countDocuments(rtype:String, query:Option[Bson], filteringStages:Seq[Bson] = Nil)(implicit transactionSession: Option[TransactionSession] = None): Future[Long] = { + if(filteringStages.isEmpty) + getCount( + rtype, + query + ) + else + getCountWithAgg(rtype, query, filteringStages, history = false) } /** @@ -549,9 +679,9 @@ object DocumentManager { * @param transactionSession * @return */ - def countDocumentsFromMultipleCollections(queries:Map[String, Option[Bson]])(implicit transactionSession: Option[TransactionSession] = None): Future[Long] = { + def countDocumentsFromMultipleCollections(queries:Map[String, (Option[Bson], Seq[Bson])])(implicit transactionSession: Option[TransactionSession] = None): Future[Long] = { Future - .sequence(queries.map(q => countDocuments(q._1, q._2))) + .sequence(queries.map(q => countDocuments(q._1, q._2._1, q._2._2))) .map(counts => counts.sum) } @@ -592,6 +722,37 @@ object DocumentManager { } } + /** + * Count documents by using MongoDb aggregation pipeline + * + * @param rtype Resource type + * @param filter Filter for query + * @param stages Further filtering stages + * @param history If this search is on history + * @param transactionSession Transaction session if exists + * @return + */ + private def getCountWithAgg(rtype:String, filter:Option[Bson], stages:Seq[Bson], history:Boolean = false)(implicit transactionSession: Option[TransactionSession] = None): Future[Long] = { + val aggregations = new ListBuffer[Bson] + //Add filter + filter.foreach(f => aggregations.append(Aggregates.`match`(f))) + //Add aggregation stages + stages.foreach(s => aggregations.append(s)) + //Count the documents + aggregations.append(Aggregates.count()) + + val countResult = + transactionSession match { + case None => MongoDB.getCollection(rtype).aggregate(aggregations.toSeq).toFuture() + case Some(ts) => MongoDB.getCollection(rtype).aggregate(ts.dbSession, aggregations.toSeq).toFuture() + } + //Return the result + countResult.map { + case Nil => 0L + case Seq(r) => r.getInteger("count").toLong + } + } + /** * Search documents but return their resource ids * @param rtype Resource type @@ -603,11 +764,12 @@ object DocumentManager { */ def searchDocumentsReturnIds(rtype:String, filter:Bson, + filteringStages:Seq[Bson], count:Int = -1, page:Int= -1, sortingPaths:Seq[(String, Int, Seq[String])] = Seq.empty) (implicit transactionSession: Option[TransactionSession] = None):Future[Seq[String]] = { - searchDocuments(rtype, Some(filter), count, page, sortingPaths, includingOrExcludingFields = Some(true -> Set(FHIR_COMMON_FIELDS.ID))) + searchDocuments(rtype, Some(filter),filteringStages, count, page, sortingPaths, includingOrExcludingFields = Some(true -> Set(FHIR_COMMON_FIELDS.ID))) .map(_.map(_.getString(FHIR_COMMON_FIELDS.ID))) } @@ -621,7 +783,7 @@ object DocumentManager { * @param skip Number of records to be skipped * @return */ - def getHistoricLast(rtype:String, rid:Option[String], filter:Option[Bson], count:Int = -1, skip:Int = -1):Future[Seq[Document]] = { + def getHistoricLast(rtype:String, rid:Option[String], filter:Option[Bson], filteringStages:Seq[Bson], count:Int = -1, skip:Int = -1):Future[Seq[Document]] = { val collection = MongoDB.getCollection(rtype, true) val finalFilter = andQueries(rid.map(ridQuery).toSeq ++ filter.toSeq) @@ -629,6 +791,9 @@ object DocumentManager { //Filter with the query finalFilter.foreach(ff => aggregations.append(Aggregates.filter(ff))) + //Add the filtering stages + filteringStages.foreach(s => aggregations.append(s)) + //Sort according to version in descending order aggregations.append(Aggregates.sort(descending("meta.versionId"))) //Group by resource id and get the first for each group (means the biggest version) @@ -652,7 +817,7 @@ object DocumentManager { * @param filter Query itself * @return */ - def countHistoricLast(rtype:String, rid:Option[String], filter:Option[Bson]):Future[Long] = { + def countHistoricLast(rtype:String, rid:Option[String], filter:Option[Bson], filteringStages:Seq[Bson]):Future[Long] = { val collection = MongoDB.getCollection(rtype, true) val finalFilter = andQueries(rid.map(ridQuery).toSeq ++ filter.toSeq) @@ -660,6 +825,9 @@ object DocumentManager { //Filter with the query finalFilter.foreach(ff => aggregations.append(Aggregates.filter(ff))) + //Add the filtering stages + filteringStages.foreach(s => aggregations.append(s)) + //Sort according to version in descending order aggregations.append(Aggregates.sort(descending("meta.versionId"))) //Group by resource id and get the first for each group (means the biggest version) @@ -671,38 +839,48 @@ object DocumentManager { ) } - def searchHistoricDocumentsWithAt(rtype:String, rid:Option[String], filter:Option[Bson], count:Int = -1, page:Int = -1):Future[(Long,Seq[Document])] = { + /** + * Handle the history search with _at parameter + * @param rtype + * @param rid + * @param filter + * @param filteringStages + * @param count + * @param page + * @return + */ + def searchHistoricDocumentsWithAt(rtype:String, rid:Option[String], filter:Option[Bson], filteringStages:Seq[Bson], count:Int = -1, page:Int = -1):Future[(Long,Seq[Document])] = { def getIdsOfAllCurrents():Future[Seq[String]] = { DocumentManager - .searchDocuments(rtype, DocumentManager.andQueries(rid.map(ridQuery).toSeq ++ filter.toSeq), count, page, includingOrExcludingFields = Some(true, Set(FHIR_COMMON_FIELDS.ID))) + .searchDocuments(rtype, DocumentManager.andQueries(rid.map(ridQuery).toSeq ++ filter.toSeq), filteringStages, count, page, includingOrExcludingFields = Some(true, Set(FHIR_COMMON_FIELDS.ID))) .map(docs => docs.map(d => d.getString(FHIR_COMMON_FIELDS.ID))) } - val totalCurrentFuture = DocumentManager.countDocuments(rtype, DocumentManager.andQueries(rid.map(ridQuery).toSeq ++ filter.toSeq)) + val totalCurrentFuture = DocumentManager.countDocuments(rtype, DocumentManager.andQueries(rid.map(ridQuery).toSeq ++ filter.toSeq), filteringStages) totalCurrentFuture.flatMap { //If all records can be supplied from currents case totalCurrent:Long if count * page <= totalCurrent => DocumentManager - .searchDocuments(rtype, DocumentManager.andQueries(rid.map(ridQuery).toSeq ++ filter.toSeq), count, page) + .searchDocuments(rtype, DocumentManager.andQueries(rid.map(ridQuery).toSeq ++ filter.toSeq), filteringStages, count, page) .flatMap(docs => { getIdsOfAllCurrents().flatMap(ids => DocumentManager - .countHistoricLast(rtype, rid, if(ids.nonEmpty) DocumentManager.andQueries(filter.toSeq :+ nin(FHIR_COMMON_FIELDS.ID, ids:_*)) else filter) + .countHistoricLast(rtype, rid, if(ids.nonEmpty) DocumentManager.andQueries(filter.toSeq :+ nin(FHIR_COMMON_FIELDS.ID, ids:_*)) else filter, filteringStages) .map(totalHistory => (totalCurrent + totalHistory) -> docs) ) }) //If some of them should be from current some from history case totalCurrent:Long if count * page > totalCurrent && count * (page-1) < totalCurrent => DocumentManager - .searchDocuments(rtype, DocumentManager.andQueries(rid.map(ridQuery).toSeq ++ filter.toSeq), count, page) + .searchDocuments(rtype, DocumentManager.andQueries(rid.map(ridQuery).toSeq ++ filter.toSeq), filteringStages, count, page) .flatMap(currentDocs => { val numOfNeeded = count * page - totalCurrent getIdsOfAllCurrents().flatMap(ids => DocumentManager - .countHistoricLast(rtype, rid, if(ids.nonEmpty) DocumentManager.andQueries(filter.toSeq :+ nin(FHIR_COMMON_FIELDS.ID, ids:_*)) else filter) + .countHistoricLast(rtype, rid, if(ids.nonEmpty) DocumentManager.andQueries(filter.toSeq :+ nin(FHIR_COMMON_FIELDS.ID, ids:_*)) else filter, filteringStages) .flatMap(totalHistory=> - DocumentManager.getHistoricLast(rtype, rid, if(ids.nonEmpty) DocumentManager.andQueries(filter.toSeq :+ nin(FHIR_COMMON_FIELDS.ID, ids:_*)) else filter, numOfNeeded.toInt).map(historicDocs => + DocumentManager.getHistoricLast(rtype, rid, if(ids.nonEmpty) DocumentManager.andQueries(filter.toSeq :+ nin(FHIR_COMMON_FIELDS.ID, ids:_*)) else filter, filteringStages, numOfNeeded.toInt).map(historicDocs => (totalCurrent + totalHistory) -> (currentDocs ++ historicDocs) ) ) @@ -712,9 +890,9 @@ object DocumentManager { val numToSkip = count * (page-1) - totalCurrent getIdsOfAllCurrents().flatMap(ids => DocumentManager - .countHistoricLast(rtype, rid, if(ids.nonEmpty) DocumentManager.andQueries(filter.toSeq :+ nin(FHIR_COMMON_FIELDS.ID, ids:_*)) else filter) + .countHistoricLast(rtype, rid, if(ids.nonEmpty) DocumentManager.andQueries(filter.toSeq :+ nin(FHIR_COMMON_FIELDS.ID, ids:_*)) else filter, filteringStages) .flatMap(totalHistory => - DocumentManager.getHistoricLast(rtype, rid, if(ids.nonEmpty) DocumentManager.andQueries(filter.toSeq :+ nin(FHIR_COMMON_FIELDS.ID, ids:_*)) else filter, count, numToSkip.toInt).map(historicDocs => + DocumentManager.getHistoricLast(rtype, rid, if(ids.nonEmpty) DocumentManager.andQueries(filter.toSeq :+ nin(FHIR_COMMON_FIELDS.ID, ids:_*)) else filter, filteringStages, count, numToSkip.toInt).map(historicDocs => (totalCurrent + totalHistory) -> historicDocs ) ) @@ -732,12 +910,12 @@ object DocumentManager { * @param page page number * @return */ - def searchHistoricDocuments(rtype:String, rid:Option[String], filter:Option[Bson], count:Int = -1, page:Int = -1)(implicit transactionSession: Option[TransactionSession] = None):Future[(Long,Seq[Document])] = { + def searchHistoricDocuments(rtype:String, rid:Option[String], filter:Option[Bson], filteringStages:Seq[Bson], count:Int = -1, page:Int = -1)(implicit transactionSession: Option[TransactionSession] = None):Future[(Long,Seq[Document])] = { val finalFilter = andQueries(rid.map(ridQuery).toSeq ++ filter.toSeq) val numOfDocs:Future[(Long, Long)] = for { - totalCurrent <- getCount(rtype, finalFilter) - totalHistory <- getCount(rtype, finalFilter, history = true) + totalCurrent <- if(filteringStages.isEmpty) getCount(rtype, finalFilter) else getCountWithAgg(rtype, finalFilter, filteringStages) + totalHistory <- if(filteringStages.isEmpty) getCount(rtype, finalFilter, history = true) else getCountWithAgg(rtype, finalFilter, filteringStages, history = true) } yield totalCurrent -> totalHistory numOfDocs.flatMap { @@ -756,7 +934,7 @@ object DocumentManager { //Skip this number of record val skip = count * (page - 1) //Otherwise search the history directly - searchHistoricDocumentsHelper(rtype, history = true, finalFilter, count, skip) + searchHistoricDocumentsHelper(rtype, history = true, finalFilter, filteringStages, count, skip) .map(docs => totalHistory -> docs) case (totalCurrent, totalHistory) => val fresults = @@ -765,11 +943,11 @@ object DocumentManager { case Some(_) => //If they want page 1, look also to the current collection for resource type if(page == 1) - searchHistoricDocumentsHelper(rtype, history = false, finalFilter, count, 0) + searchHistoricDocumentsHelper(rtype, history = false, finalFilter,filteringStages, count, 0) .flatMap(currentResults => //If count is not one or current result is not empty, search also history and merge if(count!=1 || totalCurrent > 0) - searchHistoricDocumentsHelper(rtype, history = true, finalFilter, count-1, 0).map(historyResults => + searchHistoricDocumentsHelper(rtype, history = true, finalFilter, filteringStages, count-1, 0).map(historyResults => currentResults ++ historyResults ) else @@ -779,15 +957,15 @@ object DocumentManager { //Skip this number of record val skip = count * (page-1) - 1 //Otherwise search the history directly - searchHistoricDocumentsHelper(rtype, history = true, finalFilter, count, skip) + searchHistoricDocumentsHelper(rtype, history = true, finalFilter,filteringStages, count, skip) } //If history interaction is executed on type level case None => - searchHistoricDocumentsHelper(rtype, history = false, finalFilter, count, count * (page-1)).flatMap { cresults => + searchHistoricDocumentsHelper(rtype, history = false, finalFilter,filteringStages, count, count * (page-1)).flatMap { cresults => if(count * page > totalCurrent) { var skip = count * (page-1) - totalCurrent if(skip < 0) skip = 0 - searchHistoricDocumentsHelper(rtype, history = true, finalFilter, count - cresults.length, skip.toInt).map { hresults => + searchHistoricDocumentsHelper(rtype, history = true, finalFilter,filteringStages, count - cresults.length, skip.toInt).map { hresults => cresults ++ hresults } } else { @@ -809,37 +987,77 @@ object DocumentManager { * @param skip Skip this number of record * @return */ - private def searchHistoricDocumentsHelper(rtype:String, history:Boolean, finalFilter:Option[Bson], count:Int, skip:Int)(implicit transactionSession: Option[TransactionSession] = None):Future[Seq[Document]] = { - val collection = MongoDB.getCollection(rtype, history) - //Construct query - var query = transactionSession match { - case None => - finalFilter - .map(f => collection.find(f)) - .getOrElse(collection.find()) - case Some(ts) => - finalFilter - .map(f => collection.find(ts.dbSession, f)) - .getOrElse(collection.find(ts.dbSession)) - } + private def searchHistoricDocumentsHelper(rtype:String, history:Boolean, finalFilter:Option[Bson], filteringStages:Seq[Bson], count:Int, skip:Int)(implicit transactionSession: Option[TransactionSession] = None):Future[Seq[Document]] = { + if(filteringStages.nonEmpty) + searchHistoricDocumentsHelperWithAgg(rtype, history, finalFilter, filteringStages, count, skip) + else { + val collection = MongoDB.getCollection(rtype, history) + //Construct query + var query = transactionSession match { + case None => + finalFilter + .map(f => collection.find(f)) + .getOrElse(collection.find()) + case Some(ts) => + finalFilter + .map(f => collection.find(ts.dbSession, f)) + .getOrElse(collection.find(ts.dbSession)) + } - //If count is given limit the search result - if(count > 0) - query = query.skip(skip).limit(count) + //If count is given limit the search result + if (count > 0) + query = query.skip(skip).limit(count) + + //Exclude extra params + query = query.projection(exclude(FHIR_COMMON_FIELDS.MONGO_ID)) + + //Sort by last updated and version id + val LAST_UPDATED = s"${FHIR_COMMON_FIELDS.META}.${FHIR_COMMON_FIELDS.LAST_UPDATED}" + val VERSION_ID = s"${FHIR_COMMON_FIELDS.META}.${FHIR_COMMON_FIELDS.VERSION_ID}" + query = query.sort(descending(LAST_UPDATED, VERSION_ID)) + + //Execute the query + query.toFuture() + } + } + + /** + * Search helper for historic or current via MongoDB aggregation pipeline + * @param rtype Resource type + * @param history Is search on history instances + * @param finalFilter Final query if exist + * @param count Pagination count + * @param skip Skip this number of record + * @param transactionSession + * @return + */ + private def searchHistoricDocumentsHelperWithAgg(rtype:String, history:Boolean, finalFilter:Option[Bson], filteringStages:Seq[Bson], count:Int, skip:Int)(implicit transactionSession: Option[TransactionSession] = None):Future[Seq[Document]] = { + val aggregations = new ListBuffer[Bson] + //Set the query + finalFilter.foreach(f => aggregations.append(Aggregates.`match`(f))) + //Add the filtering stages + filteringStages.foreach(s => aggregations.append(s)) + if(count > 0) { + aggregations.append(Aggregates.skip(skip)) + aggregations.append(Aggregates.limit(count)) + } //Exclude extra params - query = query.projection(exclude(FHIR_COMMON_FIELDS.MONGO_ID)) + aggregations.append(Aggregates.project(Projections.exclude(FHIR_COMMON_FIELDS.MONGO_ID))) //Sort by last updated and version id val LAST_UPDATED = s"${FHIR_COMMON_FIELDS.META}.${FHIR_COMMON_FIELDS.LAST_UPDATED}" - val VERSION_ID = s"${FHIR_COMMON_FIELDS.META}.${FHIR_COMMON_FIELDS.VERSION_ID}" - query = query.sort(descending(LAST_UPDATED, VERSION_ID)) + val VERSION_ID = s"${FHIR_COMMON_FIELDS.META}.${FHIR_COMMON_FIELDS.VERSION_ID}" + aggregations.append(Aggregates.sort(Sorts.descending(LAST_UPDATED, VERSION_ID))) - //Execute the query - query.toFuture() + transactionSession match { + case None => MongoDB.getCollection(rtype, history).aggregate(aggregations.toSeq).toFuture() + case Some(ts) =>MongoDB.getCollection(rtype, history).aggregate(ts.dbSession, aggregations.toSeq).toFuture() + } } + /** * Inserts the given document into a appropriate collection * @param rtype type of the resource @@ -1159,6 +1377,7 @@ object DocumentManager { */ def searchLastOrFirstNByAggregation(rtype:String, filter:Option[Bson], + filteringStages:Seq[Bson], lastOrFirstN:Int = -1, sortingPaths:Seq[(String, Seq[String])] = Seq.empty, groupByExpressions:Seq[(String, BsonValue)], @@ -1169,6 +1388,8 @@ object DocumentManager { //First, append the match query if(filter.isDefined) aggregations.append(Aggregates.`match`(filter.get)) + //Add the further filtering stages if exist + filteringStages.foreach(s => aggregations.append(s)) val spaths = sortingPaths.map(s => (s._1, if(lastOrFirstN < 0) -1 else 1, s._2)) @@ -1190,7 +1411,8 @@ object DocumentManager { //Handle projections val extraSortingFieldsToExclude = paramsWithAlternativeSorting.map(sp => s"__sort_${sp._1}") - aggregations.append(handleProjectionForAggregationSearch(includingOrExcludingFields, excludeExtraFields, extraSortingFieldsToExclude.toSet)) + handleProjectionForAggregationSearch(includingOrExcludingFields, excludeExtraFields, extraSortingFieldsToExclude.toSet) + .foreach(prj => aggregations.append(prj)) //Merge the expressions into single groupBy expression val groupByExpr:Document = Document.apply(groupByExpressions) diff --git a/onfhir-core/src/main/scala/io/onfhir/db/IFhirQueryBuilder.scala b/onfhir-core/src/main/scala/io/onfhir/db/IFhirQueryBuilder.scala new file mode 100644 index 00000000..0ffb1912 --- /dev/null +++ b/onfhir-core/src/main/scala/io/onfhir/db/IFhirQueryBuilder.scala @@ -0,0 +1,67 @@ +package io.onfhir.db + +import io.onfhir.api.FHIR_PREFIXES_MODIFIERS +import org.mongodb.scala.bson.conversions.Bson +import org.mongodb.scala.model.Filters + +trait IFhirQueryBuilder { + /** + * + * @param queries + * @return + */ + def andQueries(queries:Seq[Bson]):Bson = { + queries match { + case Seq(single) => single + case multiple => Filters.and(multiple: _*) + } + } + + /** + * Combine the queries as logical OR + * @param queries Given queries + * @return + */ + def orQueries(queries:Seq[Bson]):Bson = { + queries match { + case Seq(single) => single + case multiple => Filters.or(multiple:_*) + } + } + + /** + * + * @param queries + * @return + */ + def neitherQueries(queries:Seq[Bson]):Bson = { + queries match { + case Seq(single) => Filters.not(single) + case multiple => Filters.and(multiple.map(Filters.not): _*) + } + } + + /** + * Construct final query from given query and elemMatch path (the last array element on the path) + * @param elemMatchPath The path until (including) the last array element + * @param query Query constructed for the search + * @return + */ + def getFinalQuery(elemMatchPath:Option[String], query:Bson):Bson = { + elemMatchPath match { + case None => query + case Some(emp) => Filters.elemMatch(emp, query) + } + } + + def mergeQueriesFromDifferentPaths(modifier:String, queries:Seq[Bson]):Bson = { + modifier match { + //If the modifier is negation, we should and them + case FHIR_PREFIXES_MODIFIERS.NOT | FHIR_PREFIXES_MODIFIERS.NOT_IN => + andQueries(queries) + //Otherwise use logical OR + case _ => + orQueries(queries) + } + } +} diff --git a/onfhir-core/src/main/scala/io/onfhir/db/NumberQueryBuilder.scala b/onfhir-core/src/main/scala/io/onfhir/db/NumberQueryBuilder.scala new file mode 100644 index 00000000..d1bcdd6f --- /dev/null +++ b/onfhir-core/src/main/scala/io/onfhir/db/NumberQueryBuilder.scala @@ -0,0 +1,156 @@ +package io.onfhir.db + +import io.onfhir.api.{FHIR_COMMON_FIELDS, FHIR_DATA_TYPES, FHIR_PREFIXES_MODIFIERS} +import io.onfhir.api.util.FHIRUtil +import io.onfhir.exception.InternalServerException +import org.mongodb.scala.bson.conversions.Bson +import org.mongodb.scala.model.Filters + +/** + * + */ +object NumberQueryBuilder extends IFhirQueryBuilder { + + /** + * Search based on numerical values and range + * + * @param prefixAndValues Supplied prefix and values + * @param path Path to the target element to be queried + * @param targetType FHIR Type of the target element + * @return respective BsonDocument for target query + */ + def getQuery(prefixAndValues: Seq[(String, String)], path:String, targetType:String):Bson = { + orQueries( + prefixAndValues.map { + case (prefix, value) => + targetType match { + case FHIR_DATA_TYPES.RANGE => + //Split the parts + val (elemMatchPath, queryPath) = FHIRUtil.splitElementPathIntoElemMatchAndQueryPaths(path) + //Main query on Range object + val mainQuery = getQueryForRange(queryPath.getOrElse(""), value, prefix) + //If an array exist, use elemMatch otherwise return the query + getFinalQuery(elemMatchPath, mainQuery) + //Ad these are simple searches we don't need to handle arrays with elemMatch + case FHIR_DATA_TYPES.INTEGER => getQueryForInteger(FHIRUtil.normalizeElementPath(path), value, prefix) + case FHIR_DATA_TYPES.DECIMAL => getQueryForDecimal(FHIRUtil.normalizeElementPath(path), value, prefix) + case other => throw new InternalServerException(s"Unknown target element type $other !!!") + } + } + ) + } + + /** + * Handles prefixes for integer values + * + * @param path absolute path of the parameter + * @param value value of the parameter + * @param prefix prefix of the parameter + * @return BsonDocument for the query + */ + def getQueryForInteger(path: String, value: String, prefix: String): Bson = { + //If there is non-zero digits after a decimal point, there cannot be any matches + if (value.toDouble != value.toDouble.toLong * 1.0) + Filters.equal(path, 0.05) + else { + //If the value is given in exponential form, we use precision + if (value.contains("e") || value.contains("E")) { + val precision = FHIRUtil.calculatePrecisionDelta(value) + // Generated function values for comparison + val floor = value.toDouble - precision + val ceil = value.toDouble + precision + + prefix match { + case FHIR_PREFIXES_MODIFIERS.BLANK_EQUAL | FHIR_PREFIXES_MODIFIERS.EQUAL => Filters.and(Filters.gte(path, floor), Filters.lt(path, ceil)) + case FHIR_PREFIXES_MODIFIERS.GREATER_THAN | FHIR_PREFIXES_MODIFIERS.GREATER_THAN_M => Filters.gt(path, value.toLong) + case FHIR_PREFIXES_MODIFIERS.LESS_THAN | FHIR_PREFIXES_MODIFIERS.LESS_THAN_M => Filters.lt(path, value.toLong) + case FHIR_PREFIXES_MODIFIERS.GREATER_THAN_EQUAL => Filters.gte(path, value.toLong) + case FHIR_PREFIXES_MODIFIERS.LESS_THAN_EQUAL => Filters.lte(path, value.toLong) + case FHIR_PREFIXES_MODIFIERS.NOT_EQUAL => Filters.or(Filters.lt(path, floor), Filters.gte(path, ceil)) + case FHIR_PREFIXES_MODIFIERS.APPROXIMATE => Filters.and(Filters.gte(path, value.toDouble * 0.9), Filters.lte(path, value.toDouble * 1.1)) + case FHIR_PREFIXES_MODIFIERS.STARTS_AFTER | FHIR_PREFIXES_MODIFIERS.ENDS_BEFORE => throw new IllegalArgumentException("Prefixes sa and eb can not be used with integer values.") + } + } else { //Otherwise we need extact integer match + // Prefix matching and query filter generation + prefix match { + case FHIR_PREFIXES_MODIFIERS.BLANK_EQUAL | FHIR_PREFIXES_MODIFIERS.EQUAL => Filters.equal(path, value.toLong) + case FHIR_PREFIXES_MODIFIERS.GREATER_THAN | FHIR_PREFIXES_MODIFIERS.GREATER_THAN_M => Filters.gt(path, value.toLong) + case FHIR_PREFIXES_MODIFIERS.LESS_THAN | FHIR_PREFIXES_MODIFIERS.LESS_THAN_M => Filters.lt(path, value.toLong) + case FHIR_PREFIXES_MODIFIERS.GREATER_THAN_EQUAL => Filters.gte(path, value.toLong) + case FHIR_PREFIXES_MODIFIERS.LESS_THAN_EQUAL => Filters.lte(path, value.toLong) + case FHIR_PREFIXES_MODIFIERS.NOT_EQUAL => Filters.not(Filters.equal(path, value.toLong)) + case FHIR_PREFIXES_MODIFIERS.APPROXIMATE => Filters.and(Filters.gte(path, value.toDouble * 0.9), Filters.lte(path, value.toDouble * 1.1)) + case FHIR_PREFIXES_MODIFIERS.STARTS_AFTER | FHIR_PREFIXES_MODIFIERS.ENDS_BEFORE => throw new IllegalArgumentException("Prefixes sa and eb can not be used with integer values.") + } + } + } + } + + /** + * Handles prefixes for decimal values + * + * @param path absolute path of the parameter + * @param prefix prefix of the parameter + * @return BsonDocument for the query + */ + def getQueryForDecimal(path: String, value: String, prefix: String): Bson = { + // Calculation of precision to generate implicit ranges + val precision = FHIRUtil.calculatePrecisionDelta(value) + //if(!value.contains('.')) 0.5 else pow(0.1, value.length - (value.indexOf(".") + 1)) * 0.5 + // Generated function values for comparison + val floor = value.toDouble - precision + val ceil = value.toDouble + precision + // Prefix matching and query generation + prefix match { + case FHIR_PREFIXES_MODIFIERS.BLANK_EQUAL | FHIR_PREFIXES_MODIFIERS.EQUAL => Filters.and(Filters.gte(path, floor), Filters.lt(path, ceil)) + case FHIR_PREFIXES_MODIFIERS.GREATER_THAN | FHIR_PREFIXES_MODIFIERS.GREATER_THAN_M => Filters.gt(path, value.toDouble) + case FHIR_PREFIXES_MODIFIERS.LESS_THAN | FHIR_PREFIXES_MODIFIERS.LESS_THAN_M => Filters.lt(path, value.toDouble) + case FHIR_PREFIXES_MODIFIERS.GREATER_THAN_EQUAL => Filters.gte(path, value.toDouble) + case FHIR_PREFIXES_MODIFIERS.LESS_THAN_EQUAL => Filters.lte(path, value.toDouble) + case FHIR_PREFIXES_MODIFIERS.NOT_EQUAL => Filters.or(Filters.lt(path, floor), Filters.gte(path, ceil)) + case FHIR_PREFIXES_MODIFIERS.STARTS_AFTER => Filters.gt(path, value.toDouble) + case FHIR_PREFIXES_MODIFIERS.ENDS_BEFORE => Filters.lt(path, value.toDouble) + case FHIR_PREFIXES_MODIFIERS.APPROXIMATE => + val approximateLow = getQueryForDecimal(path, (value.toDouble * 0.9).toString, FHIR_PREFIXES_MODIFIERS.GREATER_THAN_EQUAL) + val approximateHigh = getQueryForDecimal(path, (value.toDouble * 1.1).toString, FHIR_PREFIXES_MODIFIERS.LESS_THAN_EQUAL) + Filters.and(approximateLow, approximateHigh) + } + } + + /** + * Handles prefixes for range type + * + * @param path absolute path of the parameter + * @param value value of the parameter + * @param prefix prefix of the parameter + * @return BsonDocument for the query + */ + def getQueryForRange(path: String, value: String, prefix: String, isSampleData: Boolean = false): Bson = { + // Calculation of precision to generate implicit ranges + val precision = FHIRUtil.calculatePrecisionDelta(value) + // Paths to the range structure's high and low values + val pathLow = if (isSampleData) FHIRUtil.mergeElementPath(path, FHIR_COMMON_FIELDS.LOWER_LIMIT) else FHIRUtil.mergeElementPath(path, s"${FHIR_COMMON_FIELDS.LOW}.${FHIR_COMMON_FIELDS.VALUE}") + val pathHigh = if (isSampleData) FHIRUtil.mergeElementPath(path, FHIR_COMMON_FIELDS.UPPER_LIMIT) else FHIRUtil.mergeElementPath(path, s"${FHIR_COMMON_FIELDS.HIGH}.${FHIR_COMMON_FIELDS.VALUE}") + // Implicit input value ranges + val floor = value.toDouble - precision + val ceil = value.toDouble + precision + // BsonDocuments to represent nonexistence of high and low values + val fieldHighNotExist = Filters.and(Filters.exists(pathLow, exists = true), Filters.exists(pathHigh, exists = false)) + val fieldLowNotExist = Filters.and(Filters.exists(pathLow, exists = false), Filters.exists(pathHigh, exists = true)) + // Prefix matching and query generation + prefix match { + case FHIR_PREFIXES_MODIFIERS.BLANK_EQUAL | FHIR_PREFIXES_MODIFIERS.EQUAL => Filters.and(Filters.gte(pathLow, floor), Filters.lt(pathHigh, ceil)) + case FHIR_PREFIXES_MODIFIERS.GREATER_THAN | FHIR_PREFIXES_MODIFIERS.GREATER_THAN_M => Filters.or(Filters.gt(pathHigh, value.toDouble), fieldHighNotExist) + case FHIR_PREFIXES_MODIFIERS.LESS_THAN | FHIR_PREFIXES_MODIFIERS.LESS_THAN_M => Filters.or(Filters.lt(pathLow, value.toDouble), fieldLowNotExist) + case FHIR_PREFIXES_MODIFIERS.GREATER_THAN_EQUAL => Filters.or(Filters.gte(pathHigh, value.toDouble), fieldHighNotExist) + case FHIR_PREFIXES_MODIFIERS.LESS_THAN_EQUAL => Filters.or(Filters.lte(pathLow, value.toDouble), fieldLowNotExist) + case FHIR_PREFIXES_MODIFIERS.NOT_EQUAL => Filters.or(Filters.lt(pathLow, floor), Filters.gte(pathHigh, ceil)) + case FHIR_PREFIXES_MODIFIERS.STARTS_AFTER => Filters.gt(pathLow, value.toDouble) + case FHIR_PREFIXES_MODIFIERS.ENDS_BEFORE => Filters.lt(pathHigh, value.toDouble) + case FHIR_PREFIXES_MODIFIERS.APPROXIMATE => + val approximateLow = getQueryForDecimal(FHIRUtil.mergeElementPath(path, FHIR_COMMON_FIELDS.LOW), (value.toDouble * 0.9).toString, FHIR_PREFIXES_MODIFIERS.GREATER_THAN_EQUAL) + val approximateHigh = getQueryForDecimal(FHIRUtil.mergeElementPath(path, FHIR_COMMON_FIELDS.HIGH), (value.toDouble * 1.1).toString, FHIR_PREFIXES_MODIFIERS.LESS_THAN_EQUAL) + Filters.and(approximateLow, approximateHigh) + } + } +} diff --git a/onfhir-core/src/main/scala/io/onfhir/db/PrefixModifierHandler.scala b/onfhir-core/src/main/scala/io/onfhir/db/PrefixModifierHandler.scala deleted file mode 100644 index 17e769e8..00000000 --- a/onfhir-core/src/main/scala/io/onfhir/db/PrefixModifierHandler.scala +++ /dev/null @@ -1,678 +0,0 @@ -package io.onfhir.db - -import java.net.URL -import java.util.regex.Pattern -import io.onfhir.api._ - -import io.onfhir.api.util.FHIRUtil -import io.onfhir.config.FhirConfigurationManager -import io.onfhir.exception.{InvalidParameterException, UnsupportedParameterException} -import io.onfhir.util.DateTimeUtil -import org.mongodb.scala.bson.{BsonDateTime, BsonValue} -import org.mongodb.scala.bson.conversions.Bson -import org.mongodb.scala.model.Filters -import org.mongodb.scala.model.Filters.{exists, _} - -import scala.collection.mutable.ListBuffer -import scala.concurrent.Future -import scala.math.pow -import scala.util.Try - -/** - * Handles prefixes and modifiers over the parameters - */ -object PrefixModifierHandler { - /** - * Handles missing modifier - * - * @param pathList absolute path of the parameter - * @param bool boolean value - * @return BsonDocument for the query - */ - def missingHandler(pathList: Seq[String], bool: String): Bson = { - val missingQuery:ListBuffer[Bson] = ListBuffer() - bool match { - case MISSING_MODIFIER_VALUES.STRING_TRUE => - pathList foreach { path => - missingQuery.append(exists(path, exists=false)) - } - and(missingQuery.toList:_*) - case MISSING_MODIFIER_VALUES.STRING_FALSE => - pathList foreach { path => - missingQuery.append(exists(path, exists=true)) - } - or(missingQuery.toList:_*) - case _ => - throw new InvalidParameterException("Correct Boolean Value Should be Provided") - } - } - - - /** - * Handles prefixes for integer values - * - * @param path absolute path of the parameter - * @param value value of the parameter - * @param prefix prefix of the parameter - * @return BsonDocument for the query - */ - def intPrefixHandler(path:String, value:String, prefix:String): Bson = { - //If there is non-zero digits after a decimal point, there cannot be any matches - if(value.toDouble != value.toDouble.toLong * 1.0) - equal(path, 0.05) - else { - //If the value is given in exponential form, we use precision - if(value.contains("e") || value.contains("E")){ - val precision = FHIRUtil.calculatePrecisionDelta(value) - // Generated function values for comparison - val floor = value.toDouble - precision - val ceil = value.toDouble + precision - - prefix match { - case FHIR_PREFIXES_MODIFIERS.BLANK_EQUAL | FHIR_PREFIXES_MODIFIERS.EQUAL => and(gte(path, floor), lt(path, ceil)) - case FHIR_PREFIXES_MODIFIERS.GREATER_THAN | FHIR_PREFIXES_MODIFIERS.GREATER_THAN_M => gt(path, value.toLong) - case FHIR_PREFIXES_MODIFIERS.LESS_THAN | FHIR_PREFIXES_MODIFIERS.LESS_THAN_M => lt(path, value.toLong) - case FHIR_PREFIXES_MODIFIERS.GREATER_THAN_EQUAL => gte(path, value.toLong) - case FHIR_PREFIXES_MODIFIERS.LESS_THAN_EQUAL => lte(path, value.toLong) - case FHIR_PREFIXES_MODIFIERS.NOT_EQUAL => or(lt(path, floor), gte(path, ceil)) - case FHIR_PREFIXES_MODIFIERS.APPROXIMATE => and(gte(path, value.toDouble * 0.9), lte(path, value.toDouble * 1.1)) - case FHIR_PREFIXES_MODIFIERS.STARTS_AFTER | FHIR_PREFIXES_MODIFIERS.ENDS_BEFORE => throw new IllegalArgumentException("Prefixes sa and eb can not be used with integer values.") - } - } else { //Otherwise we need extact integer match - // Prefix matching and query filter generation - prefix match { - case FHIR_PREFIXES_MODIFIERS.BLANK_EQUAL | FHIR_PREFIXES_MODIFIERS.EQUAL => equal(path, value.toLong) - case FHIR_PREFIXES_MODIFIERS.GREATER_THAN | FHIR_PREFIXES_MODIFIERS.GREATER_THAN_M => gt(path, value.toLong) - case FHIR_PREFIXES_MODIFIERS.LESS_THAN | FHIR_PREFIXES_MODIFIERS.LESS_THAN_M => lt(path, value.toLong) - case FHIR_PREFIXES_MODIFIERS.GREATER_THAN_EQUAL => gte(path, value.toLong) - case FHIR_PREFIXES_MODIFIERS.LESS_THAN_EQUAL => lte(path, value.toLong) - case FHIR_PREFIXES_MODIFIERS.NOT_EQUAL => not(equal(path, value.toLong)) - case FHIR_PREFIXES_MODIFIERS.APPROXIMATE => and(gte(path, value.toDouble * 0.9), lte(path, value.toDouble * 1.1)) - case FHIR_PREFIXES_MODIFIERS.STARTS_AFTER | FHIR_PREFIXES_MODIFIERS.ENDS_BEFORE => throw new IllegalArgumentException("Prefixes sa and eb can not be used with integer values.") - } - } - } - } - - /** - * Handles prefixes for decimal values - * - * @param path absolute path of the parameter - * @param prefix prefix of the parameter - * @return BsonDocument for the query - */ - def decimalPrefixHandler(path:String, value:String, prefix:String): Bson = { - // Calculation of precision to generate implicit ranges - val precision = FHIRUtil.calculatePrecisionDelta(value) - //if(!value.contains('.')) 0.5 else pow(0.1, value.length - (value.indexOf(".") + 1)) * 0.5 - // Generated function values for comparison - val floor = value.toDouble - precision - val ceil = value.toDouble + precision - // Prefix matching and query generation - prefix match { - case FHIR_PREFIXES_MODIFIERS.BLANK_EQUAL | FHIR_PREFIXES_MODIFIERS.EQUAL => and(gte(path, floor), lt(path, ceil)) - case FHIR_PREFIXES_MODIFIERS.GREATER_THAN | FHIR_PREFIXES_MODIFIERS.GREATER_THAN_M => gt(path, value.toDouble) - case FHIR_PREFIXES_MODIFIERS.LESS_THAN | FHIR_PREFIXES_MODIFIERS.LESS_THAN_M => lt(path, value.toDouble) - case FHIR_PREFIXES_MODIFIERS.GREATER_THAN_EQUAL => gte(path, value.toDouble) - case FHIR_PREFIXES_MODIFIERS.LESS_THAN_EQUAL => lte(path, value.toDouble) - case FHIR_PREFIXES_MODIFIERS.NOT_EQUAL => or(lt(path, floor), gte(path, ceil)) - case FHIR_PREFIXES_MODIFIERS.STARTS_AFTER => gt(path, value.toDouble) - case FHIR_PREFIXES_MODIFIERS.ENDS_BEFORE => lt(path, value.toDouble) - case FHIR_PREFIXES_MODIFIERS.APPROXIMATE => - val approximateLow = decimalPrefixHandler(path, (value.toDouble*0.9).toString, FHIR_PREFIXES_MODIFIERS.GREATER_THAN_EQUAL) - val approximateHigh = decimalPrefixHandler(path, (value.toDouble*1.1).toString, FHIR_PREFIXES_MODIFIERS.LESS_THAN_EQUAL) - and(approximateLow, approximateHigh) - } - } - - /** - * Handles prefixes for range type - * - * @param path absolute path of the parameter - * @param value value of the parameter - * @param prefix prefix of the parameter - * @return BsonDocument for the query - */ - def rangePrefixHandler(path:String, value:String, prefix:String, isSampleData:Boolean = false): Bson = { - // Calculation of precision to generate implicit ranges - val precision = FHIRUtil.calculatePrecisionDelta(value) - // Paths to the range structure's high and low values - val pathLow = if(isSampleData) FHIRUtil.mergeElementPath(path, FHIR_COMMON_FIELDS.LOWER_LIMIT) else FHIRUtil.mergeElementPath(path,s"${FHIR_COMMON_FIELDS.LOW}.${FHIR_COMMON_FIELDS.VALUE}") - val pathHigh = if(isSampleData) FHIRUtil.mergeElementPath(path, FHIR_COMMON_FIELDS.UPPER_LIMIT) else FHIRUtil.mergeElementPath(path, s"${FHIR_COMMON_FIELDS.HIGH}.${FHIR_COMMON_FIELDS.VALUE}") - // Implicit input value ranges - val floor = value.toDouble - precision - val ceil = value.toDouble + precision - // BsonDocuments to represent nonexistence of high and low values - val fieldHighNotExist = and(exists(pathLow, exists=true), exists(pathHigh, exists=false)) - val fieldLowNotExist = and(exists(pathLow, exists=false), exists(pathHigh, exists=true)) - // Prefix matching and query generation - prefix match { - case FHIR_PREFIXES_MODIFIERS.BLANK_EQUAL | FHIR_PREFIXES_MODIFIERS.EQUAL => and(gte(pathLow, floor), lt(pathHigh, ceil)) - case FHIR_PREFIXES_MODIFIERS.GREATER_THAN | FHIR_PREFIXES_MODIFIERS.GREATER_THAN_M => or(gt(pathHigh, value.toDouble), fieldHighNotExist) - case FHIR_PREFIXES_MODIFIERS.LESS_THAN | FHIR_PREFIXES_MODIFIERS.LESS_THAN_M => or(lt(pathLow, value.toDouble), fieldLowNotExist) - case FHIR_PREFIXES_MODIFIERS.GREATER_THAN_EQUAL => or(gte(pathHigh, value.toDouble), fieldHighNotExist) - case FHIR_PREFIXES_MODIFIERS.LESS_THAN_EQUAL => or(lte(pathLow, value.toDouble), fieldLowNotExist) - case FHIR_PREFIXES_MODIFIERS.NOT_EQUAL => or(lt(pathLow, floor), gte(pathHigh, ceil)) - case FHIR_PREFIXES_MODIFIERS.STARTS_AFTER => gt(pathLow, value.toDouble) - case FHIR_PREFIXES_MODIFIERS.ENDS_BEFORE => lt(pathHigh, value.toDouble) - case FHIR_PREFIXES_MODIFIERS.APPROXIMATE => - val approximateLow = decimalPrefixHandler(FHIRUtil.mergeElementPath(path,FHIR_COMMON_FIELDS.LOW), (value.toDouble*0.9).toString, FHIR_PREFIXES_MODIFIERS.GREATER_THAN_EQUAL) - val approximateHigh = decimalPrefixHandler(FHIRUtil.mergeElementPath(path,FHIR_COMMON_FIELDS.HIGH), (value.toDouble*1.1).toString, FHIR_PREFIXES_MODIFIERS.LESS_THAN_EQUAL) - and(approximateLow, approximateHigh) - } - } - - /** - * FHIR string type query handler (including modifiers) - * @param path absolute path of the target element - * @param value value of the parameter - * @param modifier Search modifier - * @return - */ - def stringModifierHandler(path:String, value:String, modifier:String):Bson = { - //TODO Ignorance of accents or other diacritical marks, punctuation and non-significant whitespace is not supported yet - - // Escape characters for to have valid regular expression - val regularExpressionValue = FHIRUtil.escapeCharacters(value) - // Generator for regular expression queries(Only regex fields empty) - val caseInsensitiveStringRegex = regex(path, _:String, "i") - modifier match { - case FHIR_PREFIXES_MODIFIERS.EXACT => - // Exact match provided with $eq mongo operator - equal(path, value) - case FHIR_PREFIXES_MODIFIERS.CONTAINS => - // Partial match - caseInsensitiveStringRegex(".*" + regularExpressionValue + ".*") - //No modifier - case "" => - // Case insensitive, partial matches at the end of string - caseInsensitiveStringRegex("^" + regularExpressionValue) - case other => - throw new InvalidParameterException(s"Modifier $other is not supported for FHIR string queries!") - } - } - - /** - * FHIR uri type query handler (including modifiers) - * @param path absolute path of the target element - * @param uri Uri value - * @param modifier Search modifier - * @return - */ - def uriModifierHandler(path:String, uri:String, modifier:String):Bson = { - modifier match { - case FHIR_PREFIXES_MODIFIERS.ABOVE if uri.contains("/")=> - val url = Try(new URL(uri)).toOption - if(url.isEmpty) - throw new InvalidParameterException(s"Modifier ${FHIR_PREFIXES_MODIFIERS.ABOVE} is only supported for URLs not URNs or OIDs!") - - val initialPart = url.get.getProtocol + "://" + url.get.getHost + (if(uri.contains(url.get.getHost + ":"+url.get.getPort.toString)) ":" + url.get.getPort else "") - var urlPath = url.get.getPath - if(urlPath.length == 0 || urlPath == "/") urlPath = "" else urlPath = urlPath.drop(1) - val parts = urlPath.split("/") - - def constructRegexForAbove(parts:Seq[String]):String = { - parts match { - case Nil => "" - case oth => "(" + FHIRUtil.escapeCharacters("/"+ parts.head) + constructRegexForAbove(parts.drop(1)) + ")?" - } - } - //Constuct the regex to match any url above - val regularExpressionValue = FHIRUtil.escapeCharacters(initialPart) + constructRegexForAbove(parts.toIndexedSeq) - regex(path, "\\A" + regularExpressionValue + "$") - case FHIR_PREFIXES_MODIFIERS.BELOW if uri.contains("/") => - val url = Try(new URL(uri)).toOption - if(url.isEmpty) - throw new InvalidParameterException(s"Modifier ${FHIR_PREFIXES_MODIFIERS.ABOVE} is only supported for URLs not URNs or OIDs!") - // Escape characters for to have valid regular expression - val regularExpressionValue = FHIRUtil.escapeCharacters(uri) + "("+ FHIRUtil.escapeCharacters("/")+".*)*" - // Match at the beginning of the uri - regex(path, "\\A" + regularExpressionValue + "$") - case _ => - //If this is a query on Canonical URLs of the conformance and knowledge resources (e.g. StructureDefinition, ValueSet, PlanDefinition etc) and a version part is given |[version] - var finalQuery = - if(path == "url" && uri.contains("|")){ - val canonicalRef = Try(FHIRUtil.parseCanonicalReference(uri)).toOption - if(canonicalRef.exists(_.version.isDefined)) - and(equal(path, canonicalRef.get.getUrl()), equal("version", canonicalRef.get.version.get)) - else - equal(path, uri) - } else { - // Exact match - equal(path, uri) - } - if(modifier == FHIR_PREFIXES_MODIFIERS.NOT) - finalQuery = not(finalQuery) - finalQuery - } - } - - /** - * Handle FHIR token type queries on FHIR boolean values - * @param path absolute path of the target element - * @param boolean Boolean value of the parameter - * @param modifier Search modifier - * @return - */ - def tokenBooleanModifierHandler(path:String, boolean:String, modifier:String):Bson = { - - def handleTokenBooleanQuery(path:String, value:String, isNot:Boolean = false):Bson = { - if( value.equalsIgnoreCase("false") | - value.equalsIgnoreCase("true")) - Filters.eq(path, if(isNot) !value.toBoolean else value.toBoolean) - else - throw new InvalidParameterException(s"Invalid usage of parameter. Target element (with path $path) for search parameter is boolean, use either 'false' or 'true' for the parameter value!!!") - } - - modifier match { - case FHIR_PREFIXES_MODIFIERS.NOT => - handleTokenBooleanQuery(path, boolean, true) - case "" => - handleTokenBooleanQuery(path, boolean) - case other => - throw new InvalidParameterException(s"Modifier $other is not supported for FHIR token queries on FHIR boolean elements!") - } - } - - /** - * Handle FHIR token type search with modifiers - * @param systemPath Path to the system field - * @param codePath Path to the code field - * @param system Expected system value if exist; - * None means don't care - * Some("") means system should not exist - * Some(x) means system should match to x - * @param code Expected code - * @param modifier Modifier - * @return - */ - def tokenModifierHandler(systemPath:String, codePath:String, system:Option[String], code:Option[String], modifier:String):Bson = { - modifier match { - //Without modifier - case "" | FHIR_PREFIXES_MODIFIERS.NOT=> - handleTokenCodeSystemQuery(systemPath, codePath, system, code) - case FHIR_PREFIXES_MODIFIERS.STARTS_WITH | FHIR_PREFIXES_MODIFIERS.NOT_STARTS_WITH => - handleTokenStartsWithModifier(systemPath, codePath, system, code, modifier == FHIR_PREFIXES_MODIFIERS.NOT_STARTS_WITH) - case FHIR_PREFIXES_MODIFIERS.IN | FHIR_PREFIXES_MODIFIERS.NOT_IN => - handleTokenInModifier(systemPath, codePath, code.get, modifier) - //If this is one of those code systems that syntactically hierarchical, use it like startsWith - case FHIR_PREFIXES_MODIFIERS.BELOW if system.exists(s => COMMON_SYNTACTICALLY_HIERARCHICAL_CODE_SYSTEMS.contains(s)) => - handleTokenStartsWithModifier(systemPath, codePath, system, code, isNot = false) - case FHIR_PREFIXES_MODIFIERS.BELOW | FHIR_PREFIXES_MODIFIERS.ABOVE => - throw new UnsupportedParameterException("Modifier is not supported by onFhir.io system yet!") - case other => - throw new InvalidParameterException(s"Modifier $other is not supported for FHIR token queries!") - } - } - - /** - * onFHIR specific starts with modifier - * @param systemPath - * @param codePath - * @param system - * @param code - * @return - */ - private def handleTokenStartsWithModifier(systemPath:String, codePath:String, system:Option[String], code:Option[String], isNot:Boolean):Bson = { - if(code.isEmpty) - throw new InvalidParameterException(s"Code value should be given when modifier ':sw' is used!") - //val pattern = Pattern.compile("^"+code.get+"") - var codeStartsWithQuery = Filters.regex(codePath, "^"+code.get+"" ) - if(isNot) - codeStartsWithQuery = Filters.not(codeStartsWithQuery) - - system match { - // Query like [code] -> the value of [code] matches a Coding.code or Identifier.value irrespective of the value of the system property - case None => - codeStartsWithQuery - // Query like |[code] -> the value of [code] matches a Coding.code or Identifier.value, and the Coding/Identifier has no system property - case Some("") => - and(exists(systemPath, exists = false), codeStartsWithQuery) - // Query like [system][code] -> the value of [code] matches a Coding.code or Identifier.value, and the value of [system] matches the system property of the Identifier or Coding - case Some(sys) => - code match { - //[system]| --> should macth only systen - case None => - Filters.eq(systemPath, sys) - // Query like [system][code] - case Some(cd) => - and(Filters.eq(systemPath, sys), codeStartsWithQuery) - } - } - } - - /** - * Handle the in modifier for token search - * @param systemPath Path to the system element - * @param codePath Path to the code element - * @param vsUrl URL of the ValueSet to search in - * @return - */ - private def handleTokenInModifier(systemPath:String, codePath:String, vsUrl:String, modifier:String) :Bson = { - val vs = FHIRUtil.parseCanonicalReference(vsUrl) - if(FhirConfigurationManager.fhirTerminologyValidator.isValueSetSupported(vs.getUrl(), vs.version)){ - val vsCodes = FhirConfigurationManager.fhirTerminologyValidator.getAllCodes(vs.getUrl(), vs.version).toSeq - - val queriesForEachCodeSystem = modifier match { - case FHIR_PREFIXES_MODIFIERS.IN => - vsCodes.map(vsc => and(Filters.eq(systemPath, vsc._1), Filters.in(codePath, vsc._2.toSeq :_* ))) - case FHIR_PREFIXES_MODIFIERS.NOT_IN => - vsCodes.map(vsc => or(Filters.ne(systemPath, vsc._1), Filters.nin(codePath, vsc._2.toSeq :_* ))) - } - - queriesForEachCodeSystem.length match { - case 0 => throw new UnsupportedParameterException(s"ValueSet given with url '$vsUrl' by 'in' or 'not-in' modifier is empty!") - case 1 => queriesForEachCodeSystem.head - case _ => if(modifier == FHIR_PREFIXES_MODIFIERS.IN) or(queriesForEachCodeSystem:_*) else and(queriesForEachCodeSystem:_*) - } - } - //If it is not a canonical reference - else { - //TODO check if it is a literal reference and hande that - throw new UnsupportedParameterException(s"ValueSet url '$vsUrl' given with 'in' or 'not-in' modifier is not known!") - } - } - - /** - * Handles text modifier - * - * @param path Absolute path for the query parameter - * @param value value of the parameter - * @return BsonDocument for the query - */ - def handleTokenTextModifier(path: String, value: String): Bson = { - // Regular expression query definition for partial matching - val textQuery = regex(_:String, ".*" + FHIRUtil.escapeCharacters(value) + ".*", "i") - textQuery(path) - /*// Get the token :text field for target type - val textFields = TOKEN_DISPLAY_PATHS.get(targetType) - if(textFields.isEmpty) - throw new InitializationException(s"The modifier :text cannot be used for elements with type $targetType !!!") - - val queries = textFields.get.map(textField => { - textQuery(path + textField) - }) - if(queries.length > 1) or(queries:_*) else queries.head*/ - } - /** - * Handle the Token query on system and code fields - * @param systemPath Path to the system part e.g. Coding.system, Identifier.system - * @param codePath Path to the code part - * @param system Expected system value - * @param code Expected code value - * @return - */ - private def handleTokenCodeSystemQuery(systemPath:String, codePath:String, system:Option[String], code:Option[String]):Bson = { - system match { - // Query like [code] -> the value of [code] matches a Coding.code or Identifier.value irrespective of the value of the system property - case None => - Filters.eq(codePath, code.get) - // Query like |[code] -> the value of [code] matches a Coding.code or Identifier.value, and the Coding/Identifier has no system property - case Some("") => - and(exists(systemPath, exists = false), Filters.eq(codePath, code.get)) - // Query like [system][code] -> the value of [code] matches a Coding.code or Identifier.value, and the value of [system] matches the system property of the Identifier or Coding - case Some(sys) => - code match { - //[system]| --> should macth only systen - case None => - Filters.eq(systemPath, sys) - // Query like [system][code] - case Some(cd) => - and(Filters.eq(codePath, cd), Filters.eq(systemPath, sys)) - } - } - } - - def handleOfTypeModifier(typeSystemPath:String, typeCodePath:String, valuePath:String, typeSystem:String, typeCode:String, value:String):Bson = { - and(Filters.eq(typeSystemPath, typeSystem), Filters.eq(typeCodePath, typeCode), Filters.eq(valuePath, value)) - } - - - - /** - * Handles prefix for date values(implicit range) for date parameters. - * For further information about using prefixes with range values - * please refer to prefix table's third column in page; - * https://www.hl7.org/fhir/search.html#prefix - * - * @param path absolute path of the parameter - * @param value value of the parameter - * @param prefix prefix of the parameter - * @return BsonDocument for the query - */ - // TODO Missing ap for date queries - def dateRangePrefixHandler(path:String, value:String, prefix:String): Bson = { - // Populate Implicit ranges(e.g. 2010-10-10 represents the range 2010-10-10T00:00Z/2010-10-10T23:59ZZ) - val implicitRanges = DateTimeUtil.populateImplicitDateTimeRanges(value) - // Generate the implicit range paths(i.e. the paths created by the server) - val rangePaths = (FHIRUtil.mergeElementPath(path,FHIR_EXTRA_FIELDS.TIME_RANGE_START), FHIRUtil.mergeElementPath(path,FHIR_EXTRA_FIELDS.TIME_RANGE_END)) - - //If it is a datetime or instant base query - if(value.contains("T")){ - //We handle this specially as onfhir store this in millisecond precision - if(path == "meta.lastUpdated") - dateTimeQueryBuilder(FHIRUtil.mergeElementPath(path,FHIR_EXTRA_FIELDS.TIME_TIMESTAMP), prefix, implicitRanges) - else - // Build dateTime query on date time and period query on implicit ranges and combine them. - or(dateTimeQueryBuilder(FHIRUtil.mergeElementPath(path,FHIR_EXTRA_FIELDS.TIME_TIMESTAMP), prefix, implicitRanges), periodQueryBuilder(rangePaths, prefix, implicitRanges)) - } else { - if(path == "meta.lastUpdated") - dateQueryBuilder(FHIRUtil.mergeElementPath(path,FHIR_EXTRA_FIELDS.TIME_DATE), prefix, implicitRanges) - else - //If it is year, year-month, or date query - //Query over the sub date field - or(dateQueryBuilder(FHIRUtil.mergeElementPath(path,FHIR_EXTRA_FIELDS.TIME_DATE), prefix, implicitRanges), periodQueryBuilder(rangePaths, prefix, implicitRanges)) - } - } - - /** - * Handle prefixes for period parameters. For further information - * about the prefixes with range values please refer to prefix table's - * third column in page; https://www.hl7.org/fhir/search.html#prefix - * - * @param path absolute path of the parameter - * @param value value of the parameter - * @param prefix prefix of the parameter - * @param isTiming determines if the field is timing - * @return BsonDocument for the query - */ - def periodPrefixHandler(path:String, value:String, prefix:String, isTiming:Boolean): Bson = { - // Generate period fields - val periodPath = if(isTiming) FHIRUtil.mergeElementPath(path,s"${FHIR_COMMON_FIELDS.REPEAT}.${FHIR_COMMON_FIELDS.BOUNDS_PERIOD}") else path - val periodRanges = ( - FHIRUtil.mergeElementPath(periodPath, s"${FHIR_COMMON_FIELDS.START}.${FHIR_EXTRA_FIELDS.TIME_TIMESTAMP}"), - FHIRUtil.mergeElementPath(periodPath, s"${FHIR_COMMON_FIELDS.END}.${FHIR_EXTRA_FIELDS.TIME_TIMESTAMP}") - ) - // Populate implicit date ranges(e.g. 2010 represents the range 2010-01-01/2010-12-31) - //val implicitDate = DateTimeUtil.populateImplicitDateRanges(value) - // Populate implicit date time ranges(i.e. same process with the time ranges) - val implicitDateTime = DateTimeUtil.populateImplicitDateTimeRanges(value) - // Generate queries for both date and date time ranges - periodQueryBuilder(periodRanges, prefix, implicitDateTime) - //or(periodQueryBuilder(periodRanges, prefix, implicitDate), periodQueryBuilder(periodRanges, prefix, implicitDateTime)) - } - - /** - * Special processing for Timing.event; all elements should satisfy the query - * @param path - * @param value - * @param prefix - * @return - */ - def timingEventHandler(path:String, value:String, prefix:String):Bson = { - // Populate Implicit ranges(e.g. 2010-10-10 represents the range 2010-10-10T00:00Z/2010-10-10T23:59ZZ) - val implicitRanges = DateTimeUtil.populateImplicitDateTimeRanges(value) - // Convert implicit range to dateTime objects(inputted values have already been converted to dataTime format) - var (floor, ceil) = (OnFhirBsonTransformer.dateToISODate(implicitRanges._1), OnFhirBsonTransformer.dateToISODate(implicitRanges._2)) - - val subpath = if (value.contains("T")) FHIR_EXTRA_FIELDS.TIME_TIMESTAMP else FHIR_EXTRA_FIELDS.TIME_DATE - dateRangePrefixHandler(subpath, value, prefix) - val oppositeQuery = prefix match { - case FHIR_PREFIXES_MODIFIERS.BLANK_EQUAL | FHIR_PREFIXES_MODIFIERS.EQUAL => or(lt(subpath, floor), gt(subpath, ceil)) - case FHIR_PREFIXES_MODIFIERS.GREATER_THAN | FHIR_PREFIXES_MODIFIERS.GREATER_THAN_M => or(lt(subpath, floor), and(gte(subpath, floor), lt(subpath, ceil)), equal(path, floor)) - case FHIR_PREFIXES_MODIFIERS.LESS_THAN | FHIR_PREFIXES_MODIFIERS.LESS_THAN_M => or(gt(subpath, ceil), and(gte(subpath, floor), lt(subpath, ceil)), equal(subpath, floor)) - case FHIR_PREFIXES_MODIFIERS.GREATER_THAN_EQUAL => lt(subpath, floor) - case FHIR_PREFIXES_MODIFIERS.LESS_THAN_EQUAL => gt(subpath, ceil) - case FHIR_PREFIXES_MODIFIERS.NOT_EQUAL => or(lt(subpath, floor), gt(subpath, ceil)) - case FHIR_PREFIXES_MODIFIERS.STARTS_AFTER => or(lt(subpath, ceil), equal(subpath, ceil)) - case FHIR_PREFIXES_MODIFIERS.ENDS_BEFORE => or(gt(subpath, floor), equal(subpath, floor)) - case FHIR_PREFIXES_MODIFIERS.APPROXIMATE => - if (ceil == floor) - or(lt(subpath, floor), gt(subpath, ceil)) - else { - val delta: Long = ((ceil.asInstanceOf[BsonDateTime].getValue - floor.asInstanceOf[BsonDateTime].getValue) * 0.1).toLong - ceil = BsonDateTime.apply(ceil.asInstanceOf[BsonDateTime].getValue + delta) - floor = BsonDateTime.apply(floor.asInstanceOf[BsonDateTime].getValue - delta) - or(lt(subpath, floor), gt(subpath, ceil)) - } - } - val (fieldStart, fieldEnd) = (FHIR_EXTRA_FIELDS.TIME_RANGE_START, FHIR_EXTRA_FIELDS.TIME_RANGE_END) - - val oppositeQuery2 = prefix match { - case FHIR_PREFIXES_MODIFIERS.BLANK_EQUAL | FHIR_PREFIXES_MODIFIERS.EQUAL => or(lt(fieldStart, floor), gt(fieldEnd, ceil)) - case FHIR_PREFIXES_MODIFIERS.GREATER_THAN | FHIR_PREFIXES_MODIFIERS.GREATER_THAN_M => lte(fieldEnd, ceil) - case FHIR_PREFIXES_MODIFIERS.LESS_THAN | FHIR_PREFIXES_MODIFIERS.LESS_THAN_M => gte(fieldStart, floor) - case FHIR_PREFIXES_MODIFIERS.GREATER_THAN_EQUAL => or(lte(fieldEnd, ceil), lt(fieldStart, floor), gt(fieldEnd, ceil)) - case FHIR_PREFIXES_MODIFIERS.LESS_THAN_EQUAL => or(gte(fieldStart, floor), lt(fieldStart, floor), gt(fieldEnd, ceil)) - case FHIR_PREFIXES_MODIFIERS.NOT_EQUAL => or(lt(fieldStart, floor), gt(fieldEnd, ceil)) - case FHIR_PREFIXES_MODIFIERS.STARTS_AFTER => lte(fieldStart, ceil) - case FHIR_PREFIXES_MODIFIERS.ENDS_BEFORE => gte(fieldEnd, floor) - case FHIR_PREFIXES_MODIFIERS.APPROXIMATE => - if (ceil == floor) - or(lt(fieldStart, floor), gt(fieldEnd, ceil)) - else { - val delta: Long = ((ceil.asInstanceOf[BsonDateTime].getValue - floor.asInstanceOf[BsonDateTime].getValue) * 0.1).toLong - ceil = BsonDateTime.apply(ceil.asInstanceOf[BsonDateTime].getValue + delta) - floor = BsonDateTime.apply(floor.asInstanceOf[BsonDateTime].getValue - delta) - or(lt(fieldStart, floor), gt(fieldEnd, ceil)) - } - } - - and( - Filters.exists(FHIRUtil.mergeElementPath(path, "event")), - if(prefix == FHIR_PREFIXES_MODIFIERS.NOT_EQUAL) - elemMatch(FHIRUtil.mergeElementPath(path, "event"), or(oppositeQuery, oppositeQuery2)) - else - Filters.nor(elemMatch(FHIRUtil.mergeElementPath(path, "event"), or(oppositeQuery, oppositeQuery2))) - ) - } - - - /** - * Query builders for period type searches - * - * @param path path to the lower and upper boundaries - * @param prefix prefix of the date - * @param valueRange value of lower and upper boundaries - * @return BsonDocument for the target query - */ - private def periodQueryBuilder(path:(String, String), prefix:String, valueRange:(String, String)):Bson = { - val isoDate:(BsonValue, BsonValue) = (OnFhirBsonTransformer.dateToISODate(valueRange._1), OnFhirBsonTransformer.dateToISODate(valueRange._2)) - /*try { - // Try to convert input date to date time(only fails when checking periods for date) - isoDate = (DateTimeUtil.dateToISODate(valueRange._1), DateTimeUtil.dateToISODate(valueRange._2)) - } catch { - // If the conversion fails accept it as a string - case e:IllegalArgumentException => isoDate = (BsonString(valueRange._1), BsonString(valueRange._2)) - }*/ - // Initiliaze start and end fields of the ranges - val (fieldStart, fieldEnd) = path - // Implicit date range - var(floor, ceil) = isoDate - // BsonDocuments that represent the nonexistence of boundary values - val fieldEndNotExist = and(exists(fieldStart, exists=true), exists(fieldEnd, exists=false)) - val fieldStartNotExist = and(exists(fieldStart, exists=false), exists(fieldEnd, exists=true)) - - // Prefix matching and query generation - prefix match { - case FHIR_PREFIXES_MODIFIERS.BLANK_EQUAL | FHIR_PREFIXES_MODIFIERS.EQUAL => and(gte(fieldStart, floor), lte(fieldEnd, ceil)) - case FHIR_PREFIXES_MODIFIERS.GREATER_THAN | FHIR_PREFIXES_MODIFIERS.GREATER_THAN_M => or(gt(fieldEnd, ceil), fieldEndNotExist) - case FHIR_PREFIXES_MODIFIERS.LESS_THAN | FHIR_PREFIXES_MODIFIERS.LESS_THAN_M => or(lt(fieldStart, floor), fieldStartNotExist) - case FHIR_PREFIXES_MODIFIERS.GREATER_THAN_EQUAL => or(periodQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.EQUAL, valueRange), periodQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.GREATER_THAN, valueRange)) - case FHIR_PREFIXES_MODIFIERS.LESS_THAN_EQUAL => or(periodQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.EQUAL, valueRange), periodQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.LESS_THAN, valueRange)) - case FHIR_PREFIXES_MODIFIERS.NOT_EQUAL => or(periodQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.LESS_THAN, valueRange), periodQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.GREATER_THAN, valueRange)) - case FHIR_PREFIXES_MODIFIERS.STARTS_AFTER => gt(fieldStart, ceil) - //or(periodQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.NOT_EQUAL, valueRange), periodQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.LESS_THAN, valueRange)) - case FHIR_PREFIXES_MODIFIERS.ENDS_BEFORE => lt(fieldEnd, floor) - //or(periodQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.NOT_EQUAL, valueRange), periodQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.GREATER_THAN, valueRange)) - case FHIR_PREFIXES_MODIFIERS.APPROXIMATE => - if(ceil == floor) - and(gte(fieldStart, floor), lte(fieldEnd, ceil)) - else { - val delta:Long = ((ceil.asInstanceOf[BsonDateTime].getValue - floor.asInstanceOf[BsonDateTime].getValue) * 0.1).toLong - ceil = BsonDateTime.apply(ceil.asInstanceOf[BsonDateTime].getValue + delta) - floor = BsonDateTime.apply(floor.asInstanceOf[BsonDateTime].getValue - delta) - and(gte(fieldStart, floor), lte(fieldEnd, ceil)) - } - } - } - - /** - * Query builders for dateTime type searches e.g. ge2012-10-15T10:00:00Z - * - * @param path path to the target value - * @param prefix prefix of the date - * @param valueRange value of lower and upper boundaries - * @return BsonDocument for the target query - */ - private def dateTimeQueryBuilder(path:String, prefix:String, valueRange:(String, String)):Bson = { - // Convert implicit range to dateTime objects(inputted values have already been converted to dataTime format) - var(floor, ceil) = (OnFhirBsonTransformer.dateToISODate(valueRange._1), OnFhirBsonTransformer.dateToISODate(valueRange._2)) - prefix match { - case FHIR_PREFIXES_MODIFIERS.BLANK_EQUAL | FHIR_PREFIXES_MODIFIERS.EQUAL => or(and(gte(path, floor), lt(path, ceil)), equal(path, floor)) - case FHIR_PREFIXES_MODIFIERS.GREATER_THAN | FHIR_PREFIXES_MODIFIERS.GREATER_THAN_M => gt(path, ceil) - case FHIR_PREFIXES_MODIFIERS.LESS_THAN | FHIR_PREFIXES_MODIFIERS.LESS_THAN_M => lt(path, floor) - case FHIR_PREFIXES_MODIFIERS.GREATER_THAN_EQUAL => or(dateTimeQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.EQUAL, valueRange), dateTimeQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.GREATER_THAN, valueRange)) - case FHIR_PREFIXES_MODIFIERS.LESS_THAN_EQUAL => or(dateTimeQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.EQUAL, valueRange), dateTimeQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.LESS_THAN, valueRange)) - case FHIR_PREFIXES_MODIFIERS.NOT_EQUAL => or(lt(path, floor), gt(path, ceil)) - case FHIR_PREFIXES_MODIFIERS.STARTS_AFTER => gt(path, ceil) - //or(dateTimeQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.NOT_EQUAL, valueRange), dateTimeQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.LESS_THAN, valueRange)) - case FHIR_PREFIXES_MODIFIERS.ENDS_BEFORE => lt(path, floor) - //or(dateTimeQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.NOT_EQUAL, valueRange), dateTimeQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.GREATER_THAN, valueRange)) - case FHIR_PREFIXES_MODIFIERS.APPROXIMATE => - if(ceil == floor) - or(and(gte(path, floor), lt(path, ceil)), equal(path, floor)) - else { - val delta:Long = ((ceil.asInstanceOf[BsonDateTime].getValue - floor.asInstanceOf[BsonDateTime].getValue) * 0.1).toLong - ceil = BsonDateTime.apply(ceil.asInstanceOf[BsonDateTime].getValue + delta) - floor = BsonDateTime.apply(floor.asInstanceOf[BsonDateTime].getValue - delta) - or(and(gte(path, floor), lt(path, ceil)), equal(path, floor)) - } - } - } - - /** - * Query builders for date type searches e.g. ge2012-10-15 or eq2012-05 - * - * @param path path to the target value - * @param prefix prefix of the date - * @param valueRange value of lower and upper boundaries - * @return BsonDocument for the target query - */ - private def dateQueryBuilder(path: String, prefix: String, valueRange: (String, String)): Bson = { - // Convert implicit range to dateTime objects(inputted values have already been converted to dataTime format) - var (floor, ceil) = (OnFhirBsonTransformer.dateToISODate(valueRange._1), OnFhirBsonTransformer.dateToISODate(valueRange._2)) - prefix match { - case FHIR_PREFIXES_MODIFIERS.BLANK_EQUAL | FHIR_PREFIXES_MODIFIERS.EQUAL => or(and(gte(path, floor), lt(path, ceil)), equal(path, floor)) - case FHIR_PREFIXES_MODIFIERS.GREATER_THAN | FHIR_PREFIXES_MODIFIERS.GREATER_THAN_M => gt(path, ceil) - case FHIR_PREFIXES_MODIFIERS.LESS_THAN | FHIR_PREFIXES_MODIFIERS.LESS_THAN_M => lt(path, floor) - case FHIR_PREFIXES_MODIFIERS.GREATER_THAN_EQUAL => gte(path, floor) - case FHIR_PREFIXES_MODIFIERS.LESS_THAN_EQUAL => lte(path, ceil) - case FHIR_PREFIXES_MODIFIERS.NOT_EQUAL => or(lt(path, floor), gt(path, ceil)) - case FHIR_PREFIXES_MODIFIERS.STARTS_AFTER => gt(path, ceil) - //or(dateTimeQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.NOT_EQUAL, valueRange), dateTimeQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.LESS_THAN, valueRange)) - case FHIR_PREFIXES_MODIFIERS.ENDS_BEFORE => lt(path, floor) - //or(dateTimeQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.NOT_EQUAL, valueRange), dateTimeQueryBuilder(path, FHIR_PREFIXES_MODIFIERS.GREATER_THAN, valueRange)) - case FHIR_PREFIXES_MODIFIERS.APPROXIMATE => - if (ceil == floor) - or(and(gte(path, floor), lt(path, ceil)), equal(path, floor)) - else { - val delta: Long = ((ceil.asInstanceOf[BsonDateTime].getValue - floor.asInstanceOf[BsonDateTime].getValue) * 0.1).toLong - ceil = BsonDateTime.apply(ceil.asInstanceOf[BsonDateTime].getValue + delta) - floor = BsonDateTime.apply(floor.asInstanceOf[BsonDateTime].getValue - delta) - or(and(gte(path, floor), lt(path, ceil)), equal(path, floor)) - } - } - } - -} diff --git a/onfhir-core/src/main/scala/io/onfhir/db/QuantityQueryBuilder.scala b/onfhir-core/src/main/scala/io/onfhir/db/QuantityQueryBuilder.scala new file mode 100644 index 00000000..fbde829f --- /dev/null +++ b/onfhir-core/src/main/scala/io/onfhir/db/QuantityQueryBuilder.scala @@ -0,0 +1,127 @@ +package io.onfhir.db + +import io.onfhir.api.{FHIR_COMMON_FIELDS, FHIR_DATA_TYPES} +import io.onfhir.api.util.FHIRUtil +import io.onfhir.exception.InternalServerException +import org.mongodb.scala.bson.conversions.Bson +import org.mongodb.scala.model.Filters + +object QuantityQueryBuilder extends IFhirQueryBuilder { + + /** + * A quantity parameter searches on the Quantity data type. The syntax for the + * value follows the form: + * + * [parameter]=[prefix][number]|[system]|[code] matches a quantity with the given unit + * + * @param prefixAndValues Supplied prefix and values + * @param path Path for the element + * @param targetType Type of target element + * @return + */ + def getQuery(prefixAndValues: Seq[(String, String)], path: String, targetType: String): Bson = { + orQueries( + prefixAndValues + .map { + case (prefix, parameterValue) => + //Parse the given value + val (value, system, code) = FHIRUtil.parseQuantityValue(parameterValue) + + //Find out the elemMatch and query parts of the path + val (elemMatchPath, queryPath) = FHIRUtil.splitElementPathIntoElemMatchAndQueryPaths(path) + + //Try to construct main query + val mainQuery = targetType match { + case FHIR_DATA_TYPES.QUANTITY | + FHIR_DATA_TYPES.SIMPLE_QUANTITY | + FHIR_DATA_TYPES.MONEY_QUANTITY | + FHIR_DATA_TYPES.AGE | + FHIR_DATA_TYPES.DISTANCE | + FHIR_DATA_TYPES.COUNT | + FHIR_DATA_TYPES.DURATION => + //Query on the quentity + val valueQuery = NumberQueryBuilder.getQueryForDecimal(FHIRUtil.mergeElementPath(queryPath, FHIR_COMMON_FIELDS.VALUE), value, prefix) + //Also merge it with query on system and code + val sysCodeQuery = getQueryForUnitSystemCode(system, code, queryPath, FHIR_COMMON_FIELDS.SYSTEM, FHIR_COMMON_FIELDS.CODE, FHIR_COMMON_FIELDS.UNIT) + sysCodeQuery + .map(sq => Filters.and(valueQuery, sq)) + .getOrElse(valueQuery) + + case FHIR_DATA_TYPES.MONEY => + //Query on the quatity + val valueQuery = NumberQueryBuilder.getQueryForDecimal(FHIRUtil.mergeElementPath(queryPath, FHIR_COMMON_FIELDS.VALUE), value, prefix) + //Also merge it with query on currency code + val sysCodeQuery = code.map(c => Filters.eq(FHIRUtil.mergeElementPath(queryPath, FHIR_COMMON_FIELDS.CURRENCY), c)) + sysCodeQuery.map(sq => Filters.and(valueQuery, sq)).getOrElse(valueQuery) + + //Handle range + case FHIR_DATA_TYPES.RANGE => + //Query on range values + val valueQuery = NumberQueryBuilder.getQueryForRange(queryPath.getOrElse(""), value, prefix) + val lowPath = FHIRUtil.mergeElementPath(queryPath, FHIR_COMMON_FIELDS.LOW) + val highPath = FHIRUtil.mergeElementPath(queryPath, FHIR_COMMON_FIELDS.HIGH) + + val lq = + getQueryForUnitSystemCode(system, code, Some(lowPath), FHIR_COMMON_FIELDS.SYSTEM, FHIR_COMMON_FIELDS.CODE, FHIR_COMMON_FIELDS.UNIT) + .map(sq => Filters.or(Filters.exists(lowPath, exists = false), sq)) + val hq = + getQueryForUnitSystemCode(system, code, Some(highPath), FHIR_COMMON_FIELDS.SYSTEM, FHIR_COMMON_FIELDS.CODE, FHIR_COMMON_FIELDS.UNIT) + .map(sq => Filters.or(Filters.exists(highPath, exists = false), sq)) + //Merge all (both lq and hq should be SOME or NONE + lq.map(Filters.and(_, hq.get, valueQuery)).getOrElse(valueQuery) + + case FHIR_DATA_TYPES.SAMPLED_DATA => + //For SampleData, we should check for lowerLimit and upperLimit like a range query + val valueQuery = NumberQueryBuilder.getQueryForRange(queryPath.getOrElse(""), value, prefix, isSampleData = true) + val sysCodeQuery = + getQueryForUnitSystemCode(system, code, queryPath, + systemPath = s"${FHIR_COMMON_FIELDS.ORIGIN}.${FHIR_COMMON_FIELDS.SYSTEM}", + codePath = s"${FHIR_COMMON_FIELDS.ORIGIN}.${FHIR_COMMON_FIELDS.CODE}", + unitPath = s"${FHIR_COMMON_FIELDS.ORIGIN}.${FHIR_COMMON_FIELDS.UNIT}") + sysCodeQuery + .map(sq => Filters.and(valueQuery, sq)) + .getOrElse(valueQuery) + } + //If an array exist, use elemMatch otherwise return the query + getFinalQuery(elemMatchPath, mainQuery) + } + ) + } + + /** + * Merge the query ont the Quantity value with system and code restrictions + * + * @param system Expected system + * @param code Expected code/unit + * @param queryPath Main path to the FHIR quantity element + * @param systemPath system field path within the element + * @param codePath code field path within the element + * @param unitPath unit field path within the element + * @return + */ + private def getQueryForUnitSystemCode(system: Option[String], code: Option[String], queryPath: Option[String], systemPath: String, codePath: String, unitPath: String): Option[Bson] = { + (system, code) match { + //Only query on value + case (None, None) => + None + //Query on value + unit + case (None, Some(c)) => + Some( + Filters.or( + Filters.eq(FHIRUtil.mergeElementPath(queryPath, codePath), c), + Filters.eq(FHIRUtil.mergeElementPath(queryPath, unitPath), c) + ) + ) + //query on value + system + code + case (Some(s), Some(c)) => + Some( + Filters.and( + Filters.eq(FHIRUtil.mergeElementPath(queryPath, codePath), c), + Filters.eq(FHIRUtil.mergeElementPath(queryPath, systemPath), s) + ) + ) + case _ => throw new InternalServerException("Invalid state!") + } + } + +} diff --git a/onfhir-core/src/main/scala/io/onfhir/db/ReferenceQueryBuilder.scala b/onfhir-core/src/main/scala/io/onfhir/db/ReferenceQueryBuilder.scala new file mode 100644 index 00000000..5b29e663 --- /dev/null +++ b/onfhir-core/src/main/scala/io/onfhir/db/ReferenceQueryBuilder.scala @@ -0,0 +1,336 @@ +package io.onfhir.db + +import io.onfhir.api.util.FHIRUtil +import io.onfhir.api.{FHIR_COMMON_FIELDS, FHIR_DATA_TYPES, FHIR_PREFIXES_MODIFIERS, FHIR_EXTRA_FIELDS} +import io.onfhir.config.OnfhirConfig +import io.onfhir.exception.InvalidParameterException +import org.mongodb.scala.bson.conversions.Bson +import org.mongodb.scala.model.Filters + +/** + * Utility class to construct queries for FHIR reference type search parameters for a specific path and target type + * Over Reference type elements, this supports the following modifiers + * - :identifier + * - :[type] + * - :type Onfhir specific modifier to search on resource type references e.g. subject:type=Patient + * + * TODO :above :below not supported yet + * + * Over canonical type elements, supports the following modifiers + * - :below + * + * TODO :above is not supported yet + * + * @param onlyLocalReferences Whether the FHIR server supports only local references + */ +class ReferenceQueryBuilder(onlyLocalReferences:Boolean) extends IFhirQueryBuilder { + + /** + * A reference parameter refers to references between resources. The interpretation of a reference + * parameter is either: + * + * [parameter]=[id] the logical [id] of a resource using a local reference (i.e. a relative reference) + * + * [parameter]=[type]/[id] the logical [id] of a resource of a specified type using + * a local reference (i.e. a relative reference), for when the reference can point to different + * types of resources (e.g. Observation.subject) + * + * [parameter]=[url] where the [url] is an absolute URL - a reference to a resource by its absolute location + * + * @param values Supplied parameter values e.g. Patient/513515 + * @param modifier Modifier used ("" indicates no modifier) + * @param path Path to the target element to run search on + * @param targetType Data type of the target element + * @param targetReferenceTypes Allowed resource types to give reference for the target element + * @return Equivalent BsonDocument for the target query + */ + def getQuery(values:Seq[String], modifier:String, path:String, targetType:String, targetReferenceTypes:Seq[String]):Bson = { + targetType match { + //If this is a search on a FHIR Reference type element + case FHIR_DATA_TYPES.REFERENCE => + getQueryOnReference(values, modifier, path, targetReferenceTypes) + //If this is a search on Canonical references + case FHIR_DATA_TYPES.CANONICAL => + ReferenceQueryBuilder.getQueryOnCanonicals(values, modifier, path) + case _ => + throw new InvalidParameterException(s"Invalid usage of parameter. The reference type parameters cannot search on $targetType type elements!!!") + } + } + + + /** + * Get query for reference queries over FHIR Reference elements + * + * @param values Supplied parameter values e.g. Patient/513515 + * @param modifier Modifier used ("" indicates no modifier) + * @param path Path to the target element to run search on + * @param targetReferenceTypes Allowed resource types to give reference for the target element + * @return + */ + private def getQueryOnReference(values:Seq[String], modifier:String, path:String, targetReferenceTypes:Seq[String]):Bson = { + modifier match { + //No modifier, normal reference search + //e.g. Observation?subject=Patient/465465,Patient/4654654 + case "" => + getQueryForReferences(values, path, targetReferenceTypes) + //If modifier is identifier, search like a token on identifier element (Reference.identifier) + //e.g. Observation?subject:identifier=http://example.org/fhir/mrn|12345 + case FHIR_PREFIXES_MODIFIERS.IDENTIFIER => + getQueryForIdentifierModifier(values, path) + + //Handle :text modifier (search on Reference.display) + //e.g. Observation?subject:display=Tuncay + case FHIR_PREFIXES_MODIFIERS.TEXT => + orQueries(values.map(value => StringQueryBuilder.getQueryOnTargetStringElement(FHIRUtil.normalizeElementPath(FHIRUtil.mergeElementPath(path, FHIR_COMMON_FIELDS.DISPLAY)), value, ""))) + + //Handle :type modifier (searching with only type - onFHIR specific) + //e.g. Observation?subject:type=Patient + case FHIR_PREFIXES_MODIFIERS.TYPE => + ReferenceQueryBuilder.getQueryForTypeModifier(values, path) + + case FHIR_PREFIXES_MODIFIERS.ABOVE | FHIR_PREFIXES_MODIFIERS.BELOW => + throw new InvalidParameterException(s"The modifier $modifier is not supported by onFhir.io yet !!!") + + //Handle :[type] modifier + //e.g. Observation?subject:Patient=321,5464 + case resourceType if resourceType.apply(1).isUpper => + getQueryForReferences(values, path, Seq(resourceType.drop(1))) + + case _ => + throw new InvalidParameterException(s"The modifier $modifier is not supported by onFhir.io yet !!!") + } + } + + /** + * Construct query for :identifier modifier + * e.g. Observation?subject:identifier=http://example.org/fhir/mrn|12345 + * @param values Supplied parameter values e.g. http://example.org/fhir/mrn|12345,http://example.org/fhir/mrn|12346 + * @param path Path to the element e.g. subject.reference + * @return + */ + private def getQueryForIdentifierModifier(values:Seq[String], path:String):Bson = { + //Find out the elemMatch and query parts of the path + val (elemMatchPath, queryPath) = FHIRUtil.splitElementPathIntoElemMatchAndQueryPaths(path) + // Parse the given the parameter value that should be [system]|[value] format + val parsedSystemAndValues = values.map(FHIRUtil.parseTokenValue) + if (parsedSystemAndValues.exists(sv => sv._1.isEmpty || sv._2.isEmpty)) + throw new InvalidParameterException(s"Invalid usage of parameter. The parameter value should be provided in [system]|[value] format for ${FHIR_PREFIXES_MODIFIERS.IDENTIFIER} modifier!!!") + + val groupedSystemValues = + parsedSystemAndValues + .map(sv => sv._1.get -> sv._2.get) + .groupBy(_._1).map(g => g._1 -> g._2.map(_._2)) + .toSeq + + val systemPath = FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.IDENTIFIER}.${FHIR_COMMON_FIELDS.SYSTEM}") + val valuePath = FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.IDENTIFIER}.${FHIR_COMMON_FIELDS.VALUE}") + + val mainQuery = + orQueries( + groupedSystemValues + .map { + case (system, Seq(single)) => Filters.and(Filters.eq(systemPath, system), Filters.eq(valuePath, single)) + case (system, values) => Filters.and(Filters.eq(systemPath, system), Filters.in(valuePath, values: _*)) + } + ) + getFinalQuery(elemMatchPath, mainQuery) + } + + /** + * Construct query for normal reference search + * e.g. Observation?subject=Patient/35135,Patient/25423451 + * @param values Supplied parameter values e.g. Patient/2413 + * @param path Path to the Reference type element + * @param targetReferenceTypes Allowed target referenced types + * @return + */ + def getQueryForReferences(values:Seq[String], path:String, targetReferenceTypes:Seq[String]):Bson = { + //Find out the elemMatch and query parts of the path + val (elemMatchPath, queryPath) = FHIRUtil.splitElementPathIntoElemMatchAndQueryPaths(path) + //Parse reference value (URL part, resource type, resource id, version) + //(URL part, resource type, resource id, version) + val parsedReferences: Seq[(Option[String], String, String, Option[String])] = + values + .map(reference => FHIRUtil.resolveReferenceValue(reference, "", targetReferenceTypes)) + .map { + case (Some(OnfhirConfig.fhirRootUrl), rtype, rid, version) => (None, rtype, rid, version) //If root url of server is given just ignore it + case (Some(url), rtype, rid, version) => (Some(url), rtype, rid, version) + case oth => oth + } + + //For the ones where a version is not given, construct queries + //e.g. Patient/31321 + //e.g. http://onfhir.io/fhir/Patient/151 + val referencesWithoutVersion = parsedReferences.filter(_._4.isEmpty) + //Group the references with url, rtype + val groupedReferences = referencesWithoutVersion.groupBy(r => (r._1, r._2)).map(g => g._1 -> g._2.map(_._3)).toSeq + val queries = + groupedReferences + .map { + //Local reference allowed only and no url given (or local server's url given), check only resource type and identifiers + case ((None, rtype), references) if onlyLocalReferences=> + ReferenceQueryBuilder.getQueryForReferenceMatch(queryPath, rtype, references) + //If url is not given, but server also allowed referencing to remote url resources + case ((None, rtype), references) => + Filters.and( + ReferenceQueryBuilder.getQueryForReferenceMatch(queryPath, rtype, references), + Filters.or( + Filters.eq(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_URL}"), OnfhirConfig.fhirRootUrl), //Either it should equal to our root URL + Filters.exists(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_URL}"), exists= false) //Or url part does not exist + ) + ) + case ((Some(url),rtype), references ) => + Filters.and( + ReferenceQueryBuilder.getQueryForReferenceMatch(queryPath, rtype, references), + Filters.eq(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_URL}"), url) + ) + } + //For the ones where version is given + //e.g. Patient/123/_history/1 + val referencesWithVersion = parsedReferences.filter(_._4.isDefined) + val queriesWithVersions = + referencesWithVersion + .map { + case (None, rtype, rid, Some(version)) if onlyLocalReferences => + Filters.and( + ReferenceQueryBuilder.getQueryForReferenceMatch(queryPath, rtype, Seq(rid)), + Filters.eq(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_RESOURCE_VERSION}"), version) + ) + case (None, rtype, rid, Some(version)) => + Filters.and( + ReferenceQueryBuilder.getQueryForReferenceMatch(queryPath, rtype, Seq(rid)), + Filters.or( + Filters.eq(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_URL}"), OnfhirConfig.fhirRootUrl), //Either it should equal to our root URL + Filters.exists(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_URL}"), exists = false) //Or url part does not exist + ), + Filters.eq(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_RESOURCE_VERSION}"), version) + ) + case (Some(url), rtype, rid, Some(version)) => + Filters.and( + ReferenceQueryBuilder.getQueryForReferenceMatch(queryPath, rtype, Seq(rid)), + Filters.eq(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_URL}"), url), + Filters.eq(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_RESOURCE_VERSION}"), version) + ) + } + //Combine all the queries for each group with Logical OR + val mainQuery = orQueries(queries ++ queriesWithVersions) + getFinalQuery(elemMatchPath, mainQuery) + } + + +} + +object ReferenceQueryBuilder extends IFhirQueryBuilder { + + /** + * Construct the query to search resources with canonical urls and versions + * + * @param urlAndVersions List of url and versions + * @return + */ + def getQueryOnCanonicalRefs(urlAndVersions: Seq[(String, Option[String])]): Bson = { + val canonicalWithVersions = urlAndVersions.filter(_._2.isDefined) + val canonicalWithoutVersions = urlAndVersions.filter(_._2.isEmpty) + + val withoutVersionQueries = + if(canonicalWithoutVersions.nonEmpty) + Some(Filters.in(FHIR_COMMON_FIELDS.URL, canonicalWithoutVersions.map(_._1):_*)) + else + None + + val withVersionQueries = + canonicalWithVersions.map { + case (url, None) => Filters.eq(FHIR_COMMON_FIELDS.URL, url) + case (url, Some(v)) => Filters.and(Filters.eq(FHIR_COMMON_FIELDS.URL, url), Filters.eq(FHIR_COMMON_FIELDS.VERSION, v)) + } + orQueries(withVersionQueries ++ withoutVersionQueries.toSeq) + } + /** + * Construct query for reference type parameters on canonical references + * + * @param values Supplied parameter values e.g. http://onfhir.io/fhir/ValueSet/myValueSet|1.0 + * @param modifier Modifier used ("" indicates no modifier) + * @param path Path to the target element to run search on + * @return + */ + def getQueryOnCanonicals(values: Seq[String], modifier: String, path: String): Bson = { + //As for canonical, we only look at one field we don't need to care arrays in the path + modifier match { + //No modifier + case "" => + val parsedCanonicals = values.map(FHIRUtil.parseCanonicalValue) + val canonicalWithVersions = + parsedCanonicals + .filter(_._2.isDefined) + .map(c => s"${c._1}|${c._2.get}") + + val queryWithVersion = + canonicalWithVersions match { + case Nil => None + case Seq(single) => Some(Filters.eq(FHIRUtil.normalizeElementPath(path), single)) + case multiple => Some(Filters.in(FHIRUtil.normalizeElementPath(path), multiple: _*)) + } + val queriesWithoutVersion = + parsedCanonicals.filter(_._2.isEmpty).map(_._1).map(url => + Filters.regex(FHIRUtil.normalizeElementPath(path), "\\A" + FHIRUtil.escapeCharacters(url) + "(\\|[0-9]+(\\.[0-9]*)*)?$") + ) + orQueries(queryWithVersion.toSeq ++ queriesWithoutVersion) + + //Searching with :below modifier + case FHIR_PREFIXES_MODIFIERS.BELOW => + orQueries( + values + .map(FHIRUtil.parseCanonicalValue) + .map { + case (canonicalUrl, canonicalVersion) => + // Escape characters for to have valid regular expression + val regularExpressionValue = FHIRUtil.escapeCharacters(canonicalUrl) + canonicalVersion.map(v => s"\\|$v(\\.[0-9]*)+").getOrElse("") + // Match at the beginning of the uri + Filters.regex(FHIRUtil.normalizeElementPath(path), "\\A" + regularExpressionValue + "$") + } + ) + case _ => + throw new InvalidParameterException(s"The modifier $modifier is not supported by onFhir.io yet for canonical targets !!!") + } + } + + /** + * Construct query for basic reference match + * + * @param queryPath Path to the Reference type element + * @param rtype Resource type to match for reference + * @param rids Resource id or ids to match + * @return + */ + private def getQueryForReferenceMatch(queryPath: Option[String], rtype: String, rids: Seq[String]): Bson = { + Filters + .and( + rids match { + case Seq(single) => + Filters.eq(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_RESOURCE_ID}"), single) + case multiple => + Filters.in(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_RESOURCE_ID}"), multiple: _*) + }, + Filters.eq(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_RESOURCE_TYPE}"), rtype) + ) + } + + + /** + * Get query for onFhir specific :type modifier + * + * @param values Resource type or types e.g. Patient, Observation + * @param path Path to the Reference type element + * @return + */ + private def getQueryForTypeModifier(values: Seq[String], path: String): Bson = { + //Find out the elemMatch and query parts of the path + val (elemMatchPath, queryPath) = FHIRUtil.splitElementPathIntoElemMatchAndQueryPaths(path) + val mainQuery = + values match { + case Seq(single) => Filters.eq(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_RESOURCE_TYPE}"), single) + case multiple => Filters.in(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_RESOURCE_TYPE}"), multiple: _*) + } + getFinalQuery(elemMatchPath, mainQuery) + } +} \ No newline at end of file diff --git a/onfhir-core/src/main/scala/io/onfhir/db/ResourceManager.scala b/onfhir-core/src/main/scala/io/onfhir/db/ResourceManager.scala index 5ffbc424..f1e3c974 100644 --- a/onfhir-core/src/main/scala/io/onfhir/db/ResourceManager.scala +++ b/onfhir-core/src/main/scala/io/onfhir/db/ResourceManager.scala @@ -7,36 +7,40 @@ import io.onfhir.api._ import io.onfhir.api.model.{FHIRSearchResult, FhirCanonicalReference, FhirLiteralReference, FhirReference, Parameter} import io.onfhir.api.parsers.FHIRResultParameterResolver import io.onfhir.api.util.FHIRUtil -import io.onfhir.config.{FhirServerConfig, OnfhirConfig} +import io.onfhir.config.{FhirServerConfig, OnfhirConfig, ResourceConf} import org.mongodb.scala.bson.collection.immutable.Document import org.mongodb.scala.bson.conversions.Bson import io.onfhir.db.OnFhirBsonTransformer._ -import io.onfhir.event.{FhirEventBus, IFhirEventBus, ResourceCreated, ResourceDeleted, ResourceUpdated} +import io.onfhir.event.{IFhirEventBus, ResourceCreated, ResourceDeleted, ResourceUpdated} import io.onfhir.exception.UnsupportedParameterException import io.onfhir.util.DateTimeUtil import org.json4s.JsonAST.JValue +import org.mongodb.scala.bson.{BsonBoolean, BsonString} +import org.mongodb.scala.model.{Aggregates, Filters, Projections} import org.slf4j.{Logger, LoggerFactory} import scala.concurrent.{ExecutionContext, Future} /** - * FHIR Resource Persistency Manager (Mapping FHIR operations to Mongo queries/commands) - * //TODO Handle resolution of Logical references for chaining and includes (also reverse) - */ -class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = null) { + * FHIR Resource Persistency Manager (Mapping FHIR operations to Mongo queries/commands) + * //TODO Handle resolution of Logical references for chaining and includes (also reverse) + */ +class ResourceManager(fhirConfig: FhirServerConfig, fhirEventBus: IFhirEventBus = null) { private implicit val logger: Logger = LoggerFactory.getLogger("ResourceManager") - implicit val executionContext:ExecutionContext = Onfhir.actorSystem.dispatchers.lookup("akka.actor.onfhir-blocking-dispatcher") + implicit val executionContext: ExecutionContext = Onfhir.actorSystem.dispatchers.lookup("akka.actor.onfhir-blocking-dispatcher") val fhirResultParameterResolver = new FHIRResultParameterResolver(fhirConfig) + /** - * FHIR Search Operation - * @param rtype Resource Type to search - * @param parameters Parsed FHIR parameters - * @param excludeExtraParams If true, the extra params set by Onfhir are excluded from the returned resources - * @return Num of Total Matched Resources, Matched Resources (with Paging) and Included Resources (based on matched ones) - */ - def searchResources(rtype:String, parameters:List[Parameter] = List.empty, excludeExtraParams:Boolean = false)(implicit transactionSession: Option[TransactionSession] = None):Future[FHIRSearchResult] = { + * FHIR Search Operation + * + * @param rtype Resource Type to search + * @param parameters Parsed FHIR parameters + * @param excludeExtraParams If true, the extra params set by Onfhir are excluded from the returned resources + * @return Num of Total Matched Resources, Matched Resources (with Paging) and Included Resources (based on matched ones) + */ + def searchResources(rtype: String, parameters: List[Parameter] = List.empty, excludeExtraParams: Boolean = false)(implicit transactionSession: Option[TransactionSession] = None): Future[FHIRSearchResult] = { //Extract FHIR result parameters val resultParameters = parameters.filter(_.paramCategory == FHIR_PARAMETER_CATEGORIES.RESULT) //Check _page and _count @@ -46,7 +50,7 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = //Check _elements param to include further val elementsIncludes = fhirResultParameterResolver.resolveElementsParameter(resultParameters) //Decide on final includes and excludes - val finalIncludesOrExcludes = if(elementsIncludes.nonEmpty) Some(true -> elementsIncludes) else summaryIncludesOrExcludes + val finalIncludesOrExcludes = if (elementsIncludes.nonEmpty) Some(true -> elementsIncludes) else summaryIncludesOrExcludes //Find sorting details val sortingFields = fhirResultParameterResolver.resolveSortingParameters(rtype, resultParameters) @@ -58,10 +62,10 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = val revIncludeParams = resultParameters.filter(_.name == FHIR_SEARCH_RESULT_PARAMETERS.REVINCLUDE) //Now others are query parameters - val queryParams = parameters.filter(_.paramCategory != FHIR_PARAMETER_CATEGORIES.RESULT) + val queryParams = parameters.filter(_.paramCategory != FHIR_PARAMETER_CATEGORIES.RESULT) //If _summary parameter is count, just count the documents - if(summaryIncludesOrExcludes.exists(s => s._1 && s._2.isEmpty)){ + if (summaryIncludesOrExcludes.exists(s => s._1 && s._2.isEmpty)) { countResources(rtype, queryParams) .map(total => FHIRSearchResult(total)) } else { //Otherwise normal search @@ -78,7 +82,7 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = } queryResultsFuture .flatMap(totalAndMatchedResources => - if(totalAndMatchedResources._2.nonEmpty) { + if (totalAndMatchedResources._2.nonEmpty) { //Handle _include and _revinclude params (includeParams, revIncludeParams) match { //No _include or _revinclude @@ -108,12 +112,13 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = /** * FHIR system level search over multiple resource types - * @param parametersForResourceTypes Parsed search parameters for each resource type to search - * @param excludeExtraParams If true, the extra params set by Onfhir are excluded from the returned resources - * @param transactionSession If part of a transaction, the transaction session - * @return Num of Total Matched Resources, Matched Resources (with Paging) + * + * @param parametersForResourceTypes Parsed search parameters for each resource type to search + * @param excludeExtraParams If true, the extra params set by Onfhir are excluded from the returned resources + * @param transactionSession If part of a transaction, the transaction session + * @return Num of Total Matched Resources, Matched Resources (with Paging) */ - def searchResourcesFromMultipleResourceTypes(parametersForResourceTypes:Map[String, List[Parameter]],excludeExtraParams:Boolean = false)(implicit transactionSession: Option[TransactionSession] = None):Future[(Long, Seq[Resource])] = { + def searchResourcesFromMultipleResourceTypes(parametersForResourceTypes: Map[String, List[Parameter]], excludeExtraParams: Boolean = false)(implicit transactionSession: Option[TransactionSession] = None): Future[(Long, Seq[Resource])] = { val resultParameters = parametersForResourceTypes .map(p => p._1 -> p._2.filter(_.paramCategory == FHIR_PARAMETER_CATEGORIES.RESULT)) @@ -134,10 +139,10 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = val needTotal = fhirResultParameterResolver.resolveTotalParameter(commonResultParameters) //For each resource type find out summary and sorting fields - val summaryFields = + val summaryFields = resultParameters .map(rparams => - rparams._1 -> fhirResultParameterResolver.resolveSummaryParameter( rparams._1, rparams._2) + rparams._1 -> fhirResultParameterResolver.resolveSummaryParameter(rparams._1, rparams._2) ) val sortingFields = resultParameters @@ -152,48 +157,47 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = .mapValues(_.filterNot(_.paramCategory == FHIR_PARAMETER_CATEGORIES.RESULT)) //If summary is count - if(summaryFields.exists(_._2.exists(s => s._1 && s._2.isEmpty))){ + if (summaryFields.exists(_._2.exists(s => s._1 && s._2.isEmpty))) { Future .sequence(queryParameters.map(rqp => countResources(rqp._1, rqp._2))) //Count the query for each resource type .map(counts => counts.sum -> Nil) //and sum them } else { - Future - .sequence(queryParameters.map(rqp => constructQuery(rqp._1, rqp._2).map(q => rqp._1 -> q))) //Construct queries for each resource type - .flatMap(queries => - queryResourcesDirectlyFromMultipleResourceTypes(queries.toMap, count, page,sortingFields, summaryFields, excludeExtraParams, needTotal) - ) + val queryMap = + queryParameters + .map { + case (rtype, params) => rtype -> constructQueryNew(rtype, params) + } + .toMap + + queryResourcesDirectlyFromMultipleResourceTypes(queryMap, count, page, sortingFields, summaryFields, excludeExtraParams, needTotal) } } /** - * Search FHIR resources of a specific Resource Type with given query - * @param rtype Resource type - * @param queryParams Parsed FHIR parameters - * @param count FHIR _count - * @param page FHIR _page - * @param sortingFields Sorting params their sort direction and field paths and target types e.g. date, 1, Seq((effectiveDateTime, DateTime), (effectiveInstant, Instant)) - * @param elementsIncludedOrExcluded Element paths to specifically include or exclude within the resource; first element true -> include, false -> exclude - * @param excludeExtraFields If true extra onFhir elements are excluded from the resource - * @param needTotal If false, the total number of matched resources is not returned, instead -1 is returned - * @return Number of Total resources and the resulting resources (due to paging only a subset can be returned) - */ - def queryResources(rtype:String, - queryParams:List[Parameter] = List.empty, - count:Int = -1, page:Int = 1, - sortingFields:Seq[(String, Int, Seq[(String, String)])] = Seq.empty, - elementsIncludedOrExcluded:Option[(Boolean, Set[String])] = None, - excludeExtraFields:Boolean = false, - needTotal:Boolean = true - )(implicit transactionSession: Option[TransactionSession] = None):Future[(Long, Seq[Resource])] = { - - //Run the search - constructQuery(rtype, queryParams).flatMap { - //If there is no query, although there are parameters - case None if queryParams.nonEmpty => Future.apply((0L, Nil)) - //Otherwise run it - case finalQuery => - queryResourcesDirectly(rtype, finalQuery, count, page, sortingFields, elementsIncludedOrExcluded, excludeExtraFields, needTotal) - } + * Search FHIR resources of a specific Resource Type with given query + * + * @param rtype Resource type + * @param queryParams Parsed FHIR parameters + * @param count FHIR _count + * @param page FHIR _page + * @param sortingFields Sorting params their sort direction and field paths and target types e.g. date, 1, Seq((effectiveDateTime, DateTime), (effectiveInstant, Instant)) + * @param elementsIncludedOrExcluded Element paths to specifically include or exclude within the resource; first element true -> include, false -> exclude + * @param excludeExtraFields If true extra onFhir elements are excluded from the resource + * @param needTotal If false, the total number of matched resources is not returned, instead -1 is returned + * @return Number of Total resources and the resulting resources (due to paging only a subset can be returned) + */ + def queryResources(rtype: String, + queryParams: List[Parameter] = List.empty, + count: Int = -1, page: Int = 1, + sortingFields: Seq[(String, Int, Seq[(String, String)])] = Seq.empty, + elementsIncludedOrExcluded: Option[(Boolean, Set[String])] = None, + excludeExtraFields: Boolean = false, + needTotal: Boolean = true + )(implicit transactionSession: Option[TransactionSession] = None): Future[(Long, Seq[Resource])] = { + + //Construct the MongoDB query and filtering stages if any + val (finalQuery, aggFilterStages) = constructQueryNew(rtype, queryParams) + queryResourcesDirectly(rtype, finalQuery, aggFilterStages, count, page, sortingFields, elementsIncludedOrExcluded, excludeExtraFields, needTotal) } /** @@ -214,51 +218,48 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = def queryResourcesWithOffsetPagination(rtype: String, queryParams: List[Parameter] = List.empty, count: Int = -1, - offset:(Seq[String],Boolean), + offset: (Seq[String], Boolean), sortingFields: Seq[(String, Int, Seq[(String, String)])] = Seq.empty, elementsIncludedOrExcluded: Option[(Boolean, Set[String])] = None, excludeExtraFields: Boolean = false, needTotal: Boolean = true )(implicit transactionSession: Option[TransactionSession] = None): Future[(Long, Seq[Resource], Seq[String], Seq[String])] = { + val (finalQuery, filteringStages) = constructQueryNew(rtype, queryParams) //Run the search - constructQuery(rtype, queryParams).flatMap { - //If there is no query, although there are parameters - case None if queryParams.nonEmpty => Future.apply((0L, Nil, Nil, Nil)) - //Otherwise run it - case finalQuery => - val offsetOpt = if(!offset._1.exists(_ != "")) None else Some(offset) - queryResourcesDirectlyWithOffsetPagination(rtype, finalQuery, count, offsetOpt, sortingFields, elementsIncludedOrExcluded, excludeExtraFields, needTotal) - } + val offsetOpt = if (!offset._1.exists(_ != "")) None else Some(offset) + queryResourcesDirectlyWithOffsetPagination(rtype, finalQuery, filteringStages, count, offsetOpt, sortingFields, elementsIncludedOrExcluded, excludeExtraFields, needTotal) } /** - * Search FHIR resources of a specific Resource Type with given query - * @param rtype Resource type - * @param query Mongo query - * @param count Number of resources to return - * @param page Page to return - * @param sortingFields Sorting fields (param name, sorting direction, path and target resource type - * @param elementsIncludedOrExcluded Elements to include or exclude - * @param excludeExtraFields If true, extra fields are cleared - * @param needTotal If total number of results is needed at the response - * @return - */ - def queryResourcesDirectly(rtype:String, - query:Option[Bson] = None, - count:Int = -1, - page:Int=1, - sortingFields:Seq[(String, Int, Seq[(String, String)])] = Seq.empty, - elementsIncludedOrExcluded:Option[(Boolean, Set[String])] = None, - excludeExtraFields:Boolean = false, - needTotal:Boolean = true)(implicit transactionSession: Option[TransactionSession] = None):Future[(Long, Seq[Resource])] = { + * Search FHIR resources of a specific Resource Type with given query + * + * @param rtype Resource type + * @param query Mongo query + * @param count Number of resources to return + * @param page Page to return + * @param sortingFields Sorting fields (param name, sorting direction, path and target resource type + * @param elementsIncludedOrExcluded Elements to include or exclude + * @param excludeExtraFields If true, extra fields are cleared + * @param needTotal If total number of results is needed at the response + * @return + */ + def queryResourcesDirectly(rtype: String, + query: Option[Bson] = None, + filteringStages: Seq[Bson] = Nil, + count: Int = -1, + page: Int = 1, + sortingFields: Seq[(String, Int, Seq[(String, String)])] = Seq.empty, + elementsIncludedOrExcluded: Option[(Boolean, Set[String])] = None, + excludeExtraFields: Boolean = false, + needTotal: Boolean = true)(implicit transactionSession: Option[TransactionSession] = None): Future[(Long, Seq[Resource])] = { val sortingPaths = constructFinalSortingPaths(sortingFields) for { - total <- if(needTotal) DocumentManager.countDocuments(rtype, query) else Future.apply(-1L) //If they don't need total number, just return -1 for it + total <- if (needTotal) DocumentManager.countDocuments(rtype, query, filteringStages) else Future.apply(-1L) //If they don't need total number, just return -1 for it resultResources <- DocumentManager - .searchDocuments(rtype, query, count, page, sortingPaths, elementsIncludedOrExcluded, excludeExtraFields) + .searchDocuments(rtype, query, filteringStages, count, page, sortingPaths, elementsIncludedOrExcluded, excludeExtraFields) .map(_.map(_.fromBson)) } yield total -> resultResources } @@ -276,21 +277,22 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = * @return */ def queryResourcesDirectlyWithOffsetPagination(rtype: String, - query: Option[Bson] = None, - count: Int = -1, - offset:Option[(Seq[String],Boolean)], - sortingFields: Seq[(String, Int, Seq[(String, String)])] = Seq.empty, - elementsIncludedOrExcluded: Option[(Boolean, Set[String])] = None, - excludeExtraFields: Boolean = false, - needTotal: Boolean = true)(implicit transactionSession: Option[TransactionSession] = None): Future[(Long, Seq[Resource], Seq[String], Seq[String])] = { + query: Option[Bson] = None, + filteringStages: Seq[Bson] = Nil, + count: Int = -1, + offset: Option[(Seq[String], Boolean)], + sortingFields: Seq[(String, Int, Seq[(String, String)])] = Seq.empty, + elementsIncludedOrExcluded: Option[(Boolean, Set[String])] = None, + excludeExtraFields: Boolean = false, + needTotal: Boolean = true)(implicit transactionSession: Option[TransactionSession] = None): Future[(Long, Seq[Resource], Seq[String], Seq[String])] = { val sortingPaths = constructFinalSortingPaths(sortingFields) for { - total <- if (needTotal) DocumentManager.countDocuments(rtype, query) else Future.apply(-1L) //If they don't need total number, just return -1 for it + total <- if (needTotal) DocumentManager.countDocuments(rtype, query, filteringStages) else Future.apply(-1L) //If they don't need total number, just return -1 for it resultResources <- DocumentManager - .searchDocumentsWithOffset(rtype, query, count, offset, sortingPaths, elementsIncludedOrExcluded, excludeExtraFields) + .searchDocumentsWithOffset(rtype, query, filteringStages, count, offset, sortingPaths, elementsIncludedOrExcluded, excludeExtraFields) .map { case (previousOffset, nextOffset, docs) => (previousOffset, nextOffset, docs.map(_.fromBson)) } @@ -299,27 +301,28 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = /** * Search FHIR resources from multiple resource types - * @param queries Queries for each resource type - * @param count Number of resources to return - * @param page Page to return - * @param sortingFields Sorting fields (param name, sorting direction, path and target data type) for each resource type - * @param elementsIncludedOrExcluded Elements to include or exclude for each resource type - * @param excludeExtraFields If true, extra fields are cleared - * @param needTotal If total number of results is needed at the response - * @param transactionSession If part of a transaction, the transaction session + * + * @param queries Queries for each resource type i.e. resourceType -> (Main filter, filtering aggregation phases) + * @param count Number of resources to return + * @param page Page to return + * @param sortingFields Sorting fields (param name, sorting direction, path and target data type) for each resource type + * @param elementsIncludedOrExcluded Elements to include or exclude for each resource type + * @param excludeExtraFields If true, extra fields are cleared + * @param needTotal If total number of results is needed at the response + * @param transactionSession If part of a transaction, the transaction session * @return */ - def queryResourcesDirectlyFromMultipleResourceTypes(queries:Map[String, Option[Bson]], - count:Int = -1, page:Int = 1, - sortingFields:Map[String, Seq[(String, Int, Seq[(String, String)])]], - elementsIncludedOrExcluded:Map[String, Option[(Boolean, Set[String])]], - excludeExtraFields:Boolean = false, - needTotal:Boolean = true - )(implicit transactionSession: Option[TransactionSession] = None):Future[(Long, Seq[Resource])] = { + def queryResourcesDirectlyFromMultipleResourceTypes(queries: Map[String, (Option[Bson], Seq[Bson])], + count: Int = -1, page: Int = 1, + sortingFields: Map[String, Seq[(String, Int, Seq[(String, String)])]], + elementsIncludedOrExcluded: Map[String, Option[(Boolean, Set[String])]], + excludeExtraFields: Boolean = false, + needTotal: Boolean = true + )(implicit transactionSession: Option[TransactionSession] = None): Future[(Long, Seq[Resource])] = { val sortingPaths = sortingFields.map(sf => sf._1 -> constructFinalSortingPaths(sf._2)) for { - total <- if(needTotal) DocumentManager.countDocumentsFromMultipleCollections(queries) else Future.apply(-1L) //If they don't need total number, just return -1 for it + total <- if (needTotal) DocumentManager.countDocumentsFromMultipleCollections(queries) else Future.apply(-1L) //If they don't need total number, just return -1 for it resultResources <- DocumentManager .searchDocumentsFromMultipleCollection(queries, count, page, sortingPaths, elementsIncludedOrExcluded, excludeExtraFields) @@ -340,26 +343,26 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = * - positive value means first e.g. 3 --> first 3 * @param excludeExtraFields If true, extra fields are cleared * @param transactionSession Session if this is part of a transaction - * @return Bucket keys and results for each group , all included or revincluded resources + * @return Bucket keys and results for each group , all included or revincluded resources */ - def searchLastOrFirstNResources(rtype:String, - parameters:List[Parameter] = List.empty, - sortingParams:List[String], - groupByParams:List[String], - lastOrFirstN:Int = -1, - excludeExtraFields:Boolean = false)(implicit transactionSession: Option[TransactionSession] = None):Future[(Seq[(Map[String, JValue], Seq[Resource])], Seq[Resource])] = { - - if(sortingParams.isEmpty || groupByParams.isEmpty) + def searchLastOrFirstNResources(rtype: String, + parameters: List[Parameter] = List.empty, + sortingParams: List[String], + groupByParams: List[String], + lastOrFirstN: Int = -1, + excludeExtraFields: Boolean = false)(implicit transactionSession: Option[TransactionSession] = None): Future[(Seq[(Map[String, JValue], Seq[Resource])], Seq[Resource])] = { + + if (sortingParams.isEmpty || groupByParams.isEmpty) throw new RuntimeException(s"Parameters sortingFields or groupByParams is empty! They are required for this method 'queryLastOrFirstNResources'") //Extract FHIR result parameters val resultParameters = parameters.filter(_.paramCategory == FHIR_PARAMETER_CATEGORIES.RESULT) //Check _summary param to identify what to include or exclude - val summaryIncludesOrExcludes = fhirResultParameterResolver.resolveSummaryParameter(rtype, resultParameters) + val summaryIncludesOrExcludes = fhirResultParameterResolver.resolveSummaryParameter(rtype, resultParameters) //Check _elements param to include further val elementsIncludes = fhirResultParameterResolver.resolveElementsParameter(resultParameters) //Decide on final includes and excludes - val finalIncludesOrExcludes = if(elementsIncludes.nonEmpty) Some(true -> elementsIncludes) else summaryIncludesOrExcludes + val finalIncludesOrExcludes = if (elementsIncludes.nonEmpty) Some(true -> elementsIncludes) else summaryIncludesOrExcludes //Find out include and revinclude params val includeParams = resultParameters.filter(_.name == FHIR_SEARCH_RESULT_PARAMETERS.INCLUDE) @@ -368,48 +371,45 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = //other result parameters are ignored as they are not relevant //Now others are query parameters - val queryParams = parameters.filter(_.paramCategory != FHIR_PARAMETER_CATEGORIES.RESULT) + val queryParams = parameters.filter(_.paramCategory != FHIR_PARAMETER_CATEGORIES.RESULT) //Run the search - constructQuery(rtype, queryParams).flatMap { - //If there is no query, although there are parameters - case None if queryParams.nonEmpty => Future.apply(Nil-> Nil) - //Otherwise process groupBy and sorting parameters and execute the query - case finalQuery => - //Find out paths for each sorting parameter - val finalSortingPaths:Seq[(String, Seq[String])] = - sortingParams - .map(sp => fhirConfig.findSupportedSearchParameter(rtype, sp) match { - case None => - throw new UnsupportedParameterException(s"Search parameter $sp is not supported for resource type $rtype, or you can not use it for sorting! Check conformance statement of server!") - case Some(spConf) => - spConf.pname -> - spConf - .extractElementPathsAndTargetTypes() - .flatMap { case (path, ttype) => - SORTING_SUBPATHS - .getOrElse(ttype, Seq("")) //Get the subpaths for sorting for the target element type - .map(subpath => path + subpath) - } - }) - - //Construct expressions for groupBy params - val groupByParamConfs = - groupByParams - .map(gbyp => - fhirConfig.findSupportedSearchParameter(rtype, gbyp) match { - case None => - throw new UnsupportedParameterException(s"Search parameter $gbyp is not supported for resource type $rtype, or you can not use it for grouping! Check conformance statement of server!") - case Some(gbypConf) => gbypConf - } - ) + val (finalQuery, filteringStages) = constructQueryNew(rtype, queryParams) + + //Find out paths for each sorting parameter + val finalSortingPaths: Seq[(String, Seq[String])] = + sortingParams + .map(sp => fhirConfig.findSupportedSearchParameter(rtype, sp) match { + case None => + throw new UnsupportedParameterException(s"Search parameter $sp is not supported for resource type $rtype, or you can not use it for sorting! Check conformance statement of server!") + case Some(spConf) => + spConf.pname -> + spConf + .extractElementPathsAndTargetTypes() + .flatMap { case (path, ttype) => + SORTING_SUBPATHS + .getOrElse(ttype, Seq("")) //Get the subpaths for sorting for the target element type + .map(subpath => path + subpath) + } + }) + + //Construct expressions for groupBy params + val groupByParamConfs = + groupByParams + .map(gbyp => + fhirConfig.findSupportedSearchParameter(rtype, gbyp) match { + case None => + throw new UnsupportedParameterException(s"Search parameter $gbyp is not supported for resource type $rtype, or you can not use it for grouping! Check conformance statement of server!") + case Some(gbypConf) => gbypConf + } + ) - val groupByExpressions = AggregationHandler.constructGroupByExpression(groupByParamConfs, parameters) + val groupByExpressions = AggregationHandler.constructGroupByExpression(groupByParamConfs, parameters) - //Execute the query and find the matched results - val fResults = - DocumentManager - .searchLastOrFirstNByAggregation(rtype, finalQuery, lastOrFirstN,finalSortingPaths, groupByExpressions) + //Execute the query and find the matched results + val fResults = + DocumentManager + .searchLastOrFirstNByAggregation(rtype, finalQuery, filteringStages, lastOrFirstN, finalSortingPaths, groupByExpressions, finalIncludesOrExcludes) .map(results => results.map(r => { val keys = @@ -420,37 +420,37 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = }) ) - fResults.flatMap(matchedResources => - //Handle _include and _revinclude params - (includeParams, revIncludeParams) match { - //No _include or _revinclude - case (Nil, Nil) => Future.apply(matchedResources, Seq.empty) - //Only _revinclude - case (Nil, _) => - handleRevIncludes(rtype, matchedResources.flatMap(_._2), revIncludeParams) - .map(revIncludedResources => (matchedResources, revIncludedResources)) - //Only _include - case (_, Nil) => - handleIncludes(rtype, matchedResources.flatMap(_._2), includeParams) - .map(includedResources => (matchedResources, includedResources)) - //Both - case (_, _) => - val allMatchedResources = matchedResources.flatMap(_._2) - for { - includedResources <- handleIncludes(rtype, allMatchedResources, includeParams) - revIncludedResources <- handleRevIncludes(rtype, allMatchedResources, revIncludeParams) - } yield (matchedResources, includedResources ++ revIncludedResources) - } - ) - } + fResults.flatMap(matchedResources => + //Handle _include and _revinclude params + (includeParams, revIncludeParams) match { + //No _include or _revinclude + case (Nil, Nil) => Future.apply(matchedResources, Seq.empty) + //Only _revinclude + case (Nil, _) => + handleRevIncludes(rtype, matchedResources.flatMap(_._2), revIncludeParams) + .map(revIncludedResources => (matchedResources, revIncludedResources)) + //Only _include + case (_, Nil) => + handleIncludes(rtype, matchedResources.flatMap(_._2), includeParams) + .map(includedResources => (matchedResources, includedResources)) + //Both + case (_, _) => + val allMatchedResources = matchedResources.flatMap(_._2) + for { + includedResources <- handleIncludes(rtype, allMatchedResources, includeParams) + revIncludedResources <- handleRevIncludes(rtype, allMatchedResources, revIncludeParams) + } yield (matchedResources, includedResources ++ revIncludedResources) + } + ) } /** - * Construct alternative paths according to FHIR type of the target element - * @param sortingFields Sorting fields (param name, sorting direction, path and target resource type - * @return - */ - private def constructFinalSortingPaths(sortingFields:Seq[(String, Int, Seq[(String, String)])]):Seq[(String, Int, Seq[String])] = { + * Construct alternative paths according to FHIR type of the target element + * + * @param sortingFields Sorting fields (param name, sorting direction, path and target resource type + * @return + */ + private def constructFinalSortingPaths(sortingFields: Seq[(String, Int, Seq[(String, String)])]): Seq[(String, Int, Seq[String])] = { sortingFields.map { case (pname, sorder, pathsAndTypes) => ( pname, @@ -466,97 +466,139 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = } /** - * Count the resources that matched the given query - * @param rtype Resource type - * @param queryParams Parsed FHIR query parameter - * @return - */ - def countResources(rtype:String, - queryParams:List[Parameter] = List.empty)(implicit transactionSession: Option[TransactionSession] = None):Future[Long] = { + * Count the resources that matched the given query + * + * @param rtype Resource type + * @param queryParams Parsed FHIR query parameter + * @return + */ + def countResources(rtype: String, + queryParams: List[Parameter] = List.empty)(implicit transactionSession: Option[TransactionSession] = None): Future[Long] = { //Construct query - constructQuery(rtype, queryParams).flatMap { - //If there is no query, although there are parameters - case None if queryParams.nonEmpty => Future.apply(0L) - //Otherwise run it - case finalQuery => DocumentManager.countDocuments(rtype, finalQuery) - } + val (query, filteringStages) = constructQueryNew(rtype, queryParams) + DocumentManager.countDocuments(rtype, query, filteringStages) } /** - * Construct the final Mongo query from FHIR parameters - * @param rtype Resource type - * @param queryParams Parsed FHIR query parameter - * @return None if the query fails (no matching resources) otherwise final Mongo query - */ - def constructQuery(rtype:String, queryParams:List[Parameter] = List.empty)(implicit transactionSession: Option[TransactionSession] = None):Future[Option[Bson]] = { - //Handle Special params - val specialParamQueries:Future[Seq[Option[Bson]]] = handleSpecialParams(rtype, queryParams.filter(_.paramCategory == FHIR_PARAMETER_CATEGORIES.SPECIAL)) + * Construct the final Mongo query and aggregation filtering stages from FHIR parameters + * + * @param rtype Resource type + * @param queryParams Parsed FHIR query parameter + * @return + */ + def constructQueryNew(rtype: String, queryParams: List[Parameter] = List.empty): (Option[Bson], Seq[Bson]) = { + //Handle Special params TODO + val specialParamQueries: (Seq[Bson], Seq[Bson]) = getQueryAndFilteringPhasesForSpecialParams(rtype, queryParams.filter(_.paramCategory == FHIR_PARAMETER_CATEGORIES.SPECIAL)) //Handle Chained Params - val chainedParamQueries:Future[Seq[Option[Bson]]] = Future.sequence( + val chainedParamQueries: Seq[Bson] = queryParams .filter(_.paramCategory == FHIR_PARAMETER_CATEGORIES.CHAINED) - .map(p => handleChainParam(rtype, p)) - ) + .flatMap(p => getChainParamStages(rtype, p)) + //Handle Reverse Chained Params - val revchainedParamQueries:Future[Seq[Option[Bson]]] = Future.sequence( + val revchainedParamQueries: Seq[Bson] = queryParams .filter(_.paramCategory == FHIR_PARAMETER_CATEGORIES.REVCHAINED) - .map(p => handleReverseChainParam(rtype, p)) - ) - - //Merge all special query handlings - val fotherQueries = - for { - r1 <- specialParamQueries - r2 <- chainedParamQueries - r3 <- revchainedParamQueries - } yield r1 ++ r2 ++ r3 + .flatMap(p => getReverseChainParamStages(rtype, p)) //Get valid query parameters for the resource type val validQueryParams = fhirConfig.getSupportedParameters(rtype) - //Construct final query by anding all of them - fotherQueries.map( otherQueries => { - //If there are other queries, but some empty it means rejection - if(otherQueries.exists(_.isEmpty)) - None - else { - val normalQueries = queryParams - .filter(p => p.paramCategory == FHIR_PARAMETER_CATEGORIES.NORMAL || p.paramCategory == FHIR_PARAMETER_CATEGORIES.COMPARTMENT) - .map(p => { - p.paramCategory match { - case FHIR_PARAMETER_CATEGORIES.NORMAL => - ResourceQueryBuilder.constructQueryForNormal(p, validQueryParams.apply(p.name), validQueryParams) - case FHIR_PARAMETER_CATEGORIES.COMPARTMENT => - ResourceQueryBuilder.constructQueryForCompartment(rtype, p, validQueryParams) - } - }) - DocumentManager.andQueries(normalQueries ++ otherQueries.flatten) - } - }) + + val normalQueries = + queryParams + .filter(p => p.paramCategory == FHIR_PARAMETER_CATEGORIES.NORMAL || p.paramCategory == FHIR_PARAMETER_CATEGORIES.COMPARTMENT) + .map(p => { + p.paramCategory match { + case FHIR_PARAMETER_CATEGORIES.NORMAL => + getResourceQueryBuilder(rtype).constructQueryForNormal(p, validQueryParams.apply(p.name), validQueryParams) + case FHIR_PARAMETER_CATEGORIES.COMPARTMENT => + getResourceQueryBuilder(rtype).constructQueryForCompartment(rtype, p, validQueryParams) + } + }) + //Merge all the queries and return + val finalMainQuery = DocumentManager.andQueries(specialParamQueries._1 ++ normalQueries) + val filteringStages = revchainedParamQueries ++ chainedParamQueries ++ specialParamQueries._2 + + finalMainQuery -> filteringStages } + /* + /** + * Construct the final Mongo query from FHIR parameters + * + * @param rtype Resource type + * @param queryParams Parsed FHIR query parameter + * @return None if the query fails (no matching resources) otherwise final Mongo query + */ + def constructQuery(rtype: String, queryParams: List[Parameter] = List.empty)(implicit transactionSession: Option[TransactionSession] = None): Future[Option[Bson]] = { + //Handle Special params + val specialParamQueries: Future[Seq[Option[Bson]]] = handleSpecialParams(rtype, queryParams.filter(_.paramCategory == FHIR_PARAMETER_CATEGORIES.SPECIAL)) + //Handle Chained Params + val chainedParamQueries: Future[Seq[Option[Bson]]] = Future.sequence( + queryParams + .filter(_.paramCategory == FHIR_PARAMETER_CATEGORIES.CHAINED) + .map(p => handleChainParam(rtype, p)) + ) + //Handle Reverse Chained Params + val revchainedParamQueries: Future[Seq[Option[Bson]]] = Future.sequence( + queryParams + .filter(_.paramCategory == FHIR_PARAMETER_CATEGORIES.REVCHAINED) + .map(p => handleReverseChainParam(rtype, p)) + ) + + //Merge all special query handlings + val fotherQueries = + for { + r1 <- specialParamQueries + r2 <- chainedParamQueries + r3 <- revchainedParamQueries + } yield r1 ++ r2 ++ r3 + + //Get valid query parameters for the resource type + val validQueryParams = fhirConfig.getSupportedParameters(rtype) + //Construct final query by anding all of them + fotherQueries.map(otherQueries => { + //If there are other queries, but some empty it means rejection + if (otherQueries.exists(_.isEmpty)) + None + else { + val normalQueries = queryParams + .filter(p => p.paramCategory == FHIR_PARAMETER_CATEGORIES.NORMAL || p.paramCategory == FHIR_PARAMETER_CATEGORIES.COMPARTMENT) + .map(p => { + p.paramCategory match { + case FHIR_PARAMETER_CATEGORIES.NORMAL => + ResourceQueryBuilder.constructQueryForNormal(p, validQueryParams.apply(p.name), validQueryParams) + case FHIR_PARAMETER_CATEGORIES.COMPARTMENT => + ResourceQueryBuilder.constructQueryForCompartment(rtype, p, validQueryParams) + } + }) + DocumentManager.andQueries(normalQueries ++ otherQueries.flatten) + } + }) + }*/ /** - * Handle FHIR _revinclude to return revinclude resources - * @param rtype Resource Type - * @param matchedResources Matched Resources - * @param revIncludeParams Parsed FHIR _revinclude parameters - * @return Linked resources - */ - private def handleRevIncludes(rtype:String, matchedResources:Seq[Resource], revIncludeParams:List[Parameter])(implicit transactionSession: Option[TransactionSession] = None):Future[Seq[Resource]] = { + * Handle FHIR _revinclude to return revinclude resources + * + * @param rtype Resource Type + * @param matchedResources Matched Resources + * @param revIncludeParams Parsed FHIR _revinclude parameters + * @return Linked resources + */ + private def handleRevIncludes(rtype: String, matchedResources: Seq[Resource], revIncludeParams: List[Parameter])(implicit transactionSession: Option[TransactionSession] = None): Future[Seq[Resource]] = { //TODO handle :iterate //Reference to matched resources and optionally their canonical urls and version - val matchedResourceReferences:Seq[(String, Option[String], Option[String])] = - matchedResources.map(mr => - ( - rtype + "/"+FHIRUtil.extractIdFromResource(mr), - FHIRUtil.extractValueOption[String](mr, FHIR_COMMON_FIELDS.URL), - FHIRUtil.extractValueOption[String](mr, FHIR_COMMON_FIELDS.VERSION) - ) - ) - Future.sequence( { - val linkedResourcesAndParams:Seq[(String, String)] = + val matchedResourceReferences: Seq[(String, Option[String], Option[String])] = + matchedResources.map(mr => + ( + rtype + "/" + FHIRUtil.extractIdFromResource(mr), + FHIRUtil.extractValueOption[String](mr, FHIR_COMMON_FIELDS.URL), + FHIRUtil.extractValueOption[String](mr, FHIR_COMMON_FIELDS.VERSION) + ) + ) + Future.sequence({ + val linkedResourcesAndParams: Seq[(String, String)] = revIncludeParams - .flatMap( p => p.valuePrefixList.head match { + .flatMap(p => p.valuePrefixList.head match { case (linkedResourceType, "*") => fhirConfig.resourceQueryParameters .get(linkedResourceType).map(_.toSeq).getOrElse(Nil) @@ -569,23 +611,24 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = linkedResourcesAndParams.map { case (linkedResourceType, linkParam) => val searchParameterConf = fhirConfig.findSupportedSearchParameter(linkedResourceType, linkParam).get - val query = ResourceQueryBuilder.constructQueryForRevInclude(matchedResourceReferences, searchParameterConf) + val query = getResourceQueryBuilder(linkedResourceType).constructQueryForRevInclude(matchedResourceReferences, searchParameterConf) DocumentManager - .searchDocuments(linkedResourceType, Some(query)) + .searchDocuments(linkedResourceType, Some(query), Nil) .map(_.map(_.fromBson)) } - } ).map(_.flatten) + }).map(_.flatten) } /** - * Handle FHIR _include to return to be included resources - * @param rtype Resource Type - * @param matchedResources Matched Resources - * @param includeParams parsed _include params - * @return Included resources - */ - private def handleIncludes(rtype:String, matchedResources:Seq[Resource], includeParams:List[Parameter])(implicit transactionSession: Option[TransactionSession] = None):Future[Seq[Resource]] = { + * Handle FHIR _include to return to be included resources + * + * @param rtype Resource Type + * @param matchedResources Matched Resources + * @param includeParams parsed _include params + * @return Included resources + */ + private def handleIncludes(rtype: String, matchedResources: Seq[Resource], includeParams: List[Parameter])(implicit transactionSession: Option[TransactionSession] = None): Future[Seq[Resource]] = { val matchedResourceMap = Map(rtype -> matchedResources .map(mr => extractIdVersionIdAndCanonicalUrl(mr)) @@ -596,19 +639,20 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = } /** - * Recuresivly handle _include iterates - * @param rtype Resource type - * @param allResources All resources compiled until now (Resource type, resource id, canonical url) - * @param newResources new resources returned at the last iteration - * @param includeParams Include parameters - * @return - */ - private def executeIncludeIteration(rtype:String, allResources:Map[String,Seq[(String, Long, Option[String])]], newResources:Seq[Resource], includeParams:List[Parameter])(implicit transactionSession: Option[TransactionSession] = None):Future[Seq[Resource]] = { + * Recuresivly handle _include iterates + * + * @param rtype Resource type + * @param allResources All resources compiled until now (Resource type, resource id, canonical url) + * @param newResources new resources returned at the last iteration + * @param includeParams Include parameters + * @return + */ + private def executeIncludeIteration(rtype: String, allResources: Map[String, Seq[(String, Long, Option[String])]], newResources: Seq[Resource], includeParams: List[Parameter])(implicit transactionSession: Option[TransactionSession] = None): Future[Seq[Resource]] = { val newResourceMap = - newResources + newResources .map(mr => (rtype -> FHIRUtil.extractIdFromResource(mr)) -> mr).toMap //First execute single iteration includes (include without :iterate - val includes:Map[String, Set[FhirReference]] = //ResourceType -> References to include + val includes: Map[String, Set[FhirReference]] = //ResourceType -> References to include includeParams .filter(_.valuePrefixList.head._1 == rtype) //Filter related include params .flatMap(p => findIncludeResources(allResources, newResourceMap, p)) @@ -616,10 +660,10 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = .map(g => g._1 -> g._2.map(_._2).toSet) //If there is nothing to include, return - if(includes.isEmpty) + if (includes.isEmpty) return Future.apply(Seq.empty) //Retrieve the resources - val fincludedResources:Future[Seq[(String, Seq[Resource])]] = + val fincludedResources: Future[Seq[(String, Seq[Resource])]] = Future.sequence( includes.map { case (rtype, includeSet) => @@ -632,11 +676,11 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = val iteratedIncludeParams = includeParams.filter(_.suffix == ":iterate") fincludedResources flatMap (includedResources => { //If there is no include, or no parameter with iterate - if(includedResources.isEmpty || iteratedIncludeParams.isEmpty) + if (includedResources.isEmpty || iteratedIncludeParams.isEmpty) Future.apply(includedResources.flatMap(_._2)) else { val newIncludedResources = includedResources.map(ir => ir._1 -> ir._2.map(r => extractIdVersionIdAndCanonicalUrl(r))).toMap - val newAllResources = mergeAllResources(allResources,newIncludedResources) + val newAllResources = mergeAllResources(allResources, newIncludedResources) Future.sequence( //Run iteration recursively for each ResourceType, run iterations only on new resources includedResources.map(ir => executeIncludeIteration(ir._1, newAllResources, ir._2, iteratedIncludeParams)) @@ -647,11 +691,12 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = /** * Merge resources - * @param allResources All resources until now (resource type -> Seq(rid, version, canonical url)) - * @param newResources New resources included + * + * @param allResources All resources until now (resource type -> Seq(rid, version, canonical url)) + * @param newResources New resources included * @return */ - private def mergeAllResources(allResources:Map[String,Seq[(String, Long, Option[String])]], newResources:Map[String,Seq[(String, Long, Option[String])]]) = { + private def mergeAllResources(allResources: Map[String, Seq[(String, Long, Option[String])]], newResources: Map[String, Seq[(String, Long, Option[String])]]) = { (allResources.toSeq ++ newResources.toSeq) .groupMap(_._1)(_._2) .map(g => g._1 -> g._2.flatten) @@ -660,20 +705,22 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = /** * Extract id, version id and optional url of the resource - * @param mr Matched/included resource + * + * @param mr Matched/included resource * @return */ - private def extractIdVersionIdAndCanonicalUrl(mr:Resource) = + private def extractIdVersionIdAndCanonicalUrl(mr: Resource) = (FHIRUtil.extractIdFromResource(mr), FHIRUtil.extractVersionFromResource(mr), FHIRUtil.extractValueOption[String](mr, FHIR_COMMON_FIELDS.URL)) /** * Search resources with given ids or canonical urls - * @param rtype Resource type - * @param includeSet Set of ids and/or canonical urls + * + * @param rtype Resource type + * @param includeSet Set of ids and/or canonical urls * @return */ - private def searchForIncludeSet(rtype:String, includeSet:Set[FhirReference]):Future[Seq[Resource]] = { + private def searchForIncludeSet(rtype: String, includeSet: Set[FhirReference]): Future[Seq[Resource]] = { var canonicalReferences = includeSet .filter(_.isInstanceOf[FhirCanonicalReference]) @@ -686,24 +733,24 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = canonicalReferences = canonicalReferences.filterNot(_.url == "") val resolvedCanonicalRefs = - if(canonicalReferences.nonEmpty) + if (canonicalReferences.nonEmpty) resolveCanonicalReferences(rtype, canonicalReferences) else Future.apply(Nil) val resolvedLiteralrefs = - if(literalReferences.nonEmpty) + if (literalReferences.nonEmpty) resolveLiteralReferences(rtype, literalReferences) else Future.apply(Nil) - for{ - nrefs <- resolvedCanonicalRefs - crefs <- resolvedLiteralrefs + for { + nrefs <- resolvedCanonicalRefs + crefs <- resolvedLiteralrefs } yield getDistinctResources(nrefs ++ crefs) } - def getDistinctResources(resources:Seq[Resource]):Seq[Resource] = { + def getDistinctResources(resources: Seq[Resource]): Seq[Resource] = { resources .map(r => (FHIRUtil.extractIdFromResource(r), FHIRUtil.extractVersionFromResource(r)) -> r) .groupBy(_._1) @@ -712,13 +759,18 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = /** * Search with given canonical urls - * @param rtype Resource type to search e.g. QuestionnaireResponse - * @param canonicalRefs Canonical urls to search on url and version e.g. http://example.com/Questionnaire/cgs|1.0 + * + * @param rtype Resource type to search e.g. QuestionnaireResponse + * @param canonicalRefs Canonical urls to search on url and version e.g. http://example.com/Questionnaire/cgs|1.0 * @return */ + private def resolveCanonicalReferences(rtype:String, canonicalRefs:Set[FhirCanonicalReference]):Future[Seq[Resource]] = { - val canonicalQuery = SearchUtil.canonicalRefQuery(canonicalRefs.map(cr => cr.getUrl() -> cr.version).toSeq) - DocumentManager.searchDocuments(rtype, Some(canonicalQuery)) + val canonicalQuery = + ReferenceQueryBuilder.getQueryOnCanonicalRefs(canonicalRefs.map(cr => cr.getUrl() -> cr.version).toSeq) + + DocumentManager + .searchDocuments(rtype, Some(canonicalQuery), Nil) .map(_.map(_.fromBson)) .map(rs => //Get the latest version of resource for each url (this is required when no version is given in canonical URL) @@ -732,45 +784,47 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = /** * Resolve literal references all together for given resource type - * @param rtype Resource type - * @param literalRefs Referred resources + * + * @param rtype Resource type + * @param literalRefs Referred resources * @return */ - private def resolveLiteralReferences(rtype:String, literalRefs:Set[FhirLiteralReference]):Future[Seq[Resource]] = { + private def resolveLiteralReferences(rtype: String, literalRefs: Set[FhirLiteralReference]): Future[Seq[Resource]] = { val literalReferencesWithVersion = literalRefs.filter(_.version.isDefined) val literalReferencesWithoutVersion = literalRefs.filter(_.version.isEmpty) val resolvedResourcesWithoutVersion = - if(literalReferencesWithoutVersion.nonEmpty) + if (literalReferencesWithoutVersion.nonEmpty) getResourcesWithIds(rtype, literalReferencesWithoutVersion.map(lr => lr.rid)) else Future.apply(Nil) val resolvedResourcesWithVersion = - if(literalReferencesWithVersion.nonEmpty) + if (literalReferencesWithVersion.nonEmpty) Future.sequence( literalReferencesWithVersion.toSeq.map(lr => getResource(rtype, lr.rid, lr.version)) ).map(_.flatten) else Future.apply(Nil) - for{ - nrefs <- resolvedResourcesWithoutVersion - crefs <- resolvedResourcesWithVersion + for { + nrefs <- resolvedResourcesWithoutVersion + crefs <- resolvedResourcesWithVersion } yield nrefs ++ crefs } /** - * Find to be included resources based on the matched resources and the _include parameter - * @param allResources All resources included until now (Resource Type, Resource id, Version Id, Canonical Url) - * @param matchedResources Matched Resources (Resource Type, Rid) -> Resource content - * @param parameter Parsed _include parameter - * @return Set of included resources as ResourceType and Rid - */ - private def findIncludeResources(allResources:Map[String,Seq[(String, Long, Option[String])]], matchedResources:Map[(String, String), Resource], parameter: Parameter):Set[(String, FhirReference)] = { + * Find to be included resources based on the matched resources and the _include parameter + * + * @param allResources All resources included until now (Resource Type, Resource id, Version Id, Canonical Url) + * @param matchedResources Matched Resources (Resource Type, Rid) -> Resource content + * @param parameter Parsed _include parameter + * @return Set of included resources as ResourceType and Rid + */ + private def findIncludeResources(allResources: Map[String, Seq[(String, Long, Option[String])]], matchedResources: Map[(String, String), Resource], parameter: Parameter): Set[(String, FhirReference)] = { //Target resource type to include - val targetResourceType = if(parameter.paramType == "") None else Some(parameter.paramType) + val targetResourceType = if (parameter.paramType == "") None else Some(parameter.paramType) //For all inclusions in this parameter parameter.valuePrefixList.flatMap { @@ -778,7 +832,7 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = //Find parameter configuration val refParamConf = fhirConfig.findSupportedSearchParameter(includeResourceType, includeParam) //Find the path to the FHIR Reference elements and its target type (either Reference or canonical) - val refPathAndTargetType:Option[(String,String)] = + val refPathAndTargetType: Option[(String, String)] = refParamConf.flatMap(pconf => { if (targetResourceType.forall(t => pconf.targets.contains(t) || pconf.targets.contains(FHIR_DATA_TYPES.RESOURCE))) Some(pconf.extractElementPaths().head -> pconf.targetTypes.head) @@ -786,7 +840,7 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = None }) - (refPathAndTargetType : @unchecked) match { + (refPathAndTargetType: @unchecked) match { case None => Nil //If target type is a reference case Some((refPath, FHIR_DATA_TYPES.REFERENCE)) => @@ -795,11 +849,11 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = matchedResources .filter(_._1._1 == includeResourceType) //Only evaluate the resources with the specified type .flatMap(mresource => - FHIRUtil - .applySearchParameterPath(refPath, mresource._2)//Extract Reference values - .map(FHIRUtil.parseReference) //Parse the literal or contained resource reference - .filter(_.isInstanceOf[FhirLiteralReference]) - .map(_.asInstanceOf[FhirLiteralReference]) + FHIRUtil + .applySearchParameterPath(refPath, mresource._2) //Extract Reference values + .map(FHIRUtil.parseReference) //Parse the literal or contained resource reference + .filter(_.isInstanceOf[FhirLiteralReference]) + .map(_.asInstanceOf[FhirLiteralReference]) ) .filter(flr => flr.url.forall(_ == OnfhirConfig.fhirRootUrl)) //Only get the ones that are persistent inside our repository for now .map(_.copy(url = None)) //clear url so that when converted to Set we really have unique references @@ -810,7 +864,7 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = resourcesToInclude //If target type of search parameter is canonical - case Some((refPath,FHIR_DATA_TYPES.CANONICAL)) => + case Some((refPath, FHIR_DATA_TYPES.CANONICAL)) => //Extract type and urls val resourcesToInclude = matchedResources @@ -820,14 +874,14 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = .applySearchParameterPath(refPath, mresource._2) .map(v => FHIRUtil.parseCanonicalRef(v)) .filter { - case cr:FhirCanonicalReference => targetResourceType.forall(_ == cr.rtype) + case cr: FhirCanonicalReference => targetResourceType.forall(_ == cr.rtype) } .filterNot { - case cr:FhirCanonicalReference => allResources.getOrElse(cr.rtype, Nil).flatMap(_._3).contains(cr.getUrl()) + case cr: FhirCanonicalReference => allResources.getOrElse(cr.rtype, Nil).flatMap(_._3).contains(cr.getUrl()) } ) .map { - case cr:FhirCanonicalReference => cr.rtype -> cr + case cr: FhirCanonicalReference => cr.rtype -> cr } .toSet @@ -835,8 +889,7 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = } }.toSet } - - +/* /** * Handle special parameters * @param rtype Resource Type @@ -848,7 +901,7 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = specialParameters.map(p => p.name match { //Search with ids case FHIR_SEARCH_SPECIAL_PARAMETERS.ID => - Future.apply(Some(ResourceQueryBuilder.constructQueryForIds(p))) + Future.apply(Some(getResourceQueryBuilder(rtype).constructQueryForIds(p))) //FHIR _list query case FHIR_SEARCH_SPECIAL_PARAMETERS.LIST => handleListSearch(rtype, p) @@ -867,12 +920,12 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = private def handleListSearch(rtype:String, parameter: Parameter)(implicit transactionSession: Option[TransactionSession] = None):Future[Option[Bson]] = { val listId = parameter.valuePrefixList.map(_._2).head listId match { - case currentInd if currentInd.startsWith("$") => throw new UnsupportedParameterException("Parameter _list is not supported for $current-* like queries!") + case currentInd if currentInd.startsWith("$") => throw new UnsupportedParameterException("Parameter _list is not supported for $current-* like queries!") case lid => //Try to retrieve the list DocumentManager .getDocument("List", lid, includingOrExcludingFields = Some(true -> Set("entry.item"))) //Retrieve the List document with given id (only item reference elements) - .map(_.map( _.fromBson)) + .map(_.map(_.fromBson)) .map(r => r.map(FHIRUtil.extractReferences("entry.item", _))) //extract the references .map(_.map(_.map(FHIRUtil.parseReferenceValue))) //Parse the references .map(_.map(parsedRefs => @@ -884,132 +937,475 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = )) .map(_.map(DocumentManager.ridsQuery)) // Construct the query } + }*/ + /* + * Construct query and aggregation filtering phases + * + * @param rtype + * @param specialParameters + * @return + */ + private def getQueryAndFilteringPhasesForSpecialParams(rtype: String, specialParameters: List[Parameter]): (Seq[Bson], Seq[Bson]) = { + if(specialParameters.isEmpty) + Nil -> Nil + else { + specialParameters + .map(p => p.name match { + //Search with ids + case FHIR_SEARCH_SPECIAL_PARAMETERS.ID => + Seq(getResourceQueryBuilder(rtype).constructQueryForIds(p)) -> Nil + //FHIR _list query + case FHIR_SEARCH_SPECIAL_PARAMETERS.LIST => + Nil -> getFilteringPhasesForListSearch(rtype, p) + case FHIR_SEARCH_SPECIAL_PARAMETERS.TEXT | FHIR_SEARCH_SPECIAL_PARAMETERS.CONTENT | FHIR_SEARCH_SPECIAL_PARAMETERS.FILTER => + throw new UnsupportedParameterException(s"Parameter ${p.name} is not supported in onFhir.io!") + }) + .reduce((s1, s2) => (s1._1 ++ s2._1) -> (s1._2 ++ s2._2)) + } } /** - * Handle FHIR _has search (Reverse chain) - * @param rtype Resource Type to search - * @param parameter Parsed parameter - * @return - */ - private def handleReverseChainParam(rtype:String, parameter: Parameter)(implicit transactionSession: Option[TransactionSession] = None):Future[Option[Bson]] = { + * Return the Mongodb Aggregation pipeline phases to handle _list search + * + * @param rtype Resource type we are searching on + * @param parameter Parsed parameter expression + * @return + */ + def getFilteringPhasesForListSearch(rtype: String, parameter: Parameter): Seq[Bson] = { + val listIds = parameter.valuePrefixList.map(_._2) + if (listIds.exists(_.startsWith("$"))) + throw new UnsupportedParameterException("Parameter _list is not supported for $current-* like queries!") + + val lookupPhase = + AggregationUtil + .constructLookupPhaseExpression( + col = "List", + localFieldPath = "id", + foreignFieldPath = "entry.item.reference.__rid", //Match id of resource with the List.entry.item.reference + letVariablePathMap = Map("rid" -> "id"), //We will use these again for comparison + pipeline = + Seq( + Aggregates.`match`(Filters.in("id", listIds: _*)).toBsonDocument, //Only with given List(s) + Aggregates.unwind("$entry").toBsonDocument, //Unwind the entry array + Aggregates.`match`( + Filters.expr(Filters.and( + AggregationUtil.constructAggEqual(BsonString("$entry.item.reference.__rtype"), BsonString(rtype)), + AggregationUtil.constructAggEqual(BsonString("$entry.item.reference.__rid"), BsonString("$$rid")), + AggregationUtil.constructAggNotEqual(BsonString("$entry.deleted"), BsonBoolean(true)) + ))).toBsonDocument, + Aggregates.count("count").toBsonDocument //Count the matched items + ), + as = "_list" + ) + //Filter the ones that has at least one matching + val checkPhase = Aggregates.filter(Filters.gt("_list.count", 0)) + val deletionOfExtraField = Aggregates.project(Projections.exclude("_list")) + Seq(lookupPhase, checkPhase, deletionOfExtraField) + } + /* + /** + * Handle special parameters + * + * @param rtype Resource Type + * @param specialParameters Parsed FHIR special parameters + * @return + */ + private def handleSpecialParams(rtype: String, specialParameters: List[Parameter])(implicit transactionSession: Option[TransactionSession] = None): Future[Seq[Option[Bson]]] = { + Future.sequence( + specialParameters.map(p => p.name match { + //Search with ids + case FHIR_SEARCH_SPECIAL_PARAMETERS.ID => + Future.apply(Some(ResourceQueryBuilder.constructQueryForIds(p))) + //FHIR _list query + case FHIR_SEARCH_SPECIAL_PARAMETERS.LIST => + handleListSearch(rtype, p) + case FHIR_SEARCH_SPECIAL_PARAMETERS.TEXT | FHIR_SEARCH_SPECIAL_PARAMETERS.CONTENT | FHIR_SEARCH_SPECIAL_PARAMETERS.FILTER => + throw new UnsupportedParameterException(s"Parameter ${p.name} is not supported in onFhir.io!") + }) + ) + } + + /** + * FHIR _list search + * + * @param rtype Resource Type + * @param parameter Parsed parameter + * @return + */ + private def handleListSearch(rtype: String, parameter: Parameter)(implicit transactionSession: Option[TransactionSession] = None): Future[Option[Bson]] = { + val listId = parameter.valuePrefixList.map(_._2).head + listId match { + case currentInd if currentInd.startsWith("$") => throw new UnsupportedParameterException("Parameter _list is not supported for $current-* like queries!") + case lid => + //Try to retrieve the list + DocumentManager + .getDocument("List", lid, includingOrExcludingFields = Some(true -> Set("entry.item"))) //Retrieve the List document with given id (only item reference elements) + .map(_.map(_.fromBson)) + .map(r => r.map(FHIRUtil.extractReferences("entry.item", _))) //extract the references + .map(_.map(_.map(FHIRUtil.parseReferenceValue))) //Parse the references + .map(_.map(parsedRefs => + parsedRefs + .filter(_._1.forall(_ == OnfhirConfig.fhirRootUrl)) //Only the references in our server + .filter(_._2 == rtype) //Only the ones refering the given resource type) + .map(_._3) //Get resource id + .toSet + )) + .map(_.map(DocumentManager.ridsQuery)) // Construct the query + } + }*/ + + /** + * Construct the Mongodb filtering aggregation phases for reverse chain param + * + * @param rtype Resource type that we are searching on + * @param parameter Parsed parameter + * @return + */ + private def getReverseChainParamStages(rtype: String, parameter: Parameter): Seq[Bson] = { //Get the resource type to query (the end of revchain) + //e.g. Patient?_has:Observation:patient:code=... //e.g. Patient?_has:Observation:patient:_has:AuditEvent:entity:agent=MyUserId + //e.g. Practitioner?_has:Patient:general-practitioner:_has:Observation:patient:code=http://loinc.org|15074-8 val rtypeToQuery = parameter.chain.last._1 val searchParameterConf = fhirConfig.findSupportedSearchParameter(rtypeToQuery, parameter.name) - if(searchParameterConf.isEmpty) + if (searchParameterConf.isEmpty) throw new UnsupportedParameterException(s"Parameter ${parameter.name} is not supported on $rtypeToQuery within '_has' query!") //Construct the Query on the leaf of chain to find the resource references - val query = ResourceQueryBuilder.constructQueryForNormal(parameter, searchParameterConf.get, fhirConfig.getSupportedParameters(rtypeToQuery)) + val query = getResourceQueryBuilder(rtypeToQuery).constructQueryForNormal(parameter, searchParameterConf.get, fhirConfig.getSupportedParameters(rtypeToQuery)) //Find the path of reference val chainParameterConf = fhirConfig.findSupportedSearchParameter(rtypeToQuery, parameter.chain.last._2) if(chainParameterConf.isEmpty) throw new UnsupportedParameterException(s"Parameter ${parameter.chain.last._2} is not supported on $rtypeToQuery within '_has' query!") - //Find the paths of reference element to return - val referencePaths = chainParameterConf.get.extractElementPaths().toSet - //Run query but only return the references on chain parameter - var fresourceReferences = DocumentManager - .searchDocuments(rtypeToQuery, Some(query), includingOrExcludingFields = Some(true -> referencePaths)) - .map(_.map(_.fromBson)) - .map( - _.flatMap(r => - referencePaths.flatMap(rpath => FHIRUtil.extractReferences(rpath, r)) - ) - ) - - // Come from deep in revchain by evaluating from right - fresourceReferences = + var previousPrefix: Option[String] = None + val chainLookupPhases = parameter.chain - .dropRight(1) // We already evaluate the last one - .foldRight(fresourceReferences)(findResourceReferencesInResources) - - fresourceReferences.map { - case Nil => None //No such resource - case references => - val rids = references - .map(FHIRUtil.parseReferenceValue) //Parse the reference value - .filter(_._1.forall(_ == OnfhirConfig.fhirRootUrl)) //Only the references in our server - .filter(_._2 == rtype) //Only the ones refering the given resource type - .map(_._3) - .toSet - //Return the query - Some(DocumentManager.ridsQuery(rids)) - } - } + .dropRight(1) + .map { + //e.g. (Patient, general-practitioner) + case (c1rtype, c1param) => + val chainedSearchParamConf = fhirConfig.findSupportedSearchParameter(c1rtype, c1param) + if (chainedSearchParamConf.isEmpty || chainedSearchParamConf.get.ptype != FHIR_PARAMETER_TYPES.REFERENCE) + throw new UnsupportedParameterException(s"Parameter $c1param is not supported on $c1rtype or cannot be used for chaining!") + //Get chained reference path for resource identifier that is chained + // e.g. Patient --> generalPractitioner.reference.__rid + val chainedPath = chainedSearchParamConf.get.extractElementPaths().head + ".reference.__rid" + val idPath = previousPrefix.map(p => p + ".").getOrElse("") + "id" + //Set previous resource type and prefix + previousPrefix = Some(s"${c1rtype}_$c1param") + //Construct the $lookup expression for chaining + //e.g. { $lookup: { + // from: "Patient", + // localField: "id", + // foreignField: "generalPractitioner.reference.__rid", + // pipeline: [ + // { + // $project: {"id": 1} + // } + // ], + // as: "Patient_general-practitioner", + //}} + AggregationUtil + .constructLookupPhaseExpression(c1rtype, idPath, chainedPath, Seq(Aggregates.project(Projections.include("id")).toBsonDocument), s"${c1rtype}_$c1param") + } + //Leaf lookup e.g. (Observation, patient) + val (crtype, cparam) = parameter.chain.last + val chainedSearchParamConf = fhirConfig.findSupportedSearchParameter(crtype, cparam) + if(chainedSearchParamConf.isEmpty) + throw new UnsupportedParameterException(s"Parameter $cparam is not supported on $crtype or cannot be used for reverse chaining!") + val chainedPath = chainedSearchParamConf.get.extractElementPaths().head + ".reference.__rid" + val idPath = previousPrefix.map(p => p + ".").getOrElse("") + "id" + //Construct last lookup phase for leaf + //e.g.{ + // from: "Observation", + // localField: "Patient_general-practitioner.id", + // foreignField: "subject.reference.__rid", + // pipeline: [ + // { + // $match: { code.coding.code: "..." }, + // }, + // { + // $project: { _id: 1 }, + // }, + // ], + // as: "Observation_patient", + //} + val leafLookupPhases = Seq( + AggregationUtil + .constructLookupPhaseExpression(crtype, idPath, chainedPath, Seq(Aggregates.`match`(query).toBsonDocument, Aggregates.project(Projections.include("_id")).toBsonDocument), s"${crtype}_$cparam"), + Aggregates.filter(AggregationUtil.constructGreaterThanSizeExpression(s"${crtype}_$cparam", 0)) //At least one resource should satisfy + ) - /*** - * Within the given resources (by references) find resource references within the given parameter - * @param rtypeAndChainParamName Resource Type and Chain Parameter Name - * @param freferences References to the resources that search will be on - * @return - */ - private def findResourceReferencesInResources(rtypeAndChainParamName:(String,String), freferences:Future[Seq[String]])(implicit transactionSession: Option[TransactionSession] = None):Future[Seq[String]] = { - freferences.flatMap(references => { - val rids = - references - .map(FHIRUtil.parseReferenceValue) //Parse the reference value - .filter(_._1.forall(_ == OnfhirConfig.fhirRootUrl)) //Only the references in our server - .filter(_._2 == rtypeAndChainParamName._1) //Only the ones refering the given resource type - .map(_._3) - .toSet + val chainPhases: Seq[Bson] = + chainLookupPhases ++ leafLookupPhases :+ + Aggregates.project(Projections.exclude(parameter.chain.map(c => s"${c._1}_${c._2}"): _*)) //Exclude the added fields in lookup phases + + chainPhases + } + /* + /** + * Handle FHIR _has search (Reverse chain) + * + * @param rtype Resource Type to search + * @param parameter Parsed parameter + * @return + */ + private def handleReverseChainParam(rtype: String, parameter: Parameter)(implicit transactionSession: Option[TransactionSession] = None): Future[Option[Bson]] = { + //Get the resource type to query (the end of revchain) + //e.g. Patient?_has:Observation:patient:_has:AuditEvent:entity:agent=MyUserId + val rtypeToQuery = parameter.chain.last._1 + val searchParameterConf = fhirConfig.findSupportedSearchParameter(rtypeToQuery, parameter.name) + if (searchParameterConf.isEmpty) + throw new UnsupportedParameterException(s"Parameter ${parameter.name} is not supported on $rtypeToQuery within '_has' query!") + + //Construct the Query on the leaf of chain to find the resource references + val query = ResourceQueryBuilder.constructQueryForNormal(parameter, searchParameterConf.get, fhirConfig.getSupportedParameters(rtypeToQuery)) //Find the path of reference - val chainParameterConf = fhirConfig.findSupportedSearchParameter(rtypeAndChainParamName._1,rtypeAndChainParamName._2) - if(chainParameterConf.isEmpty) - throw new UnsupportedParameterException(s"Parameter ${rtypeAndChainParamName._2} is not supported on ${rtypeAndChainParamName._1} within '_has' query!") + val chainParameterConf = fhirConfig.findSupportedSearchParameter(rtypeToQuery, parameter.chain.last._2) + if (chainParameterConf.isEmpty) + throw new UnsupportedParameterException(s"Parameter ${parameter.chain.last._2} is not supported on $rtypeToQuery within '_has' query!") //Find the paths of reference element to return val referencePaths = chainParameterConf.get.extractElementPaths().toSet - //Go and get the references inside the given resources - getResourcesWithIds(rtypeAndChainParamName._1, rids, Some(true -> referencePaths), excludeExtraFields = true) + //Run query but only return the references on chain parameter + var fresourceReferences = DocumentManager + .searchDocuments(rtypeToQuery, Some(query), includingOrExcludingFields = Some(true -> referencePaths)) + .map(_.map(_.fromBson)) .map( _.flatMap(r => referencePaths.flatMap(rpath => FHIRUtil.extractReferences(rpath, r)) ) ) - }) - } + // Come from deep in revchain by evaluating from right + fresourceReferences = + parameter.chain + .dropRight(1) // We already evaluate the last one + .foldRight(fresourceReferences)(findResourceReferencesInResources) + + fresourceReferences.map { + case Nil => None //No such resource + case references => + val rids = references + .map(FHIRUtil.parseReferenceValue) //Parse the reference value + .filter(_._1.forall(_ == OnfhirConfig.fhirRootUrl)) //Only the references in our server + .filter(_._2 == rtype) //Only the ones refering the given resource type + .map(_._3) + .toSet + //Return the query + Some(DocumentManager.ridsQuery(rids)) + } + } + + + /** * + * Within the given resources (by references) find resource references within the given parameter + * + * @param rtypeAndChainParamName Resource Type and Chain Parameter Name + * @param freferences References to the resources that search will be on + * @return + */ + private def findResourceReferencesInResources(rtypeAndChainParamName: (String, String), freferences: Future[Seq[String]])(implicit transactionSession: Option[TransactionSession] = None): Future[Seq[String]] = { + freferences.flatMap(references => { + val rids = + references + .map(FHIRUtil.parseReferenceValue) //Parse the reference value + .filter(_._1.forall(_ == OnfhirConfig.fhirRootUrl)) //Only the references in our server + .filter(_._2 == rtypeAndChainParamName._1) //Only the ones refering the given resource type + .map(_._3) + .toSet + + //Find the path of reference + val chainParameterConf = fhirConfig.findSupportedSearchParameter(rtypeAndChainParamName._1, rtypeAndChainParamName._2) + if (chainParameterConf.isEmpty) + throw new UnsupportedParameterException(s"Parameter ${rtypeAndChainParamName._2} is not supported on ${rtypeAndChainParamName._1} within '_has' query!") + + //Find the paths of reference element to return + val referencePaths = chainParameterConf.get.extractElementPaths().toSet + //Go and get the references inside the given resources + getResourcesWithIds(rtypeAndChainParamName._1, rids, Some(true -> referencePaths), excludeExtraFields = true) + .map( + _.flatMap(r => + referencePaths.flatMap(rpath => FHIRUtil.extractReferences(rpath, r)) + ) + ) + }) + } + */ /** - * Handle a chain parameter and returns a query indicating this chained search - * @param rtype Resource Type to search - * @param parameter Parsed Chained Parameter definition - * @return - */ - private def handleChainParam(rtype:String, parameter:Parameter)(implicit transactionSession: Option[TransactionSession] = None):Future[Option[Bson]] = { + * Return Mongodb Aggregation phases to handle the chain param for filtering + * + * @param rtype Resource type we are running the search on + * @param parameter Parsed chained parameter + * @return + */ + private def getChainParamStages(rtype: String, parameter: Parameter): Seq[Bson] = { //Get the resource type to query (the end of chain) // e.g. /DiagnosticReport?subject:Patient.name=peter --> chain=List(Patient, subject) --> Patient - // e.g. /DiagnosticReport?subject:Patient.general-practitioner.name=peter --> chain=List(Patient -> subject, Practitioner -> general-practitioner) + // e.g. /DiagnosticReport?subject:Patient.general-practitioner:Practitioner.name=peter --> chain=List(Patient -> subject, Practitioner -> general-practitioner) val rtypeToQuery = parameter.chain.last._1 val searchParameterConf = fhirConfig.findSupportedSearchParameter(rtypeToQuery, parameter.name) - if(searchParameterConf.isEmpty) + if (searchParameterConf.isEmpty) throw new UnsupportedParameterException(s"Parameter ${parameter.name} is not supported on $rtypeToQuery within chained query!") + //Run the Query on the leaf of chain to find the resource references - val query = ResourceQueryBuilder.constructQueryForNormal(parameter, searchParameterConf.get, fhirConfig.getSupportedParameters(rtypeToQuery)) - var fresourceReferences = - DocumentManager.searchDocumentsReturnIds(rtypeToQuery, query) - .map(rids => rids.map(rid => s"$rtypeToQuery/$rid") ) //Only get ids and convert them to references - - //Resource Type chains by references e.g. DiagnosticReport or DiagnosticReport, Patient - val rtypeChain = rtype +: parameter.chain.map(_._1).dropRight(1) - //Reference param names chain e.g. subject or subject, general-practitioner - val refParamChain = parameter.chain.map(_._2) - //Zip these - val chain = rtypeChain.zip(refParamChain) - // Come from deep in chain by evaluating from right - fresourceReferences = chain.tail.foldRight(fresourceReferences)( findResourceIdsReferingChained) - //Construct last query - fresourceReferences.map { - case Nil => None //If the chain search result is empty, we don't have exra query - case refs => Some(constructQueryForChained(chain.head._1, chain.head._2, refs)) - } + val query = getResourceQueryBuilder(rtypeToQuery).constructQueryForNormal(parameter, searchParameterConf.get, fhirConfig.getSupportedParameters(rtypeToQuery)) + + //Keep the resource type for each iteration + var previousResourceType = rtype + var previousPrefix: Option[String] = None + val chainLookupPhases = + parameter.chain + .sliding(2) + .filter(_.length == 2) //Eliminate for single chain + .map(s => s.head -> s.last) + .map { + //e.g. (Patient, subject), (Practitioner, general-practitioner) + case ((c1rtype, c1param), (_, c2param)) => + val chainedSearchParamConf = fhirConfig.findSupportedSearchParameter(previousResourceType, c1param) + if (chainedSearchParamConf.isEmpty || chainedSearchParamConf.get.ptype != FHIR_PARAMETER_TYPES.REFERENCE) + throw new UnsupportedParameterException(s"Parameter $c1param is not supported on $previousResourceType or cannot be used for chaining!") + //Get chained reference path for resource identifier that is chained + // e.g. Observation --> subject.reference.__rid + val chainedPath = previousPrefix.map(p => p + ".").getOrElse("") + chainedSearchParamConf.get.extractElementPaths().head + ".reference.__rid" + + //Find the next chaining param e.g. general-practitioner + val chained2SearchParamConf = fhirConfig.findSupportedSearchParameter(c1rtype, c2param) + if (chainedSearchParamConf.isEmpty || chainedSearchParamConf.get.ptype != FHIR_PARAMETER_TYPES.REFERENCE) + throw new UnsupportedParameterException(s"Parameter $c2param is not supported on $c1rtype or cannot be used for chaining!") + //Find the parameter to keep for next chaining e.g. Patient--> generalPractitioner + val elementToInclude = chained2SearchParamConf.get.extractElementPaths().head.split('.').head + //Set previous resource type and prefix + previousResourceType = c1rtype + previousPrefix = Some(s"${c1rtype}_$c1param") + //Construct the $lookup expression for chaining + //e.g. { $lookup: { + // from: "Patient", + // localField: "subject.reference.__rid", + // foreignField: "id", + // pipeline: [ + // { + // $project: {"generalPractitioner": 1} + // } + // ], + // as: "Patient_subject", + //}} + AggregationUtil + .constructLookupPhaseExpression(c1rtype, chainedPath, "id", Seq(Aggregates.project(Projections.include(elementToInclude)).toBsonDocument), s"${c1rtype}_$c1param") + }.toSeq + + //Leaf lookup + val (crtype, cparam) = parameter.chain.last + val chainedSearchParamConf = fhirConfig.findSupportedSearchParameter(previousResourceType, cparam) + if(chainedSearchParamConf.isEmpty) + throw new UnsupportedParameterException(s"Parameter $cparam is not supported on $previousResourceType or cannot be used for chaining!") + + val chainedPath = previousPrefix.map(p => p + ".").getOrElse("") + chainedSearchParamConf.get.extractElementPaths().head + ".reference.__rid" + //Construct last lookup phase for leaf + //e.g.{ + // from: "Practitioner", + // localField: "Patient_subject.generalPractitioner.reference.__rid", + // foreignField: "id", + // pipeline: [ + // { + // $match: { gender: "male" }, + // }, + // { + // $project: { _id: 1 }, + // }, + // ], + // as: "Practitioner_general-practitioner", + //} + val leafLookupPhases = Seq( + AggregationUtil + .constructLookupPhaseExpression(crtype, chainedPath, "id", Seq(Aggregates.`match`(query).toBsonDocument, Aggregates.project(Projections.include("_id")).toBsonDocument), s"${crtype}_$cparam"), + Aggregates.filter(AggregationUtil.constructGreaterThanSizeExpression(s"${crtype}_$cparam", 0)) //At least one resource should satisfy + ) + + val chainPhases: Seq[Bson] = + chainLookupPhases ++ leafLookupPhases :+ + Aggregates.project(Projections.exclude(parameter.chain.map(c => s"${c._1}_${c._2}"): _*)) //Exclude the added fields in lookup phases + + + chainPhases } + /* + /** + * Handle a chain parameter and returns a query indicating this chained search + * + * @param rtype Resource Type to search + * @param parameter Parsed Chained Parameter definition + * @return + */ + private def handleChainParam(rtype: String, parameter: Parameter)(implicit transactionSession: Option[TransactionSession] = None): Future[Option[Bson]] = { + //Get the resource type to query (the end of chain) + // e.g. /DiagnosticReport?subject:Patient.name=peter --> chain=List(Patient, subject) --> Patient + // e.g. /DiagnosticReport?subject:Patient.general-practitioner.name=peter --> chain=List(Patient -> subject, Practitioner -> general-practitioner) + val rtypeToQuery = parameter.chain.last._1 + val searchParameterConf = fhirConfig.findSupportedSearchParameter(rtypeToQuery, parameter.name) + if (searchParameterConf.isEmpty) + throw new UnsupportedParameterException(s"Parameter ${parameter.name} is not supported on $rtypeToQuery within chained query!") + + //Run the Query on the leaf of chain to find the resource references + val query = ResourceQueryBuilder.constructQueryForNormal(parameter, searchParameterConf.get, fhirConfig.getSupportedParameters(rtypeToQuery)) + var fresourceReferences = + DocumentManager.searchDocumentsReturnIds(rtypeToQuery, query) + .map(rids => rids.map(rid => s"$rtypeToQuery/$rid")) //Only get ids and convert them to references + + //Resource Type chains by references e.g. DiagnosticReport or DiagnosticReport, Patient + val rtypeChain = rtype +: parameter.chain.map(_._1).dropRight(1) + //Reference param names chain e.g. subject or subject, general-practitioner + val refParamChain = parameter.chain.map(_._2) + //Zip these + val chain = rtypeChain.zip(refParamChain) + // Come from deep in chain by evaluating from right + fresourceReferences = chain.tail.foldRight(fresourceReferences)(findResourceIdsReferingChained) + //Construct last query + fresourceReferences.map { + case Nil => None //If the chain search result is empty, we don't have exra query + case refs => Some(constructQueryForChained(chain.head._1, chain.head._2, refs)) + } + } + + /** + * Construct query for chained searches + * + * @param rtype Resource type + * @param pname Parameter name + * @param references All Resource references to search + * @return + */ + private def constructQueryForChained(rtype: String, pname: String, references: Seq[String]): Bson = { + val parameter = Parameter(FHIR_PARAMETER_CATEGORIES.NORMAL, FHIR_PARAMETER_TYPES.REFERENCE, pname, references.map(r => "" -> r)) + val searchParamConf = fhirConfig.findSupportedSearchParameter(rtype, pname) + if (searchParamConf.isEmpty) + throw new UnsupportedParameterException(s"Parameter $pname is not supported on $rtype within chained query!") + ResourceQueryBuilder.constructQueryForSimpleParameter(parameter, searchParamConf.get) + } + + /** + * Find resource ids that refer the given resources + * + * @param rtypeAndPName Resource Type and reference parameter name to search + * @param fcids given resource ids to refer + * @return + */ + private def findResourceIdsReferingChained(rtypeAndPName: (String, String), fcids: Future[Seq[String]])(implicit transactionSession: Option[TransactionSession] = None): Future[Seq[String]] = { + fcids.flatMap(cids => { + val query = constructQueryForChained(rtypeAndPName._1, rtypeAndPName._2, cids) + DocumentManager.searchDocumentsReturnIds(rtypeAndPName._1, query) + .map(rids => + rids.map(rid => s"${rtypeAndPName._1}/$rid") + ) + }) + }*/ +/* /** * Construct query for chained searches * @param rtype Resource type @@ -1022,7 +1418,7 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = val searchParamConf = fhirConfig.findSupportedSearchParameter(rtype, pname) if(searchParamConf.isEmpty) throw new UnsupportedParameterException(s"Parameter $pname is not supported on $rtype within chained query!") - ResourceQueryBuilder.constructQueryForSimpleParameter(parameter, searchParamConf.get) + getResourceQueryBuilder(rtype).constructQueryForSimpleParameter(parameter, searchParamConf.get) } /** @@ -1039,7 +1435,7 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = rids.map(rid => s"${rtypeAndPName._1}/$rid") ) }) - } + }*/ /** * Returns a specific version of a resource. If version id is not provided then the current version is return @@ -1061,32 +1457,34 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = } /** - * Check if given resource exist and active (not deleted) - * @param rtype Resource type - * @param id Resource id - * @return - */ - def isResourceExist(rtype:String, id:String):Future[Boolean] = { + * Check if given resource exist and active (not deleted) + * + * @param rtype Resource type + * @param id Resource id + * @return + */ + def isResourceExist(rtype: String, id: String): Future[Boolean] = { DocumentManager .getDocument(rtype, id, None, Some(true -> Set.empty), excludeExtraFields = false) .map(document => document.map(_.fromBson) ).map(resource => - resource.exists(r => !FHIRUtil.isDeleted(r)) - ) + resource.exists(r => !FHIRUtil.isDeleted(r)) + ) } /** - * Get resources with given Id set - * @param rtype Resource type - * @param rids Resource ids - * @param includingOrExcludingFields Elements to include or exclude - * @param excludeExtraFields If true exclude extra fields - * @return - */ - def getResourcesWithIds(rtype:String, rids:Set[String], includingOrExcludingFields:Option[(Boolean, Set[String])] = None, excludeExtraFields:Boolean = false) (implicit transactionSession: Option[TransactionSession] = None):Future[Seq[Resource]] = { + * Get resources with given Id set + * + * @param rtype Resource type + * @param rids Resource ids + * @param includingOrExcludingFields Elements to include or exclude + * @param excludeExtraFields If true exclude extra fields + * @return + */ + def getResourcesWithIds(rtype: String, rids: Set[String], includingOrExcludingFields: Option[(Boolean, Set[String])] = None, excludeExtraFields: Boolean = false)(implicit transactionSession: Option[TransactionSession] = None): Future[Seq[Resource]] = { DocumentManager - .searchDocuments(rtype, Some(DocumentManager.ridsQuery(rids)), includingOrExcludingFields = includingOrExcludingFields, excludeExtraFields = excludeExtraFields) + .searchDocuments(rtype, Some(DocumentManager.ridsQuery(rids)), Nil, includingOrExcludingFields = includingOrExcludingFields, excludeExtraFields = excludeExtraFields) .map(_.map(_.fromBson)) } @@ -1095,11 +1493,12 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = * @param since FHIR time to query history * @return */ - private def sinceQuery(since:String):Bson = - PrefixModifierHandler.dateRangePrefixHandler( + private def sinceQuery(since:String):Bson = { + DateQueryBuilder.getQueryForTimePoint( s"${FHIR_COMMON_FIELDS.META}.${FHIR_COMMON_FIELDS.LAST_UPDATED}", since, FHIR_PREFIXES_MODIFIERS.GREATER_THAN_EQUAL) + } /** * FHIR _at query on history @@ -1107,20 +1506,21 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = * @return */ private def atQuery(atTime:String):Bson = - PrefixModifierHandler.dateRangePrefixHandler( + DateQueryBuilder.getQueryForTimePoint( s"${FHIR_COMMON_FIELDS.META}.${FHIR_COMMON_FIELDS.LAST_UPDATED}", atTime, FHIR_PREFIXES_MODIFIERS.LESS_THAN) /** - * Get history of a resource type or resource instance (FHIR History interactions) - * @param rtype Resource Type - * @param rid Resource id - * @param searchParameters FHIR search parameters for history search - * @return - */ - def getResourceHistory(rtype:String, rid:Option[String], searchParameters:List[Parameter])(implicit transactionSession: Option[TransactionSession] = None):Future[(Long, Seq[Resource])] = { + * Get history of a resource type or resource instance (FHIR History interactions) + * + * @param rtype Resource Type + * @param rid Resource id + * @param searchParameters FHIR search parameters for history search + * @return + */ + def getResourceHistory(rtype: String, rid: Option[String], searchParameters: List[Parameter])(implicit transactionSession: Option[TransactionSession] = None): Future[(Long, Seq[Resource])] = { //Resolve history parameters val since = searchParameters.find(_.name == FHIR_SEARCH_RESULT_PARAMETERS.SINCE).map(_.valuePrefixList.head._2) val at = searchParameters.find(_.name == FHIR_SEARCH_RESULT_PARAMETERS.AT).map(_.valuePrefixList.head._2) @@ -1131,20 +1531,18 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = case Right(_) => throw new NotImplementedError("Offset based pagination is not supported for searching history!") } - val listQueryFuture = searchParameters.find(_.name == FHIR_SEARCH_SPECIAL_PARAMETERS.LIST) match { - case None => Future.apply(None) - case Some(p) => handleListSearch(rtype, p) - } + val listFilteringStages: Seq[Bson] = + searchParameters + .find(_.name == FHIR_SEARCH_SPECIAL_PARAMETERS.LIST) + .map(p => getFilteringPhasesForListSearch(rtype, p)) + .getOrElse(Nil) - val result = listQueryFuture flatMap(listQuery => { - if(at.isDefined){ - val finalQuery = DocumentManager.andQueries(listQuery.toSeq :+ atQuery(at.get)) - DocumentManager.searchHistoricDocumentsWithAt(rtype, rid, finalQuery, count, page) + val result = + if (at.isDefined) { + DocumentManager.searchHistoricDocumentsWithAt(rtype, rid, Some(atQuery(at.get)),listFilteringStages, count, page) } else { - val finalQuery = DocumentManager.andQueries(since.map(sinceQuery).toSeq ++ listQuery.toSeq) - DocumentManager.searchHistoricDocuments(rtype, rid, finalQuery, count, page) + DocumentManager.searchHistoricDocuments(rtype, rid, since.map(sinceQuery),listFilteringStages, count, page) } - }) result .map { @@ -1153,66 +1551,69 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = } /** - * Get the latest status of a resource - * @param rtype Resource type - * @param id Resource id - * @return Latest Version id, Timestamp, and isDeleted - */ - def getResourceStatus(rtype:String, id:String):Future[Option[(Long, DateTime, Boolean)]] = { + * Get the latest status of a resource + * + * @param rtype Resource type + * @param id Resource id + * @return Latest Version id, Timestamp, and isDeleted + */ + def getResourceStatus(rtype: String, id: String): Future[Option[(Long, DateTime, Boolean)]] = { DocumentManager .getDocument(rtype, id, includingOrExcludingFields = Some(true -> Set.empty)) .map( _.map(_.fromBson) .map(resource => - ( - FHIRUtil.extractVersionFromResource(resource), - FHIRUtil.extractLastUpdatedFromResource(resource), - FHIRUtil.isDeleted(resource) + ( + FHIRUtil.extractVersionFromResource(resource), + FHIRUtil.extractLastUpdatedFromResource(resource), + FHIRUtil.isDeleted(resource) + ) ) - ) ) } /** - * Creates the given FHIR resource into database - * @param rtype Resource type - * @param resource FHIR resource - * @param generatedId If given resource is created with this id, otherwise a uuid is generated - * @param withUpdate Indicates if this creation is done by an update operation - * @return a future indicating the assigned resource id and version and modified time and the resource itself - */ - def createResource(rtype:String, resource:Resource, generatedId:Option[String] = None, withUpdate:Boolean = false)(implicit transactionSession: Option[TransactionSession] = None):Future[(String, Long, DateTime, Resource)] = { + * Creates the given FHIR resource into database + * + * @param rtype Resource type + * @param resource FHIR resource + * @param generatedId If given resource is created with this id, otherwise a uuid is generated + * @param withUpdate Indicates if this creation is done by an update operation + * @return a future indicating the assigned resource id and version and modified time and the resource itself + */ + def createResource(rtype: String, resource: Resource, generatedId: Option[String] = None, withUpdate: Boolean = false)(implicit transactionSession: Option[TransactionSession] = None): Future[(String, Long, DateTime, Resource)] = { //1) set resource version and last update time - val newVersion = 1L //new version is always 1 for create operation - val lastModified = Instant.now() //last modified will be "now" - val newId = generatedId.getOrElse(FHIRUtil.generateResourceId()) //generate an identifier for the new resource + val newVersion = 1L //new version is always 1 for create operation + val lastModified = Instant.now() //last modified will be "now" + val newId = generatedId.getOrElse(FHIRUtil.generateResourceId()) //generate an identifier for the new resource val resourceWithMeta = FHIRUtil.populateResourceWithMeta(resource, Some(newId), newVersion, lastModified) //2) add "current" field with value true (after serializing to json) val populatedResource = FHIRUtil.populateResourceWithExtraFields( resourceWithMeta, - if(withUpdate) FHIR_METHOD_NAMES.METHOD_PUT else FHIR_METHOD_NAMES.METHOD_POST, //how it is created + if (withUpdate) FHIR_METHOD_NAMES.METHOD_PUT else FHIR_METHOD_NAMES.METHOD_POST, //how it is created StatusCodes.Created) //3) persist the resource DocumentManager .insertDocument(rtype, Document(populatedResource.toBson)) - .map( _ => resourceCreated(rtype, newId, resourceWithMeta)) //trigger the event - .map( _ => + .map(_ => resourceCreated(rtype, newId, resourceWithMeta)) //trigger the event + .map(_ => (newId, newVersion, DateTimeUtil.instantToDateTime(lastModified), resourceWithMeta) ) } /** - * Create the given resources - * @param rtype Resource type - * @param resources Resources to create - * @return - */ - def createResources(rtype:String, resources:Map[String, Resource]):Future[Seq[Resource]] = { - val newVersion = 1L //new version is always 1 for create operation - val lastModified = Instant.now() //last modified will be "now" + * Create the given resources + * + * @param rtype Resource type + * @param resources Resources to create + * @return + */ + def createResources(rtype: String, resources: Map[String, Resource]): Future[Seq[Resource]] = { + val newVersion = 1L //new version is always 1 for create operation + val lastModified = Instant.now() //last modified will be "now" val resourcesWithMeta = resources.map { case (rid, resource) => FHIRUtil.populateResourceWithMeta(resource, Some(rid), newVersion, lastModified) }.toSeq @@ -1226,103 +1627,105 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = .insertDocuments(rtype, populatedDocuments) .map(_ => resourcesWithMeta) } -/* - /** - * Updates a given FHIR resource - * @param rtype Resource type - * @param rid Resource id to update - * @param resource Updated resource - * @param previousVersion Previous version id of the resource - * @param wasDeleted If previously, this was deleted resource - * @return Latest version, modified time, and the final resource content - */ - def updateResource(rtype:String, rid:String, resource:Resource, previousVersion:Long = 0L, wasDeleted :Boolean = false)(implicit transactionSession: Option[TransactionSession] = None):Future[(Long, DateTime, Resource)] = { - //1) Update the resource version and last update time - val newVersion = previousVersion + 1L //new version always be 1 incremented of current version - val lastModified = DateTime.now //last modified will be "now" - val resourceWithMeta = FHIRUtil.populateResourceWithMeta(resource, Some(rid), newVersion, lastModified) - - //2) Add "current" field with value. (after serializing to json) - val populatedResource = FHIRUtil.populateResourceWithExtraFields(resourceWithMeta, FHIR_METHOD_NAMES.METHOD_PUT, if(previousVersion > 0 && !wasDeleted) StatusCodes.OK else StatusCodes.Created) - - //3) persist the resource - DocumentManager - .insertNewVersion(rtype, rid, Document(populatedResource.toBson)) - .map( c => resourceUpdated(rtype, rid, resource)) //trigger the event - .map( _ => - (newVersion, lastModified, resourceWithMeta) - ) - }*/ + /* + /** + * Updates a given FHIR resource + * @param rtype Resource type + * @param rid Resource id to update + * @param resource Updated resource + * @param previousVersion Previous version id of the resource + * @param wasDeleted If previously, this was deleted resource + * @return Latest version, modified time, and the final resource content + */ + def updateResource(rtype:String, rid:String, resource:Resource, previousVersion:Long = 0L, wasDeleted :Boolean = false)(implicit transactionSession: Option[TransactionSession] = None):Future[(Long, DateTime, Resource)] = { + //1) Update the resource version and last update time + val newVersion = previousVersion + 1L //new version always be 1 incremented of current version + val lastModified = DateTime.now //last modified will be "now" + val resourceWithMeta = FHIRUtil.populateResourceWithMeta(resource, Some(rid), newVersion, lastModified) + + //2) Add "current" field with value. (after serializing to json) + val populatedResource = FHIRUtil.populateResourceWithExtraFields(resourceWithMeta, FHIR_METHOD_NAMES.METHOD_PUT, if(previousVersion > 0 && !wasDeleted) StatusCodes.OK else StatusCodes.Created) + + //3) persist the resource + DocumentManager + .insertNewVersion(rtype, rid, Document(populatedResource.toBson)) + .map( c => resourceUpdated(rtype, rid, resource)) //trigger the event + .map( _ => + (newVersion, lastModified, resourceWithMeta) + ) + }*/ /** - * Updates a given FHIR resource - * @param rtype Resource type - * @param rid Resource id to update - * @param resource Updated resource - * @param previousVersion Previous version of the document together with version id - * @param wasDeleted If previously, this was deleted resource - * @param silentEvent If true, ResourceUpdate event is not triggered (used internally) - * @param transactionSession Transcation session - * @return - */ - def updateResource(rtype:String, rid:String, resource:Resource, previousVersion:(Long, Resource), wasDeleted :Boolean = false, silentEvent:Boolean = false)(implicit transactionSession: Option[TransactionSession] = None):Future[(Long, DateTime, Resource)] = { + * Updates a given FHIR resource + * + * @param rtype Resource type + * @param rid Resource id to update + * @param resource Updated resource + * @param previousVersion Previous version of the document together with version id + * @param wasDeleted If previously, this was deleted resource + * @param silentEvent If true, ResourceUpdate event is not triggered (used internally) + * @param transactionSession Transcation session + * @return + */ + def updateResource(rtype: String, rid: String, resource: Resource, previousVersion: (Long, Resource), wasDeleted: Boolean = false, silentEvent: Boolean = false)(implicit transactionSession: Option[TransactionSession] = None): Future[(Long, DateTime, Resource)] = { //1) Update the resource version and last update time - val newVersion = previousVersion._1 + 1L //new version always be 1 incremented of current version - val lastModified = Instant.now() //last modified will be "now" + val newVersion = previousVersion._1 + 1L //new version always be 1 incremented of current version + val lastModified = Instant.now() //last modified will be "now" val resourceWithMeta = FHIRUtil.populateResourceWithMeta(resource, Some(rid), newVersion, lastModified) //2) Add "current" field with value. (after serializing to json) - val populatedResource = FHIRUtil.populateResourceWithExtraFields(resourceWithMeta, FHIR_METHOD_NAMES.METHOD_PUT, if(previousVersion._1 > 0 && !wasDeleted) StatusCodes.OK else StatusCodes.Created) + val populatedResource = FHIRUtil.populateResourceWithExtraFields(resourceWithMeta, FHIR_METHOD_NAMES.METHOD_PUT, if (previousVersion._1 > 0 && !wasDeleted) StatusCodes.OK else StatusCodes.Created) //3) Construct shard query if sharding is enabled and on a field other than id - val shardQueryOpt = ResourceQueryBuilder.constructShardingQuery(rtype, resource) + val shardQueryOpt = getResourceQueryBuilder(rtype).constructShardingQuery(resource) //4) Remove mongo id from old version val oldBsonDocument = previousVersion._2.toBson oldBsonDocument.remove(FHIR_COMMON_FIELDS.MONGO_ID) val fop = - //If the resource type is versioned - if(fhirConfig.isResourceTypeVersioned(rtype)) { + //If the resource type is versioned + if (fhirConfig.isResourceTypeVersioned(rtype)) { //If deleted, just insert this new to current collection - if(wasDeleted) + if (wasDeleted) DocumentManager.insertDocument(rtype, Document(populatedResource.toBson)) - else //If not deleted, insert new version and move the old version to history + else //If not deleted, insert new version and move the old version to history DocumentManager .insertNewVersion(rtype, rid, Document(populatedResource.toBson), previousVersion._1 -> Document(oldBsonDocument), shardQueryOpt) } else - //Otherwise, replace current version + //Otherwise, replace current version DocumentManager .replaceCurrent(rtype, rid, Document(populatedResource.toBson), shardQueryOpt) - if(silentEvent) + if (silentEvent) fop.map(_ => (newVersion, DateTimeUtil.instantToDateTime(lastModified), resourceWithMeta) ) else fop - .map( _ => resourceUpdated(rtype, rid, resourceWithMeta, FHIRUtil.clearExtraFields(previousVersion._2))) //trigger the event - .map( _ => + .map(_ => resourceUpdated(rtype, rid, resourceWithMeta, FHIRUtil.clearExtraFields(previousVersion._2))) //trigger the event + .map(_ => (newVersion, DateTimeUtil.instantToDateTime(lastModified), resourceWithMeta) ) } /** * Upsert the new version of resource for no-versioning resource types - * @param rtype FHIR resource type - * @param rid FHIR resource id - * @param resource Updated content + * + * @param rtype FHIR resource type + * @param rid FHIR resource id + * @param resource Updated content * @param transactionSession * @return */ - def upsertResource(rtype:String, rid:String, resource:Resource)(implicit transactionSession: Option[TransactionSession] = None):Future[(DateTime, Resource)] = { + def upsertResource(rtype: String, rid: String, resource: Resource)(implicit transactionSession: Option[TransactionSession] = None): Future[(DateTime, Resource)] = { //1) Update the last update time val lastModified = Instant.now() //last modified will be "now" val resourceWithMeta = FHIRUtil.populateResourceWithMeta(resource, Some(rid), lastModified) //2) Add "current" field with value. (after serializing to json) val populatedResource = FHIRUtil.populateResourceWithExtraFields(resourceWithMeta, FHIR_METHOD_NAMES.METHOD_PUT, StatusCodes.OK) //3) Construct shard query if sharding is enabled and on a field other than id - val shardQueryOpt = ResourceQueryBuilder.constructShardingQuery(rtype, resource) + val shardQueryOpt = getResourceQueryBuilder(rtype).constructShardingQuery(resource) DocumentManager .replaceCurrent(rtype, rid, Document(populatedResource.toBson), shardQueryOpt) .map(_ => @@ -1332,15 +1735,16 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = /** * Insert or replace given resources - * @param rtype Resource type - * @param resources Resources with resource id if replace - * @param ordered Whether order is important + * + * @param rtype Resource type + * @param resources Resources with resource id if replace + * @param ordered Whether order is important * @param transactionSession * @return */ - def bulkUpsertResources(rtype:String, resources:Seq[(Option[String], Resource)], ordered:Boolean = false)(implicit transactionSession: Option[TransactionSession] = None):Future[(Int, Int)] = { + def bulkUpsertResources(rtype: String, resources: Seq[(Option[String], Resource)], ordered: Boolean = false)(implicit transactionSession: Option[TransactionSession] = None): Future[(Int, Int)] = { val lastModified = Instant.now() - val populatedDocuments = + val populatedDocuments = resources .map(r => r._1 -> FHIRUtil.populateResourceWithMeta(r._2, r._1.orElse(Some(FHIRUtil.generateResourceId())), lastModified)) .map(r => r._1 -> FHIRUtil.populateResourceWithExtraFields(r._2, FHIR_METHOD_NAMES.METHOD_PUT, StatusCodes.OK)) @@ -1372,25 +1776,26 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = ) }*/ - /*** - * Deletes a given FHIR resource - * @param rtype Resource type - * @param rid Resource id to delete - * @param previousVersion Previous version id of the resource - * @param statusCode Http Status to set for deletion result - * @return - */ - def deleteResource(rtype:String, rid:String, previousVersion:(Long, Resource), statusCode:StatusCode = StatusCodes.NoContent)(implicit transactionSession: Option[TransactionSession] = None):Future[(Long, DateTime)] = { + /** * + * Deletes a given FHIR resource + * + * @param rtype Resource type + * @param rid Resource id to delete + * @param previousVersion Previous version id of the resource + * @param statusCode Http Status to set for deletion result + * @return + */ + def deleteResource(rtype: String, rid: String, previousVersion: (Long, Resource), statusCode: StatusCode = StatusCodes.NoContent)(implicit transactionSession: Option[TransactionSession] = None): Future[(Long, DateTime)] = { //1) Create a empty document to represent deletion - val newVersion = previousVersion._1 + 1L //new version is 1 incremented + val newVersion = previousVersion._1 + 1L //new version is 1 incremented val lastModified = Instant.now() //2) Construct shard query if sharding is enabled and on a field other than id - val shardQueryOpt = ResourceQueryBuilder.constructShardingQuery(rtype, previousVersion._2) + val shardQueryOpt = getResourceQueryBuilder(rtype).constructShardingQuery(previousVersion._2) val fop = - // 4) If resource type is versioned, insert this deleted version, and move old version to history - if(fhirConfig.isResourceTypeVersioned(rtype)) { + // 4) If resource type is versioned, insert this deleted version, and move old version to history + if (fhirConfig.isResourceTypeVersioned(rtype)) { //3) Remove mongo id from old version val oldBsonDocument = previousVersion._2.toBson oldBsonDocument.remove(FHIR_COMMON_FIELDS.MONGO_ID) @@ -1403,59 +1808,71 @@ class ResourceManager(fhirConfig:FhirServerConfig, fhirEventBus: IFhirEventBus = DocumentManager .deleteCurrentAndMoveToHistory(rtype, rid, previousVersion._1, oldBsonDocument, deletedBsonDocument, shardQueryOpt) } else - //Otherwise, mark the current as deleted + //Otherwise, mark the current as deleted DocumentManager.deleteCurrent(rtype, rid, shardQueryOpt) - fop - .map( _ => resourceDeleted(rtype, rid, FHIRUtil.clearExtraFields(previousVersion._2))) //trigger the event - .map( _ => + fop + .map(_ => resourceDeleted(rtype, rid, FHIRUtil.clearExtraFields(previousVersion._2))) //trigger the event + .map(_ => (newVersion, DateTimeUtil.instantToDateTime(lastModified)) ) } /** - * Replace a current resource with the given content (Used by onFHIR administrative parts) without adding old version to the history - * @param rtype Resource Type - * @param rid Resource id - * @param resource Replaced resource - * @return - */ - def replaceResource(rtype:String, rid:String, resource:Resource):Future[Boolean] = { + * Replace a current resource with the given content (Used by onFHIR administrative parts) without adding old version to the history + * + * @param rtype Resource Type + * @param rid Resource id + * @param resource Replaced resource + * @return + */ + def replaceResource(rtype: String, rid: String, resource: Resource): Future[Boolean] = { //2) Construct shard query if sharding is enabled and on a field other than id - val shardQueryOpt = ResourceQueryBuilder.constructShardingQuery(rtype, resource) + val shardQueryOpt = getResourceQueryBuilder(rtype).constructShardingQuery(resource) DocumentManager.replaceCurrent(rtype, rid, Document(resource.toBson), shardQueryOpt) } /** - * Event triggered when a resource is created - * @param rtype Resource type - * @param rid Resource id - * @param resource Created resource - * @return - */ - private def resourceCreated(rtype:String, rid:String, resource:Resource): Unit = { + * Event triggered when a resource is created + * + * @param rtype Resource type + * @param rid Resource id + * @param resource Created resource + * @return + */ + private def resourceCreated(rtype: String, rid: String, resource: Resource): Unit = { fhirEventBus.publish(ResourceCreated(rtype, rid, resource)) } /** - * Event triggered when a resource is updated - * @param rtype Resource type - * @param rid Resource id - * @param resource Last version of updated resource - * @return - */ - private def resourceUpdated(rtype:String, rid:String, resource:Resource, previous:Resource): Unit = { + * Event triggered when a resource is updated + * + * @param rtype Resource type + * @param rid Resource id + * @param resource Last version of updated resource + * @return + */ + private def resourceUpdated(rtype: String, rid: String, resource: Resource, previous: Resource): Unit = { fhirEventBus.publish(ResourceUpdated(rtype, rid, resource, previous)) } /** - * Event triggered when a resource is deleted - * @param rtype Resource type - * @param rid Deleted resource id - * @return - */ - private def resourceDeleted(rtype:String, rid:String, previous:Resource):Unit = { + * Event triggered when a resource is deleted + * + * @param rtype Resource type + * @param rid Deleted resource id + * @return + */ + private def resourceDeleted(rtype: String, rid: String, previous: Resource): Unit = { fhirEventBus.publish(ResourceDeleted(rtype, rid, previous)) } + + + private def getResourceQueryBuilder(rtype:String):ResourceQueryBuilder = { + new ResourceQueryBuilder( + fhirConfig + .resourceConfigurations.getOrElse(rtype, ResourceConf(rtype)) + ) + } } 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 c7ff19c9..87941693 100644 --- a/onfhir-core/src/main/scala/io/onfhir/db/ResourceQueryBuilder.scala +++ b/onfhir-core/src/main/scala/io/onfhir/db/ResourceQueryBuilder.scala @@ -5,7 +5,7 @@ import io.onfhir.api.model.{FHIRResponse, OutcomeIssue, Parameter} import io.onfhir.api.parsers.FHIRSearchParameterValueParser import io.onfhir.api.util.FHIRUtil import io.onfhir.config.FhirConfigurationManager.fhirConfig -import io.onfhir.config.{OnfhirConfig, SearchParameterConf} +import io.onfhir.config.{OnfhirConfig, ResourceConf, SearchParameterConf} import io.onfhir.exception.{BadRequestException, InvalidParameterException} import org.mongodb.scala.bson.conversions.Bson import org.mongodb.scala.model.Filters @@ -13,109 +13,160 @@ import org.mongodb.scala.model.Filters._ import org.slf4j.{Logger, LoggerFactory} /** - * MongoDB query builder for FHIR search mechanisms - */ -object ResourceQueryBuilder { + * MongoDB query builder for FHIR search mechanisms for the given configuration of resource type + * @param resourceConf REST Configuration for research type + */ +class ResourceQueryBuilder(resourceConf: ResourceConf) extends IFhirQueryBuilder { protected val logger:Logger = LoggerFactory.getLogger(this.getClass) /** - * Construct query for Normal Category Search Parameters - * @param parameter Parsed search parameter - * @param searchParameterConf Configuration for the search parameter - * @return - */ + * Construct query for normal search parameters (token, reference, quantity, etc) + * + * @param parameter Parsed search parameter + * @param searchParameterConf Configuration for the search parameter + * @return + */ def constructQueryForSimpleParameter(parameter:Parameter, searchParameterConf:SearchParameterConf):Bson = { - //This part handles search with simple query parameters - val queries = - parameter - .valuePrefixList //Go over each value to OR them - .map(pv => { - //There exists either a prefix or modifier, not both - val modifierOrPrefix = - parameter.suffix match { - //If there is no modifier, use the prefix - case "" => pv._1 - //Not is handled specially, so no modifier or prefix - case FHIR_PREFIXES_MODIFIERS.NOT => "" - //Use other modifiers - case oth => oth - } - //Handle common modifiers here - modifierOrPrefix match { - //Missing modifier is common - case FHIR_PREFIXES_MODIFIERS.MISSING => - if(searchParameterConf.ptype == FHIR_PARAMETER_TYPES.COMPOSITE) - throw new InvalidParameterException(s"Missing modifier cannot be used with composite parameters!") - val paths = searchParameterConf.extractElementPaths(withArrayIndicators = true) - PrefixModifierHandler.missingHandler(paths.map(FHIRUtil.normalizeElementPath), parameter.valuePrefixList.head._2) - - //Otherwise - case _ => - constructQueryForSimple(pv._2, parameter.paramType, modifierOrPrefix, searchParameterConf) - } - }) - parameter.suffix match { - //Handle the not modifier specially at the end - case FHIR_PREFIXES_MODIFIERS.NOT => - if (queries.length > 1) nor(queries: _*) else not(queries.head) + //Missing modifier is common, so handle it here + case FHIR_PREFIXES_MODIFIERS.MISSING => + if (searchParameterConf.ptype == FHIR_PARAMETER_TYPES.COMPOSITE) + throw new InvalidParameterException(s"Missing modifier cannot be used with composite parameters!") + if(parameter.valuePrefixList.length != 1) + throw new InvalidParameterException("Invalid parameter value for :missing modifier, either true or false should be provided!") + val paths = searchParameterConf.extractElementPaths(withArrayIndicators = true) + constructQueryForMissingModifier(paths.map(FHIRUtil.normalizeElementPath), parameter.valuePrefixList.head._2) + //Any other modifier or no modifier case _ => - if (queries.length > 1) or(queries: _*) else queries.head + //For each possible path, construct queries + val queries:Seq[Bson] = + searchParameterConf + .extractElementPathsTargetTypesAndRestrictions(withArrayIndicators = true) + .map { + case (path, targetType, Nil) => + constructQueryForSimpleWithoutRestriction(parameter, searchParameterConf, path, targetType) + //If there is a restriction on the search or search on extension we assume it is a direct field match e.g phone parameter on Patient + //e.g. f:PlanDefinition/f:relatedArtifact[f:type/@value='depends-on']/f:resource --> path = relatedArtifact[i].resource, restriction = @.type --> (relatedArtifact[i], resource, type) + //e.g. f:OrganizationAffiliation/f:telecom[system/@value='email'] --> path => telecom[i] , restriction = system --> (telecom[i], "", system) + case (path, targetType, restrictions) => + //Find the index in the path for given restrictions + val pathParts = path.split('.').toIndexedSeq + val indexOfRestrictions = FHIRUtil.findIndexOfRestrictionsOnPath(pathParts, restrictions) + constructQueryForSimpleWithRestrictions(parameter, searchParameterConf, pathParts, targetType, indexOfRestrictions) + } + //merge queries + mergeQueriesFromDifferentPaths(parameter.suffix, queries) } } /** - * Construct Query for simple search - * @param value Query value - * @param paramType Search type - * @param modifierOrPrefix Modifier or prefix - * @param searchParameterConf Search parameter configuration - * @return - */ - private def constructQueryForSimple(value:String, paramType:String, modifierOrPrefix:String, searchParameterConf:SearchParameterConf) = { - //For each possible path, construct queries - val queries = - searchParameterConf - .extractElementPathsTargetTypesAndRestrictions(withArrayIndicators = true) - .map { - case (path, targetType, Nil) => - SearchUtil - .typeHandlerFunction(paramType)(value, modifierOrPrefix, path, targetType, searchParameterConf.targets) - //If there is a restriction on the search we assume it is a direct field match e.g phone parameter on Patient - //e.g. f:PlanDefinition/f:relatedArtifact[f:type/@value='depends-on']/f:resource --> path = relatedArtifact[i].resource, restriction = @.type --> (relatedArtifact[i], resource, type) - //e.g. f:OrganizationAffiliation/f:telecom[system/@value='email'] --> path => telecom[i] , restriction = system --> (telecom[i], "", system) - case (path, targetType, restrictions) => - val pathParts = path.split('.').toIndexedSeq - val indexOfRestrictions = FHIRUtil.findIndexOfRestrictionsOnPath(pathParts, restrictions) - SearchUtil.queryWithRestrictions(pathParts, indexOfRestrictions, value, paramType, targetType, modifierOrPrefix, searchParameterConf.targets) + * Handles missing modifier + * + * @param pathList absolute path of the parameter + * @param bool boolean value (:missing= true | false) + * @return BsonDocument for the query + */ + def constructQueryForMissingModifier(pathList: Seq[String], bool: String): Bson = { + bool match { + case MISSING_MODIFIER_VALUES.STRING_TRUE => + //All paths should be missing + pathList.map(path => Filters.exists(path, exists = false)) match { + case Seq(single) => single + case multiple => Filters.and(multiple:_*) } - //OR the queries for multiple paths - if(queries.size > 1) or(queries:_*) else queries.head + case MISSING_MODIFIER_VALUES.STRING_FALSE => + //One of the path should exist + pathList.map(path => Filters.exists(path, exists = true)) match { + case Seq(single) => single + case multiple => Filters.or(multiple: _*) + } + case _ => + throw new InvalidParameterException("Invalid parameter value for :missing modifier, either true or false should be provided!") + } } + /** + * Construct MongoDB query for simple parameter + * @param parameter Parsed parameter details + * @param searchParameterConf Corresponding search parameter configuration + * @param path Path to the element to run the search + * @param targetType Data type of target element + * @return + */ + private def constructQueryForSimpleWithoutRestriction(parameter:Parameter, searchParameterConf:SearchParameterConf, path:String, targetType:String):Bson = { + parameter.paramType match { + case FHIR_PARAMETER_TYPES.NUMBER => + NumberQueryBuilder.getQuery(parameter.valuePrefixList, path, targetType) + case FHIR_PARAMETER_TYPES.QUANTITY => + QuantityQueryBuilder.getQuery(parameter.valuePrefixList, path, targetType) + case FHIR_PARAMETER_TYPES.DATE => + DateQueryBuilder.getQuery(parameter.valuePrefixList, path, targetType) + case FHIR_PARAMETER_TYPES.TOKEN => + TokenQueryBuilder.getQuery(parameter.valuePrefixList.map(_._2), parameter.suffix, path, targetType) + case FHIR_PARAMETER_TYPES.STRING => + StringQueryBuilder.getQuery(parameter.valuePrefixList.map(_._2), parameter.suffix, path, targetType) + case FHIR_PARAMETER_TYPES.URI => + UriQueryBuilder.getQuery(parameter.valuePrefixList.map(_._2), parameter.suffix, path, targetType) + case FHIR_PARAMETER_TYPES.REFERENCE => + getReferenceQueryBuilder() + .getQuery(parameter.valuePrefixList.map(_._2), parameter.suffix, path, targetType, searchParameterConf.targets) + } + } /** - * Construct query for Extension Category Search Parameters - * @param parameter Parsed search parameter - * @param searchParameterConf Configuration for the search parameter - * @return - */ - private def constructQueryForExtensionParameter(parameter:Parameter, searchParameterConf:SearchParameterConf):Bson = { - val queries = parameter - .valuePrefixList //Go over each value to OR them - .flatMap(pv => { - //Extension path defined for search - val paths = searchParameterConf.paths.asInstanceOf[Seq[Seq[(String, String)]]] - paths.map(eachPath => { - SearchUtil.extensionQuery(pv._2, eachPath, searchParameterConf.ptype, pv._1, searchParameterConf.targets) - }) - }) + * Construct MongoDB query for simple parameter with extra restrictions + * e.g. Search parameters on extensions + * e.g. Search parameters like phone on Patient type --> Target path : Patient.telecom.where(system='phone') + * @param parameter Parsed parameter details + * @param searchParameterConf Corresponding search parameter configuration + * @param pathParts Parsed path to the element to run the search + * @param targetType Data type of target element + * @param indexOfRestrictions Parsed and indexed restrictions e.g. 0 -> Seq(system -> phone) + * @return + */ + def constructQueryForSimpleWithRestrictions(parameter:Parameter, + searchParameterConf:SearchParameterConf, + pathParts:Seq[String], + targetType:String, + indexOfRestrictions:Seq[(Int, Seq[(String, String)])] + ): Bson = { - //OR the queries for multiple paths - if(queries.length > 1) or(queries:_*) else queries.head + //Get the next restriction in the path + val restriction = indexOfRestrictions.head + //Find the parent path to this element e.g. telecom[i] + val parentPath = pathParts.slice(0, restriction._1 + 1).mkString(".") + //Find child paths + val childPathParts = pathParts.drop(restriction._1 + 1) + //Split the parent path + val (elemMatchPath, queryPath) = FHIRUtil.splitElementPathIntoElemMatchAndQueryPaths(parentPath) + //If this is the last restriction, return the query by merging the actual query and query coming from restriction + if (indexOfRestrictions.tail.isEmpty) { + val childPath = childPathParts.mkString(".") + val mainQuery = + Filters.and( + ( + //Restriction queries e.g. extension.where(url='') + restriction._2.map(r => Filters.eq(FHIRUtil.mergeElementPath(queryPath, r._1), r._2)) :+ + //Actual query + constructQueryForSimpleWithoutRestriction(parameter, searchParameterConf, FHIRUtil.mergeElementPath(queryPath, childPath), targetType) + ): _* + ) + getFinalQuery(elemMatchPath, mainQuery) + } + //If there are still restrictions on child paths, continue applying restrictions + else { + val mainQuery = + and( + ( + //Restriction queries + restriction._2.map(r => Filters.eq(FHIRUtil.mergeElementPath(queryPath, r._1), r._2)) :+ + constructQueryForSimpleWithRestrictions(parameter, searchParameterConf, childPathParts, targetType, indexOfRestrictions.tail) + ): _* + ) + elemMatch(elemMatchPath.get, mainQuery) + } } - /** * Builds query for composite parameters wrt to provided * list of paths and parameter object @@ -180,11 +231,10 @@ object ResourceQueryBuilder { val queriesForCombParam = subpathsAfterCommonPathAndTargetTypes.map { case (path, spTargetType) => - //Run query for each path - SearchUtil - .typeHandlerFunction(compParamConf.ptype)(queryPartValue, queryPartPrefix, path, spTargetType, compParamConf.targets) //Construct query for each path + val childParam = Parameter(FHIR_PARAMETER_CATEGORIES.NORMAL, compParamConf.ptype, searchParamConf.pname, Seq(queryPartPrefix->queryPartValue), parameter.suffix) + constructQueryForSimpleWithoutRestriction(childParam, searchParamConf, path, spTargetType) } - if(queriesForCombParam.length > 1) or(queriesForCombParam:_*) else queriesForCombParam.head + orQueries(queriesForCombParam) } //Queries on all combined components should hold val mainQuery = and(queriesForEachCombParam:_*) @@ -197,7 +247,7 @@ object ResourceQueryBuilder { ) //OR the queries for multiple values and multiple common paths - if(queries.length > 1) or(queries:_*) else queries.head + orQueries(queries) } /** @@ -256,33 +306,26 @@ object ResourceQueryBuilder { * @return */ def constructQueryForRevInclude(revIncludeReferences:Seq[(String, Option[String], Option[String])], parameterConf: SearchParameterConf):Bson = { - val queries = { + val queries = parameterConf.targetTypes.head match { case FHIR_DATA_TYPES.REFERENCE => parameterConf.paths.map { case normalPath: String => - val queries = revIncludeReferences.map(revIncludeRef => - SearchUtil.typeHandlerFunction(FHIR_PARAMETER_TYPES.REFERENCE)(revIncludeRef._1, "", normalPath, FHIR_DATA_TYPES.REFERENCE, Nil) - ) - if(queries.length > 1) or(queries:_*) else queries.head + getReferenceQueryBuilder().getQuery(revIncludeReferences.map(_._1), "", normalPath, FHIR_DATA_TYPES.REFERENCE, parameterConf.targets) } case FHIR_DATA_TYPES.CANONICAL => parameterConf.paths.map { case normalPath: String => - val queries = + val canonicalRefs = revIncludeReferences .filter(r => r._2.isDefined) - .map(revIncludeRef => { - val canonicalQuery = revIncludeRef._2.get + revIncludeRef._3.map(v => s"|$v").getOrElse("") - SearchUtil.typeHandlerFunction(FHIR_PARAMETER_TYPES.REFERENCE)(canonicalQuery, "", normalPath, FHIR_DATA_TYPES.CANONICAL, Nil) - }) - if(queries.length > 1) or(queries:_*) else queries.head + .map(r => s"${r._2.get}${r._3.map(v => s"|$v").getOrElse("")}") + + ReferenceQueryBuilder.getQueryOnCanonicals(canonicalRefs, "", normalPath) } } - } - //Merge queries with or (for multiple paths parameters) - if(queries.size > 1) or(queries:_*) else queries.head + orQueries(queries) } /** @@ -291,16 +334,16 @@ object ResourceQueryBuilder { * @param resource Resource content * @return */ - def constructShardingQuery(rtype:String, resource: Resource):Option[Bson] = { + def constructShardingQuery(resource: Resource):Option[Bson] = { if(!OnfhirConfig.mongoShardingEnabled) None else fhirConfig.shardKeys - .get(rtype) //get shard key, if exist + .get(resourceConf.resource) //get shard key, if exist .flatMap(_.headOption) //Only take the first, as we support single shard key .filterNot(_ == FHIR_SEARCH_SPECIAL_PARAMETERS.ID) // if shard is on id, we don't need this query, id is already used .flatMap(shardParamName => - fhirConfig.getSupportedParameters(rtype).get(shardParamName)//Try to find the param configuration + fhirConfig.getSupportedParameters(resourceConf.resource).get(shardParamName)//Try to find the param configuration ) .flatMap(shardParam => shardParam.ptype match { @@ -316,7 +359,7 @@ object ResourceQueryBuilder { FHIRResponse.SEVERITY_CODES.FATAL, FHIRResponse.OUTCOME_CODES.INVALID, None, - Some(s"Collection for the resource type $rtype is sharded on path $elementPath! Therefore it is required, but the resource does not include the field! Please consult with the maintainer of the OnFhir repository."), + Some(s"Collection for the resource type ${resourceConf.resource} is sharded on path $elementPath! Therefore it is required, but the resource does not include the field! Please consult with the maintainer of the OnFhir repository."), Seq(elementPath) ) )) @@ -333,5 +376,14 @@ object ResourceQueryBuilder { } ) } + + /** + * Construct ReferenceQueryBuilder + * @return + */ + private def getReferenceQueryBuilder(): ReferenceQueryBuilder = { + new ReferenceQueryBuilder(onlyLocalReferences = resourceConf.referencePolicies.contains("local")) + } + } diff --git a/onfhir-core/src/main/scala/io/onfhir/db/SearchUtil.scala b/onfhir-core/src/main/scala/io/onfhir/db/SearchUtil.scala deleted file mode 100644 index 71396abe..00000000 --- a/onfhir-core/src/main/scala/io/onfhir/db/SearchUtil.scala +++ /dev/null @@ -1,562 +0,0 @@ -package io.onfhir.db - -import io.onfhir.api._ -import org.mongodb.scala.model.Filters -import org.mongodb.scala.model.Filters._ - -import io.onfhir.api.util.FHIRUtil -import io.onfhir.config.OnfhirConfig -import io.onfhir.exception.{InternalServerException, InvalidParameterException} -import org.mongodb.scala.bson.conversions.Bson - - -import scala.collection.immutable.HashMap - -/** - * Handles the searching according to type of Search Parameter - */ -object SearchUtil { - //Function map based on search parameter type -> (value, prefix, path, target FHIR type) -> BSON - val typeHandlerFunction = HashMap[String, (String, String, String, String, Seq[String]) => Bson] ( - FHIR_PARAMETER_TYPES.NUMBER -> numberQuery, - FHIR_PARAMETER_TYPES.DATE -> dateQuery, - FHIR_PARAMETER_TYPES.STRING -> stringQuery, - FHIR_PARAMETER_TYPES.URI -> uriQuery, - FHIR_PARAMETER_TYPES.TOKEN -> tokenQuery, - FHIR_PARAMETER_TYPES.QUANTITY -> quantityQuery, - FHIR_PARAMETER_TYPES.REFERENCE -> referenceQuery - ) - - /** - * Search based on numerical values and range - * - * @param number Query value - * @param prefix Prefix to be handled - * @param path Path to the target element to be queried - * @param targetType FHIR Type of the target element - * @return respective BsonDocument for target query - */ - private def numberQuery(number:String, prefix:String, path:String, targetType:String, targetReferences:Seq[String] = Nil):Bson = { - targetType match{ - case FHIR_DATA_TYPES.RANGE => - //Split the parts - val (elemMatchPath, queryPath) = FHIRUtil.splitElementPathIntoElemMatchAndQueryPaths(path) - //Main query on Range object - val mainQuery = PrefixModifierHandler.rangePrefixHandler(queryPath.getOrElse(""), number, prefix) - //If an array exist, use elemMatch otherwise return the query - elemMatchPath match { - case None => mainQuery - case Some(emp) => elemMatch(emp, mainQuery) - } - //Ad these are simple searches we don't need to handle arrays with elemMatch - case FHIR_DATA_TYPES.INTEGER => PrefixModifierHandler.intPrefixHandler(FHIRUtil.normalizeElementPath(path), number, prefix) - case FHIR_DATA_TYPES.DECIMAL => PrefixModifierHandler.decimalPrefixHandler(FHIRUtil.normalizeElementPath(path), number, prefix) - case other => throw new InternalServerException(s"Unknown target element type $other !!!") - } - } - - /** - * Date parameter searches on a date/time or period. As is usual for date/time related functionality - * has the following form; - * - * yyyy-mm-ddThh:mm:ss[Z|(+|-)hh:mm] (the standard XML format). - * - * Some parameters of date type could be defined as both period or regular date, therefore, a query - * that match each case is defined. - * - * @param date Query value of the date parameter - * @param prefix Prefix to be handled - * @param path Path to the target element to be queried - * @param targetType FHIR Type of the target element - * @return equivalent BsonDocument for the target query - */ - private def dateQuery(date:String, prefix:String, path:String, targetType:String, targetReferences:Seq[String] = Nil):Bson = { - //Split the parts - val (elemMatchPath, queryPath) = FHIRUtil.splitElementPathIntoElemMatchAndQueryPaths(path) - // '+' in time zone field is replaced with ' ' by the server - val dateValue = date.replace(" ", "+") - // Construct main query on date object - val mainQuery = targetType match { - case FHIR_DATA_TYPES.DATE | - FHIR_DATA_TYPES.DATETIME | - FHIR_DATA_TYPES.INSTANT => - PrefixModifierHandler.dateRangePrefixHandler(queryPath.getOrElse(""), dateValue, prefix) - case FHIR_DATA_TYPES.PERIOD => - PrefixModifierHandler.periodPrefixHandler(queryPath.getOrElse(""), dateValue, prefix, isTiming = false) - case FHIR_DATA_TYPES.TIMING => - //TODO Handle event case better by special query on array (should match for all elements, not or) - Filters.or( - PrefixModifierHandler.timingEventHandler(queryPath.getOrElse(""), dateValue, prefix), - //PrefixModifierHandler.dateRangePrefixHandler(FHIRUtil.mergeElementPath(queryPath, "event"), dateValue, prefix), - PrefixModifierHandler.periodPrefixHandler(queryPath.getOrElse(""), dateValue, prefix, isTiming = true) - ) - case other => - throw new InternalServerException(s"Unknown target element type $other !!!") - } - //If an array exist, use elemMatch otherwise return the query - elemMatchPath match { - case None => mainQuery - case Some(emp) => elemMatch(emp, mainQuery) - } - } - - /** - * String parameter serves as the input for a case- and accent-insensitive search against sequences of - * characters, :exact modifier is used for case and accent sensitive search, :contains modifier is - * used for case and accent insensitive partial matching search. - * @param value Query value of the string parameter - * @param modifier Prefix to be handled - * @param path Path to the target element to be queried - * @param targetType FHIR Type of the target element - * @param targetReferences - * @return - */ - private def stringQuery(value:String, modifier:String, path:String, targetType:String, targetReferences:Seq[String] = Nil):Bson = { - targetType match { - case FHIR_DATA_TYPES.STRING => - PrefixModifierHandler.stringModifierHandler(FHIRUtil.normalizeElementPath(path), value, modifier) - case FHIR_DATA_TYPES.HUMAN_NAME | FHIR_DATA_TYPES.ADDRESS => - //Split the parts - val (elemMatchPath, queryPath) = FHIRUtil.splitElementPathIntoElemMatchAndQueryPaths(path) - val queries = - STRING_TYPE_SEARCH_SUBPATHS(targetType) - .map(subpath => FHIRUtil.mergeElementPath(queryPath, subpath)) - .map(p => PrefixModifierHandler.stringModifierHandler(p, value, modifier)) - val mainQuery = or(queries:_*) - //If an array exist, use elemMatch otherwise return the query - elemMatchPath match { - case None => mainQuery - case Some(emp) => elemMatch(emp, mainQuery) - } - case other => - throw new InvalidParameterException(s"String type search is not supported on target type $other!") - } - } - - /** - * The uri parameter refers to a URI (RFC 3986) element. Matches are precise (e.g. case, accent, and escape) - * sensitive, and the entire URI must match. The modifier :above or :below can be used to indicate that - * partial matching is used. - * - * @param uri value of the uri parameter - * @param modifier Modifier to be handled - * @param path Path to the target element to be queried - * @param targetType FHIR Type of the target element - * @return equivalent BsonDocument for the target query - */ - private def uriQuery(uri:String, modifier:String, path:String, targetType:String, targetReferences:Seq[String] = Nil):Bson = { - PrefixModifierHandler.uriModifierHandler(FHIRUtil.normalizeElementPath(path), uri, modifier) - } - - /** - * A token type is a parameter that searches on a URI/value pair. It is used against a code or identifier - * data type where the value may have a URI that scopes its meaning. The search is performed against the - * pair from a Coding or an Identifier. The syntax for the value is one of the following: - - * [parameter]=[code]: the value of [code] matches a Coding.code or Identifier.value - * irrespective of the value of the system property - * - * [parameter]=[system]|[code]: the value of [code] matches a Coding.code or Identifier.value, - * and the value of [system] matches the system property of the Identifier or Coding - * - * [parameter]=|[code]: the value of [code] matches a Coding.code or Identifier.value, and the - * Coding/Identifier has no system property - * - * e.g. GET [base]/Condition?code=http://acme.org/conditions/codes|ha125 - * e.g GET [base]/Patient?gender:not=male - * e.g. GET [base]/Condition?code:text=headache - * - * // TODO Missing Modifiers Requires Value-Sets (above, below, in, not-in) - * - * @param token The token part of the query - * @param modifier The modifier part of the query - * @param path The path to the element for the corresponding query parameter - * e.g. One of the path for 'value-date' search parameter in Observation is 'valueDateTime' - * @param targetType Type of the target element that path goes to e.g. 'dateTime', 'Period' - * @return corresponding MongoDB Query for single path of the query - */ - private def tokenQuery(token:String, modifier:String, path:String, targetType:String, targetReferences:Seq[String] = Nil):Bson = { - targetType match { - //Simple types, only one field to match (so we don't need to evaluate elemMatch options) - case FHIR_DATA_TYPES.STRING | FHIR_DATA_TYPES.ID | FHIR_DATA_TYPES.URI | FHIR_DATA_TYPES.CODE => - modifier match { - case FHIR_PREFIXES_MODIFIERS.NOT => Filters.ne(FHIRUtil.normalizeElementPath(path), token) - case _ => Filters.eq(FHIRUtil.normalizeElementPath(path), token) - } - case FHIR_DATA_TYPES.BOOLEAN => - PrefixModifierHandler.tokenBooleanModifierHandler(FHIRUtil.normalizeElementPath(path), token, modifier) - //A special modifier case - case FHIR_DATA_TYPES.IDENTIFIER if modifier == FHIR_PREFIXES_MODIFIERS.OF_TYPE => - val (typeSystem, typeCode, value) = FHIRUtil.parseTokenOfTypeValue(token) - //Split the parts - val (elemMatchPath, queryPath) = FHIRUtil.splitElementPathIntoElemMatchAndQueryPaths(path) - //Construct main query, it both checks the Identifier.type.coding and Identifier.value - val mainQuery = - and( - elemMatch( - FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.TYPE}.${FHIR_COMMON_FIELDS.CODING}"), //Path for type codes in Identifier - PrefixModifierHandler.tokenModifierHandler(FHIR_COMMON_FIELDS.SYSTEM, FHIR_COMMON_FIELDS.CODE, Some(typeSystem), Some(typeCode), modifier="") - ), - Filters.eq( - FHIRUtil.mergeElementPath(queryPath, FHIR_COMMON_FIELDS.VALUE), - value) - ) - //If an array exist, use elemMatch otherwise return the query - elemMatchPath match { - case None => mainQuery - case Some(emp) => elemMatch(emp, mainQuery) - } - //For the complex types, we should consider array elements within the path - case _ => - modifier match { - case FHIR_PREFIXES_MODIFIERS.TEXT => - TOKEN_TYPE_SEARCH_DISPLAY_PATHS.get(targetType) match { - case None => throw new InvalidParameterException(s"Modifier _text cannot be used on $targetType data type for token type pearameters!") - case Some(textFields) => - val queries = textFields.map(textField => - PrefixModifierHandler.handleTokenTextModifier(FHIRUtil.normalizeElementPath(FHIRUtil.mergeElementPath(path, textField)), token) - ) - if(queries.length > 1) or(queries:_*) else queries.head - } - //Otherwise (no modifier, or any other) - case _ => - var complextElementPath = path - //2 level complex type, so add path for the the last complex type - if(targetType == FHIR_DATA_TYPES.CODEABLE_CONCEPT) - complextElementPath = FHIRUtil.mergeElementPath(complextElementPath,s"${FHIR_COMMON_FIELDS.CODING}[i]") //coding is array, so we add [i] - //Find out the elemMatch and query parts of the path - val (elemMatchPath, queryPath) = FHIRUtil.splitElementPathIntoElemMatchAndQueryPaths(complextElementPath) - // Based on the target type, decide on system, code and text fields - val (systemField, codeField) = TOKEN_TYPE_SEARCH_SUBPATHS(targetType) - - // Extract system and code parts from query value - val (system, code) = FHIRUtil.parseTokenValue(token) - //Resolve main query on the fields - val mainQuery = - PrefixModifierHandler - .tokenModifierHandler(FHIRUtil.mergeElementPath(queryPath, systemField), FHIRUtil.mergeElementPath(queryPath,codeField), system, code, modifier) - - //If an array exist, use elemMatch otherwise return the query - var finalQuery = elemMatchPath match { - case None => mainQuery - case Some(emp) => elemMatch(emp, mainQuery) - } - if(modifier == FHIR_PREFIXES_MODIFIERS.NOT) - finalQuery = not(finalQuery) - finalQuery - } - } - } - - /** - * A quantity parameter searches on the Quantity data type. The syntax for the - * value follows the form: - * - * [parameter]=[prefix][number]|[system]|[code] matches a quantity with the given unit - * - * @param quantity quantity parameter - * @param prefix prefix for the quantity parameter - * @param path Path for the element - * @param targetType - * @param targetReferences - * @return - */ - private def quantityQuery(quantity:String, prefix:String, path:String, targetType:String, targetReferences:Seq[String] = Nil):Bson = { - //Parse the given value - val (value,system, code) = FHIRUtil.parseQuantityValue(quantity) - - //Find out the elemMatch and query parts of the path - val (elemMatchPath, queryPath) = FHIRUtil.splitElementPathIntoElemMatchAndQueryPaths(path) - - //Try to construct main query - val mainQuery = targetType match { - case FHIR_DATA_TYPES.QUANTITY | - FHIR_DATA_TYPES.SIMPLE_QUANTITY | - FHIR_DATA_TYPES.MONEY_QUANTITY | - FHIR_DATA_TYPES.AGE | - FHIR_DATA_TYPES.DISTANCE | - FHIR_DATA_TYPES.COUNT | - FHIR_DATA_TYPES.DURATION => - //Query on the quentity - val valueQuery = PrefixModifierHandler.decimalPrefixHandler(FHIRUtil.mergeElementPath(queryPath, FHIR_COMMON_FIELDS.VALUE), value, prefix) - //Also merge it with query on system and code - val sysCodeQuery = unitSystemCodeQuery(system, code, queryPath, FHIR_COMMON_FIELDS.SYSTEM, FHIR_COMMON_FIELDS.CODE, FHIR_COMMON_FIELDS.UNIT) - sysCodeQuery.map(sq => and(valueQuery, sq)).getOrElse(valueQuery) - - case FHIR_DATA_TYPES.MONEY => - //Query on the quatity - val valueQuery = PrefixModifierHandler.decimalPrefixHandler(FHIRUtil.mergeElementPath(queryPath, FHIR_COMMON_FIELDS.VALUE), value, prefix) - //Also merge it with query on currency code - val sysCodeQuery = code.map(c => Filters.eq(FHIRUtil.mergeElementPath(queryPath, FHIR_COMMON_FIELDS.CURRENCY), c)) - sysCodeQuery.map(sq => and(valueQuery, sq)).getOrElse(valueQuery) - - //Handle range - case FHIR_DATA_TYPES.RANGE => - //Query on range values - val valueQuery = PrefixModifierHandler.rangePrefixHandler(queryPath.getOrElse(""), value, prefix) - val lowPath = FHIRUtil.mergeElementPath(queryPath, FHIR_COMMON_FIELDS.LOW) - val highPath =FHIRUtil.mergeElementPath(queryPath, FHIR_COMMON_FIELDS.HIGH) - - val lq = unitSystemCodeQuery(system, code, Some(lowPath), FHIR_COMMON_FIELDS.SYSTEM, FHIR_COMMON_FIELDS.CODE, FHIR_COMMON_FIELDS.UNIT).map(sq => or(exists(lowPath, exists = false), sq)) - val hq = unitSystemCodeQuery(system, code, Some(highPath), FHIR_COMMON_FIELDS.SYSTEM, FHIR_COMMON_FIELDS.CODE, FHIR_COMMON_FIELDS.UNIT).map(sq => or(exists(highPath, exists = false), sq)) - //Merge all (both lq and hq should be SOME or NONE - lq.map(and(_, hq.get, valueQuery)).getOrElse(valueQuery) - - case FHIR_DATA_TYPES.SAMPLED_DATA => - //For SampleData, we should check for lowerLimit and upperLimit like a range query - val valueQuery = PrefixModifierHandler.rangePrefixHandler(queryPath.getOrElse(""), value, prefix, isSampleData = true) - val sysCodeQuery = unitSystemCodeQuery(system, code, queryPath, - systemPath = s"${FHIR_COMMON_FIELDS.ORIGIN}.${FHIR_COMMON_FIELDS.SYSTEM}", - codePath=s"${FHIR_COMMON_FIELDS.ORIGIN}.${FHIR_COMMON_FIELDS.CODE}", - unitPath= s"${FHIR_COMMON_FIELDS.ORIGIN}.${FHIR_COMMON_FIELDS.UNIT}") - sysCodeQuery.map(sq => and(valueQuery, sq)).getOrElse(valueQuery) - } - - //If an array exist, use elemMatch otherwise return the query - elemMatchPath match { - case None => mainQuery - case Some(emp) => elemMatch(emp, mainQuery) - } - } - - /** - * Merge the query ont the Quantity value with system and code restrictions - * @param system Expected system - * @param code Expected code/unit - * @param queryPath Main path to the FHIR quantity element - * @param systemPath system field path within the element - * @param codePath code field path within the element - * @param unitPath unit field path within the element - * @return - */ - private def unitSystemCodeQuery(system:Option[String], code:Option[String], queryPath:Option[String], systemPath:String, codePath:String, unitPath:String):Option[Bson] = { - (system, code) match { - //Only query on value - case (None, None) => - None - //Query on value + unit - case (None, Some(c)) => - Some( - or( - Filters.eq(FHIRUtil.mergeElementPath(queryPath, codePath), c), - Filters.eq(FHIRUtil.mergeElementPath(queryPath, unitPath), c) - ) - ) - //query on value + system + code - case (Some(s), Some(c)) => - Some( - and( - Filters.eq(FHIRUtil.mergeElementPath(queryPath, codePath), c), - Filters.eq(FHIRUtil.mergeElementPath(queryPath, systemPath), s) - ) - ) - case _ => throw new InternalServerException("Invalid state!") - } - } - - /** - * A reference parameter refers to references between resources. The interpretation of a reference - * parameter is either: - * - * [parameter]=[id] the logical [id] of a resource using a local reference (i.e. a relative reference) - * - * [parameter]=[type]/[id] the logical [id] of a resource of a specified type using - * a local reference (i.e. a relative reference), for when the reference can point to different - * types of resources (e.g. Observation.subject) - * - * [parameter]=[url] where the [url] is an absolute URL - a reference to a resource by its absolute location - * - * @param reference a reference to be handled - * @param modifier type of the reference(e.g. :type modifier) - * @param path path to the element - * @param targetType - * @param targetReferenceTypes Target reference types - * @return equivalent BsonDocument for the target query - */ - private def referenceQuery(reference:String, modifier:String, path:String, targetType:String, targetReferenceTypes:Seq[String]):Bson = { - targetType match { - //If this is a search on a FHIR Reference type element - case FHIR_DATA_TYPES.REFERENCE => - //Find out the elemMatch and query parts of the path - val (elemMatchPath, queryPath) = FHIRUtil.splitElementPathIntoElemMatchAndQueryPaths(path) - //Parse reference value (URL part, resource type, resource id, version) - val (url, rtype, rid, version) = FHIRUtil.resolveReferenceValue(reference, modifier, targetReferenceTypes) - - val mainQuery = modifier match { - //If modifier is identifier, search like a token on identifier element (Reference.identifier) - case FHIR_PREFIXES_MODIFIERS.IDENTIFIER => - tokenQuery(rid, modifier = "", FHIRUtil.mergeElementPath(queryPath, FHIR_COMMON_FIELDS.IDENTIFIER), FHIR_DATA_TYPES.IDENTIFIER) - //Query only on resource type to refer - case FHIR_PREFIXES_MODIFIERS.TYPE => - Filters.eq( - FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_RESOURCE_TYPE}"), - rid) - case FHIR_PREFIXES_MODIFIERS.NOT => - //negate all the queries - var baseQueries:Seq[Bson] = Seq( - Filters.ne(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_RESOURCE_ID}"), rid), - Filters.ne(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_RESOURCE_TYPE}"), rtype), - url match { - // If no url is given or it is our root url - case None | Some(OnfhirConfig.fhirRootUrl) => - Filters.and( - Filters.exists(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_URL}"), true), - Filters.ne(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_URL}"), OnfhirConfig.fhirRootUrl) - ) - //If given any other, it should match - case Some(other) => - Filters.or( - Filters.exists(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_URL}"), false), - Filters.ne(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_URL}"), other) - ) - } - ) - - //Add version specific query if given - baseQueries = - baseQueries ++ - version.map(v => Filters.ne(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_RESOURCE_VERSION}"), v)).toSeq - - or(baseQueries:_*) - //No modifier - case _ => - //Base queries for reference on resource type and id - var baseQueries:Seq[Bson] = Seq( - Filters.eq(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_RESOURCE_ID}"), rid), - Filters.eq(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_RESOURCE_TYPE}"), rtype), - url match { - // If no url is given or it is our root url - case None | Some(OnfhirConfig.fhirRootUrl) => - Filters.or( - Filters.eq(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_URL}"), OnfhirConfig.fhirRootUrl), //Either it should equal to our root URL - Filters.exists(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_URL}"), false) //Or url part does not exist - ) - //If given any other, it should match - case Some(other) => - Filters.eq(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_URL}"), other) - } - ) - //Add version specific query if given - baseQueries = - baseQueries ++ - version.map(v => Filters.eq(FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.REFERENCE}.${FHIR_EXTRA_FIELDS.REFERENCE_RESOURCE_VERSION}"), v)).toSeq - //And all queries - and(baseQueries:_*) - } - //If an array exist, use elemMatch otherwise return the query - elemMatchPath match { - case None => mainQuery - case Some(emp) => elemMatch(emp, mainQuery) - } - - //If this is a search on Canonical references - case FHIR_DATA_TYPES.CANONICAL => - //As for canonical, we only look at one field we don't need to care arrays in the path - modifier match { - case FHIR_PREFIXES_MODIFIERS.BELOW => - val (canonicalUrl, canonicalVersion) = FHIRUtil.parseCanonicalValue(reference) - // Escape characters for to have valid regular expression - val regularExpressionValue = FHIRUtil.escapeCharacters(canonicalUrl) + canonicalVersion.map(v => s"\\|$v(\\.[0-9]*)+").getOrElse("") - // Match at the beginning of the uri - regex(FHIRUtil.normalizeElementPath(path), "\\A" + regularExpressionValue + "$") - case _ => - val (canonicalUrl, canonicalVersion) = FHIRUtil.parseCanonicalValue(reference) - canonicalVersion match{ - case None => //Otherwise should match any version - regex(FHIRUtil.normalizeElementPath(path), "\\A" + FHIRUtil.escapeCharacters(canonicalUrl) + "(\\|[0-9]+(\\.[0-9]*)*)?$") - case Some(_) => // Exact match if version exist - Filters.eq(FHIRUtil.normalizeElementPath(path), reference) - } - - } - } - } - - /** - * - * @param pathParts - * @param restrictionsWithIndexes - * @param value - * @param paramType - * @param targetType - * @param modifierOrPrefix - * @param targetReferences - * @return - */ - def queryWithRestrictions(pathParts:Seq[String], restrictionsWithIndexes:Seq[(Int, Seq[(String,String)])], value:String, paramType:String, targetType:String, modifierOrPrefix:String, targetReferences:Seq[String]):Bson = { - val restriction = restrictionsWithIndexes.head - val parentPath = pathParts.slice(0, restriction._1 + 1).mkString(".") - //Find childpaths - val childPaths = pathParts.drop(restriction._1 + 1) - //Split the parent path - val (elemMatchPath, queryPath) = FHIRUtil.splitElementPathIntoElemMatchAndQueryPaths(parentPath) - - if(restrictionsWithIndexes.tail.isEmpty){ - val childPath = childPaths.mkString(".") - val mainQuery = - and( - ( - //Restriction queries - restriction._2.map(r => Filters.eq(FHIRUtil.mergeElementPath(queryPath, r._1), r._2)) :+ - //Actual query - SearchUtil - .typeHandlerFunction(paramType)(value, modifierOrPrefix, FHIRUtil.mergeElementPath(queryPath, childPath), targetType, targetReferences), - ):_* - ) - //Apply elem match - elemMatchPath match { - case None => mainQuery - case Some(emp) => elemMatch(emp, mainQuery) - } - } - //If there are still restrictions on child paths - else { - val mainQuery = - and( - ( - //Restriction queries - restriction._2.map(r => Filters.eq(FHIRUtil.mergeElementPath(queryPath, r._1), r._2)) :+ - queryWithRestrictions(childPaths,restrictionsWithIndexes.tail, value, paramType, targetType, modifierOrPrefix, targetReferences) - ):_* - ) - elemMatch(elemMatchPath.get, mainQuery) - } - } - - - def extensionQuery(value:String, path:Seq[(String,String)], paramType:String, prefixOrModifier:String, targetReferences:Seq[String]) = { - //The last of the Seq is the path for the value element (remove the extensions as we will use it as inner query) - val valueElementPath = path.last._1.replaceAll("extension[i].", "") - //Extension elements are like valueCodeableConcept, valueCoding, etc. So remove it to find the type - val targetType = valueElementPath.replace("value","") - - //Query on the extension element - val mainQuery = SearchUtil.typeHandlerFunction(paramType)(value, prefixOrModifier, valueElementPath, targetType, targetReferences) - //Extension URL list - val uriList = path.dropRight(1) - val query = uriList.foldRight(mainQuery)( (url, query) => { - elemMatch(FHIR_COMMON_FIELDS.EXTENSION, and(equal(FHIR_COMMON_FIELDS.URL, url._2), query)) - }) - - query - } - - /** - * Construct the query to search resources with canonical urls and versions - * @param urlAndVersions List of url and versions - * @return - */ - def canonicalRefQuery(urlAndVersions:Seq[(String, Option[String])]):Bson = { - if(urlAndVersions.forall(_._2.isEmpty)) - in(FHIR_COMMON_FIELDS.URL, urlAndVersions.map(_._1).distinct:_*) - else { - urlAndVersions.map { - case (url, None) => Filters.eq(FHIR_COMMON_FIELDS.URL, url) - case (url, Some(v)) => and(Filters.eq(FHIR_COMMON_FIELDS.URL, url), Filters.eq(FHIR_COMMON_FIELDS.VERSION, v)) - } match { - case Seq(s) => s - case oth => or(oth:_*) - } - } - } -} diff --git a/onfhir-core/src/main/scala/io/onfhir/db/StringQueryBuilder.scala b/onfhir-core/src/main/scala/io/onfhir/db/StringQueryBuilder.scala new file mode 100644 index 00000000..70852f5c --- /dev/null +++ b/onfhir-core/src/main/scala/io/onfhir/db/StringQueryBuilder.scala @@ -0,0 +1,88 @@ +package io.onfhir.db + +import io.onfhir.api.{FHIR_DATA_TYPES, FHIR_PREFIXES_MODIFIERS, STRING_TYPE_SEARCH_SUBPATHS} +import io.onfhir.api.util.FHIRUtil +import io.onfhir.exception.InvalidParameterException +import org.mongodb.scala.bson.conversions.Bson +import org.mongodb.scala.model.Filters +import org.mongodb.scala.model.Filters.{elemMatch, equal, or, regex} + +object StringQueryBuilder extends IFhirQueryBuilder { + + /** + * String parameter serves as the input for a case- and accent-insensitive search against sequences of + * characters, :exact modifier is used for case and accent sensitive search, :contains modifier is + * used for case and accent insensitive partial matching search. + * + * @param value Query value of the string parameter + * @param modifier Prefix to be handled + * @param path Path to the target element to be queried + * @param targetType FHIR Type of the target element + * @return + */ + def getQuery(values:Seq[String], modifier:String, path:String, targetType:String):Bson = { + targetType match { + case FHIR_DATA_TYPES.STRING => + orQueries(values.map(value => getQueryOnTargetStringElement(FHIRUtil.normalizeElementPath(path), value, modifier))) + + // Only we support these, Normally, FHIR states that string search can be done on any complex type by searching text fields of that complex type TODO + case FHIR_DATA_TYPES.HUMAN_NAME | FHIR_DATA_TYPES.ADDRESS => + orQueries(values.map(value => getQueryOnComplexTarget(path, value, modifier, targetType))) + + case other => + throw new InvalidParameterException(s"String type search is not supported on target type $other!") + } + } + + /** + * + * @param path + * @param value + * @param modifier + * @param targetType + * @return + */ + private def getQueryOnComplexTarget(path: String, value: String, modifier: String, targetType:String):Bson = { + //Split the parts + val (elemMatchPath, queryPath) = FHIRUtil.splitElementPathIntoElemMatchAndQueryPaths(path) + val queries = + STRING_TYPE_SEARCH_SUBPATHS(targetType) + .map(subpath => FHIRUtil.mergeElementPath(queryPath, subpath)) + .map(p => getQueryOnTargetStringElement(p, value, modifier)) + val mainQuery = or(queries: _*) + //If an array exist, use elemMatch otherwise return the query + elemMatchPath match { + case None => mainQuery + case Some(emp) => elemMatch(emp, mainQuery) + } + } + + /** + * Construct query for string search on string type element + * @param path Normalized path to the element + * @param value + * @param modifier + * @return + */ + def getQueryOnTargetStringElement(path: String, value: String, modifier: String): Bson = { + //TODO Ignorance of accents or other diacritical marks, punctuation and non-significant whitespace is not supported yet + // Escape characters for to have valid regular expression + val regularExpressionValue = FHIRUtil.escapeCharacters(value) + // Generator for regular expression queries(Only regex fields empty) + val caseInsensitiveStringRegex = regex(path, _: String, "i") + modifier match { + case FHIR_PREFIXES_MODIFIERS.EXACT => + // Exact match provided with $eq mongo operator + Filters.eq(path, value) + case FHIR_PREFIXES_MODIFIERS.CONTAINS => + // Partial match + caseInsensitiveStringRegex(".*" + regularExpressionValue + ".*") + //No modifier + case "" => + // Case insensitive, partial matches at the beginning (prefix search) + caseInsensitiveStringRegex("^" + regularExpressionValue) + case other => + throw new InvalidParameterException(s"Modifier $other is not supported for FHIR string queries!") + } + } +} diff --git a/onfhir-core/src/main/scala/io/onfhir/db/TokenQueryBuilder.scala b/onfhir-core/src/main/scala/io/onfhir/db/TokenQueryBuilder.scala new file mode 100644 index 00000000..7662f765 --- /dev/null +++ b/onfhir-core/src/main/scala/io/onfhir/db/TokenQueryBuilder.scala @@ -0,0 +1,446 @@ +package io.onfhir.db + +import io.onfhir.api.util.FHIRUtil +import io.onfhir.api.{COMMON_SYNTACTICALLY_HIERARCHICAL_CODE_SYSTEMS, FHIR_COMMON_FIELDS, FHIR_DATA_TYPES, FHIR_PREFIXES_MODIFIERS, TOKEN_TYPE_SEARCH_DISPLAY_PATHS, TOKEN_TYPE_SEARCH_SUBPATHS} +import io.onfhir.config.FhirConfigurationManager +import io.onfhir.exception.{InvalidParameterException, UnsupportedParameterException} +import org.mongodb.scala.bson.conversions.Bson +import org.mongodb.scala.model.Filters + +import scala.util.Try + +/** + * Provide utility method for query construction for FHIR token type parameters for a given target path and type + * e.g. code=http://loing.org|1235-4 + * e.g. code:in=http://my.org/fhir/ValueSet/myValueSet + * + * + * Supporting the following modifiers; + * - :text --> Checking if the display parts of element (e.g. CodeableConcept.text, CodeableConcept.coding.display, etc) starts with the given text (case insensitive) + * - :in --> Checking if the code is listed in given ValueSet (only canonical urls are supported, ***literal references to ValueSets should also be supported***) TODO + * ValueSet should be given in the configuration and should enumerate the codes. The ones that define the valueset with rules are not supported yet TODO + * - :not-in --> Checking if the code is not listed in given ValueSet. Same restrictions apply + * - :below --> Checking if the code is child of given concept (Only supported for CodeSystems that syntactically has this child relationship by using start with query e.g. ATC codes, ICD-10 codes) + * + * + */ +object TokenQueryBuilder extends IFhirQueryBuilder { + + /** + * Convert a token type search parameter into MongoDB query on the given path and data type + * + * [parameter]=[code]: the value of [code] matches a Coding.code or Identifier.value + * irrespective of the value of the system property + * + * [parameter]=[system]|[code]: the value of [code] matches a Coding.code or Identifier.value, + * and the value of [system] matches the system property of the Identifier or Coding + * + * [parameter]=|[code]: the value of [code] matches a Coding.code or Identifier.value, and the + * Coding/Identifier has no system property + * + * [parameter]=[system]|: any element where the value of [system] matches the system property of the Identifier or Coding + * + * e.g. GET [base]/Condition?code=http://acme.org/conditions/codes|ha125 + * e.g GET [base]/Patient?gender:not=male + * e.g. GET [base]/Condition?code:text=headache + * + * + * @param values Values supplied in search for the token type search parameter + * e.g. http://loinc.org|85354-9 + * @param modifier Modifier used e.g. :in, :above, :not + * @param path The path to the target element for the corresponding query parameter + * e.g. One of the path for 'value-date' search parameter in Observation is 'valueDateTime' + * @param targetType Type of the target element that path goes to e.g. 'dateTime', 'Period' + * @return Bson representation of MongoDB statement + */ + def getQuery(values:Seq[String], modifier:String, path:String, targetType:String):Bson = { + targetType match { + //Simple types, only one field to match (so we don't need to evaluate elemMatch options) + case FHIR_DATA_TYPES.STRING | FHIR_DATA_TYPES.ID | FHIR_DATA_TYPES.URI | FHIR_DATA_TYPES.CODE => + getQueryForSimpleTypeTarget(values, modifier, path) + case FHIR_DATA_TYPES.BOOLEAN => + getQueryForBooleanTarget(values, modifier, path) + //A special modifier case + case FHIR_DATA_TYPES.IDENTIFIER if modifier == FHIR_PREFIXES_MODIFIERS.OF_TYPE => + getQueryForOfTypeModifierOnIdentifier(values, path) + //For the complex types (e.g. CodeableConcept, Coding, ...), we should consider array elements within the path + case _ => + getQueryForComplexTypeTarget(values, modifier, path, targetType) + } + } + + /** + * Construct token search on a complex type + * @param values Values supplied in search for the token type search parameter + * e.g. http://loinc.org|85354-9 + * @param modifier Modifier used e.g. :in, :above, :not + * @param path The path to the target element for the corresponding query parameter + * e.g. One of the path for 'value-date' search parameter in Observation is 'valueDateTime' + * @param targetType Type of the target element that path goes to e.g. CodeableConcept + * @return + */ + private def getQueryForComplexTypeTarget(values:Seq[String], modifier:String, path:String, targetType:String):Bson = { + modifier match { + case FHIR_PREFIXES_MODIFIERS.TEXT => + //Find the paths of textual elements for target type e.g. CodeableConcept.text, CodeableConcept.coding.display + TOKEN_TYPE_SEARCH_DISPLAY_PATHS + .get(targetType) match { + case None => throw new InvalidParameterException(s"Modifier _text cannot be used on $targetType data type for token type pearameters!") + case Some(textFields) => + val queries = + textFields + .flatMap(textField => + values + .map(value => + StringQueryBuilder.getQueryOnTargetStringElement(FHIRUtil.normalizeElementPath(FHIRUtil.mergeElementPath(path, textField)), value, "") + ) + ) + orQueries(queries) + } + //Modifier :sw --> Codes start with given prefix, Modifier :nsw --> Codes not start with given prefix + // Note: Negation part (nsw) is handled on the upstream while merging queries for different paths + case FHIR_PREFIXES_MODIFIERS.STARTS_WITH | FHIR_PREFIXES_MODIFIERS.NOT_STARTS_WITH => + val (elemMatchPath, systemPath, codePath) = getElemMatchSystemAndCodePaths(path, targetType) + val parsedSystemAndCodes = values.map(FHIRUtil.parseTokenValue) + val mainQuery = orQueries(parsedSystemAndCodes.map(systemAndCode => getQueryForStartsWithModifier(systemPath, codePath, systemAndCode._1, systemAndCode._2))) + if (modifier == FHIR_PREFIXES_MODIFIERS.NOT_STARTS_WITH) + Filters.not(getFinalQuery(elemMatchPath, mainQuery)) + else + getFinalQuery(elemMatchPath, mainQuery) + //Modifier :in and :not-in (negation part is handled on the upstream while merging queries for different paths) + case FHIR_PREFIXES_MODIFIERS.IN | FHIR_PREFIXES_MODIFIERS.NOT_IN => + if(values.length != 1) + throw new InvalidParameterException(s"Invalid usage of parameter. A canonical or literal ValueSet reference should be provided in parameter value for ${FHIR_PREFIXES_MODIFIERS.IN} or ${FHIR_PREFIXES_MODIFIERS.NOT_IN} modifiers !!!") + val (elemMatchPath, systemPath, codePath) = getElemMatchSystemAndCodePaths(path, targetType) + val mainQuery = getQueryForInModifier(systemPath, codePath, values.head) + if (modifier == FHIR_PREFIXES_MODIFIERS.NOT_IN) + Filters.not(getFinalQuery(elemMatchPath, mainQuery)) + else + getFinalQuery(elemMatchPath, mainQuery) + //Modifier :below + case FHIR_PREFIXES_MODIFIERS.BELOW => + val (elemMatchPath, systemPath, codePath) = getElemMatchSystemAndCodePaths(path, targetType) + val parsedSystemAndCodes = values.map(FHIRUtil.parseTokenValue) + //If code system syntactically + val mainQuery = + if(parsedSystemAndCodes.forall(sc => sc._1.exists(COMMON_SYNTACTICALLY_HIERARCHICAL_CODE_SYSTEMS.contains))){ + orQueries( + parsedSystemAndCodes.map { + case (system, code) => getQueryForStartsWithModifier(systemPath, codePath, system, code) + } + ) + } else { + //TODO Some other mechanism or through terminology service + throw new UnsupportedParameterException(s"Modifier $modifier is not supported by onFhir.io system for token type parameters!") + } + getFinalQuery(elemMatchPath, mainQuery) + //Modifier above + case FHIR_PREFIXES_MODIFIERS.ABOVE => + throw new UnsupportedParameterException(s"Modifier $modifier is not supported by onFhir.io system for token type parameters!") + //No modifier + case "" | FHIR_PREFIXES_MODIFIERS.NOT => + val (elemMatchPath, systemPath, codePath) = getElemMatchSystemAndCodePaths(path, targetType) + // Extract system and code parts from query value + val parsedSystemAndCodes = + values + .map(FHIRUtil.parseTokenValue) + .groupBy(_._1).map(g => g._1 -> g._2.map(_._2)) //Group by given system + .toSeq + .flatMap { + case (system, codes) => + codes.flatten match { + case Nil => Seq(system -> None) + case oth if codes.forall(_.nonEmpty) => Seq(system -> Some(oth)) + case oth => Seq(system -> Some(oth), system -> None) + } + } + val mainQuery = + orQueries( + parsedSystemAndCodes + .map { + case (system, codes) => getTokenCodeSystemQueryForMultipleCodes(systemPath, codePath, system, codes) + } + ) + + if (modifier == FHIR_PREFIXES_MODIFIERS.NOT) + Filters.not( getFinalQuery(elemMatchPath, mainQuery)) + else + getFinalQuery(elemMatchPath, mainQuery) + //Any other modifier + case _ => + throw new InvalidParameterException(s"Modifier $modifier is not a valid modifier on $targetType elements !") + } + } + + /** + * Find out on elemMatch path (the last array element on the path), path to the system element (after elemMatch path), path to the code element (after elemMatch path) + * @param path Path to the target element + * @param targetType Target data type + * @return + */ + private def getElemMatchSystemAndCodePaths(path:String, targetType:String):(Option[String], String, String) = { + val complexElementPath = + //2 level complex type, so add path for the the last complex type + if (targetType == FHIR_DATA_TYPES.CODEABLE_CONCEPT) + FHIRUtil.mergeElementPath(path, s"${FHIR_COMMON_FIELDS.CODING}[i]") //coding is array, so we add [i] + else + path + //Find out the elemMatch and query parts of the path + val (elemMatchPath, queryPath) = FHIRUtil.splitElementPathIntoElemMatchAndQueryPaths(complexElementPath) + // Based on the target type, decide on system, code and text fields + val (systemField, codeField) = TOKEN_TYPE_SEARCH_SUBPATHS(targetType) + + (elemMatchPath, FHIRUtil.mergeElementPath(queryPath, systemField), FHIRUtil.mergeElementPath(queryPath, codeField)) + } + + /** + * Handle the :in and :not-in modifier for token search and construct query + * + * @param systemPath Path to the system element + * @param codePath Path to the code element + * @param vsUrl URL of the ValueSet to search in + * @return + */ + private def getQueryForInModifier(systemPath: String, codePath: String, vsUrl: String): Bson = { + Try(FHIRUtil.parseCanonicalReference(vsUrl)) + .toOption + .filter(vs => FhirConfigurationManager.fhirTerminologyValidator.isValueSetSupported(vs.getUrl(), vs.version)) match { + //If we resolve the ValueSet from canonical reference + case Some(vs) => + //All codes listed in ValueSet (system url -> list of codes) + val vsCodes: Seq[(String, Set[String])] = FhirConfigurationManager.fhirTerminologyValidator.getAllCodes(vs.getUrl(), vs.version).toSeq + + if(vsCodes.isEmpty) + throw new UnsupportedParameterException(s"ValueSet given with url '$vsUrl' by 'in' or 'not-in' modifier is empty!") + + val queriesForEachCodeSystem = vsCodes.map(vsc => Filters.and(Filters.eq(systemPath, vsc._1), Filters.in(codePath, vsc._2.toSeq: _*))) + /*modifier match { + case FHIR_PREFIXES_MODIFIERS.IN => + vsCodes.map(vsc => and(Filters.eq(systemPath, vsc._1), Filters.in(codePath, vsc._2.toSeq: _*))) + case FHIR_PREFIXES_MODIFIERS.NOT_IN => + vsCodes.map(vsc => or(Filters.ne(systemPath, vsc._1), Filters.nin(codePath, vsc._2.toSeq: _*))) + }*/ + orQueries(queriesForEachCodeSystem) + /*queriesForEachCodeSystem.length match { + case 0 => + case 1 => queriesForEachCodeSystem.head + case _ => if (modifier == FHIR_PREFIXES_MODIFIERS.IN) or(queriesForEachCodeSystem: _*) else and(queriesForEachCodeSystem: _*)}*/ + //We don't resolve the ValueSet + case None => + //TODO check if it is a literal reference and hande that + throw new UnsupportedParameterException(s"ValueSet url '$vsUrl' given with 'in' or 'not-in' modifier is not known!") + } + } + + + /** + * Construct query for a target simple type + * @param values Given values + * @param modifier Supplied modifier + * @param path Path to the target element + * @return + */ + private def getQueryForSimpleTypeTarget(values:Seq[String], modifier:String, path:String):Bson = { + modifier match { + //If no modifier + case "" => + values match { + //If a single value is given, check equality with the value in the path + case Seq(single) => Filters.eq(FHIRUtil.normalizeElementPath(path), single) + //If multiple values, check with in + case multiple => Filters.in(FHIRUtil.normalizeElementPath(path), multiple:_*) + } + //Not modifier + case FHIR_PREFIXES_MODIFIERS.NOT => + Filters.not( + values match { + //If a single value is given, check equality with the value in the path + case Seq(single) => Filters.eq(FHIRUtil.normalizeElementPath(path), single) + //If multiple values, check with in + case multiple => Filters.in(FHIRUtil.normalizeElementPath(path), multiple: _*) + } + ) + case FHIR_PREFIXES_MODIFIERS.STARTS_WITH | FHIR_PREFIXES_MODIFIERS.NOT_STARTS_WITH => + orQueries(values.map(prefix => getQueryForStartsWith(path, prefix))) + case other => + throw new InvalidParameterException(s"Modifier $other is not supported for FHIR token queries on a simple type!") + } + } + + + /** + * Handle FHIR token type queries on FHIR boolean values + * + * @param values Supplied values + * @param path Path of the target element + * @param modifier Search modifier + * @return + */ + def getQueryForBooleanTarget(values: Seq[String], modifier: String, path: String): Bson = { + if(modifier!="") + throw new InvalidParameterException(s"Modifier $modifier is not supported for FHIR token queries on FHIR boolean elements!") + + values match { + case Seq("true") => Filters.eq(path, true) + case Seq("false") => Filters.eq(path, false) + case _ => throw new InvalidParameterException(s"Invalid usage of parameter. Target element (with path $path) for search parameter is boolean, use either 'false' or 'true' for the parameter value!!!") + } + } + + /** + * Cosntruct the query for :ofType modifier on Identifiers + * @param values Supplied parameter values + * @param path Path to the Identifier element + * @return + */ + def getQueryForOfTypeModifierOnIdentifier(values: Seq[String], path: String):Bson = { + //Group supplied values into (system, code) -> values + val parsedValues: Seq[((String, String), Seq[String])] = + values + .map(FHIRUtil.parseTokenOfTypeValue) + .groupBy(v => (v._1, v._2)) + .map(g => g._1 -> g._2.map(_._3)) + .toSeq + + //Split the path to handle paths to array elements (if identifier is on a array path) + val (elemMatchPath, queryPath) = FHIRUtil.splitElementPathIntoElemMatchAndQueryPaths(path) + //Construct main query, it both checks the Identifier.type.coding and Identifier.value + val mainQuery = + orQueries( + parsedValues + .map { + case ((typeSystem, typeCode), values) => + Filters.and( + Filters.elemMatch( + FHIRUtil.mergeElementPath(queryPath, s"${FHIR_COMMON_FIELDS.TYPE}.${FHIR_COMMON_FIELDS.CODING}"), //Path for type codes in Identifier + getTokenCodeSystemQuery(FHIR_COMMON_FIELDS.SYSTEM, FHIR_COMMON_FIELDS.CODE, Some(typeSystem), Some(typeCode)) + ), + values match { + case Seq(single) => Filters.eq(FHIRUtil.mergeElementPath(queryPath, FHIR_COMMON_FIELDS.VALUE), single) + case multiple => Filters.in(FHIRUtil.mergeElementPath(queryPath, FHIR_COMMON_FIELDS.VALUE), multiple:_*) + } + ) + } + ) + + //If an array exist, use elemMatch otherwise return the query + elemMatchPath match { + case None => mainQuery + case Some(emp) => Filters.elemMatch(emp, mainQuery) + } + } + + + /** + * Get query for system and code matching for multiple codes + * + * @param systemPath Path to the system part e.g. Coding.system, Identifier.system + * @param codePath Path to the code part + * @param system Expected system value + * @param codes Expected codes + * @return + */ + private def getTokenCodeSystemQueryForMultipleCodes(systemPath: String, codePath: String, system: Option[String], codes: Option[Seq[String]]): Bson = { + + def getCodesQuery(codes:Seq[String]) = + codes match { + case Seq(single) => Filters.eq(codePath, single) + case multiple => Filters.in(codePath, multiple: _*) + } + + system match { + // Query like [code] -> the value of [code] matches a Coding.code or Identifier.value irrespective of the value of the system property + case None => getCodesQuery(codes.get) + // Query like |[code] -> the value of [code] matches a Coding.code or Identifier.value, and the Coding/Identifier has no system property + case Some("") => + Filters.and(Filters.exists(systemPath, exists = false), getCodesQuery(codes.get)) + // Query like [system][code] -> the value of [code] matches a Coding.code or Identifier.value, and the value of [system] matches the system property of the Identifier or Coding + case Some(sys) => + codes match { + //[system]| --> should macth only systen + case None => + Filters.eq(systemPath, sys) + // Query like [system][code] + case Some(cds) => + Filters.and(getCodesQuery(cds), Filters.eq(systemPath, sys)) + } + } + } + + /** + * Handle the Token query on system and code fields + * + * @param systemPath Path to the system part e.g. Coding.system, Identifier.system + * @param codePath Path to the code part + * @param system Expected system value + * @param code Expected code value + * @return + */ + private def getTokenCodeSystemQuery(systemPath: String, codePath: String, system: Option[String], code: Option[String]): Bson = { + system match { + // Query like [code] -> the value of [code] matches a Coding.code or Identifier.value irrespective of the value of the system property + case None => + Filters.eq(codePath, code.get) + // Query like |[code] -> the value of [code] matches a Coding.code or Identifier.value, and the Coding/Identifier has no system property + case Some("") => + Filters.and(Filters.exists(systemPath, exists = false), Filters.eq(codePath, code.get)) + // Query like [system][code] -> the value of [code] matches a Coding.code or Identifier.value, and the value of [system] matches the system property of the Identifier or Coding + case Some(sys) => + code match { + //[system]| --> should match only system + case None => + Filters.eq(systemPath, sys) + // Query like [system][code] + case Some(cd) => + Filters.and(Filters.eq(codePath, cd), Filters.eq(systemPath, sys)) + } + } + } + + + /** + * Query construction for :sw modifier (onFHIR.io specific) + * + * @param systemPath Path to the system element + * @param codePath Path to the code element + * @param system Supplied system part in parameter value + * @param code Supplied code part in parameter value + * @return + */ + private def getQueryForStartsWithModifier(systemPath: String, codePath: String, system: Option[String], code: Option[String]): Bson = { + if (code.isEmpty) + throw new InvalidParameterException(s"Code value should be given when modifier ':sw' is used!") + //val pattern = Pattern.compile("^"+code.get+"") + val codeStartsWithQuery = getQueryForStartsWith(codePath, code.get) + + system match { + // Query like [code] -> the value of [code] matches a Coding.code or Identifier.value irrespective of the value of the system property + case None => + codeStartsWithQuery + // Query like |[code] -> the value of [code] matches a Coding.code or Identifier.value, and the Coding/Identifier has no system property + case Some("") => + Filters.and(Filters.exists(systemPath, exists = false), codeStartsWithQuery) + // Query like [system][code] -> the value of [code] matches a Coding.code or Identifier.value, and the value of [system] matches the system property of the Identifier or Coding + case Some(sys) => + code match { + //[system]| --> should macth only systen + case None => + Filters.eq(systemPath, sys) + // Query like [system][code] + case Some(_) => + Filters.and(Filters.eq(systemPath, sys), codeStartsWithQuery) + } + } + } + + + /** + * Construct MongoDB query statement for prefix search (If the given value on target path starts with given prefix) + * @param path Path to the target element + * @param prefix Prefix + * @return + */ + private def getQueryForStartsWith(path:String, prefix:String):Bson = + Filters.regex(path, "^"+prefix+"" ) + +} diff --git a/onfhir-core/src/main/scala/io/onfhir/db/UriQueryBuilder.scala b/onfhir-core/src/main/scala/io/onfhir/db/UriQueryBuilder.scala new file mode 100644 index 00000000..09d14788 --- /dev/null +++ b/onfhir-core/src/main/scala/io/onfhir/db/UriQueryBuilder.scala @@ -0,0 +1,121 @@ +package io.onfhir.db + +import io.onfhir.api.FHIR_PREFIXES_MODIFIERS +import io.onfhir.api.util.FHIRUtil +import io.onfhir.exception.InvalidParameterException +import org.mongodb.scala.bson.conversions.Bson +import org.mongodb.scala.model.Filters + +import java.net.URL +import scala.util.Try + +object UriQueryBuilder extends IFhirQueryBuilder { + + /** + * The uri parameter refers to a URI (RFC 3986) element. Matches are precise (e.g. case, accent, and escape) + * sensitive, and the entire URI must match. + * The modifier :above or :below can be used to indicate that + * partial matching is used. + * + * @param values Values supplied for the uri parameter + * @param modifier Modifier to be handled + * @param path Path to the target element to be queried + * @param targetType FHIR Type of the target element + * @return equivalent BsonDocument for the target query + */ + def getQuery(values: Seq[String], modifier: String, path: String, targetType: String): Bson = { + modifier match { + //No modifier + case "" => getQueryForUriEquality(values, path) + case FHIR_PREFIXES_MODIFIERS.ABOVE => + if(values.length > 1) + throw new InvalidParameterException(s"Only single url value should be provided when modifier ${FHIR_PREFIXES_MODIFIERS.ABOVE} is used for FHIR url type parameters!") + getQueryForAboveModifier(values.head, path) + case FHIR_PREFIXES_MODIFIERS.BELOW => + if (values.length > 1) + throw new InvalidParameterException(s"Only single url value should be provided when modifier ${FHIR_PREFIXES_MODIFIERS.BELOW} is used for FHIR url type parameters!") + getQueryForBelowModifier(values.head, path) + case oth => + throw new InvalidParameterException(s"Modifier ${oth} is not valid or supported by onFhir.io for FHIR url type parameters!") + } + } + + /** + * Construct query for uri query (without modifier) + * @param values Supplied urls + * @param path Path to the element + * @return + */ + private def getQueryForUriEquality(values:Seq[String], path:String):Bson = { + //If this is a query on Canonical URLs of the conformance and knowledge resources (e.g. StructureDefinition, ValueSet, PlanDefinition etc) and a version part is given |[version] + if (path == "url" && values.exists(_.contains("|"))) { + val canonicalRefs = values.flatMap(value => Try(FHIRUtil.parseCanonicalReference(value)).toOption) + val queriesForWithVersions = + canonicalRefs + .filter(_.version.isDefined) + .map(cr => + Filters.and(Filters.equal(path, cr.getUrl()), Filters.equal("version", cr.version.get)) + ) + + val urls = canonicalRefs.filter(_.version.isEmpty).map(_.getUrl()) + val queryForWithoutVersions = urls match { + case Nil => None + case Seq(single) => Some(Filters.equal(path, single)) + case multiple => Some(Filters.in(path, multiple:_*)) + } + orQueries(queriesForWithVersions ++ queryForWithoutVersions.toSeq) + } else { + values match { + case Seq(single) => + // Exact match + Filters.equal(path, single) + case multiple => + Filters.in(path, values:_*) + } + } + } + + /** + * Construct query for :above on urls + * @param value Supplied URL + * @param path Path to the element + * @return + */ + private def getQueryForAboveModifier(value:String, path:String):Bson = { + val url = Try(new URL(value)).toOption + if (url.isEmpty || !value.contains('/')) + throw new InvalidParameterException(s"Modifier ${FHIR_PREFIXES_MODIFIERS.ABOVE} is only supported for URLs not URNs or OIDs!") + + val initialPart = url.get.getProtocol + "://" + url.get.getHost + (if (value.contains(url.get.getHost + ":" + url.get.getPort.toString)) ":" + url.get.getPort else "") + var urlPath = url.get.getPath + if (urlPath.isEmpty || urlPath == "/") urlPath = "" else urlPath = urlPath.drop(1) + val parts = urlPath.split("/") + + def constructRegexForAbove(parts: Seq[String]): String = { + parts match { + case Nil => "" + case oth => "(" + FHIRUtil.escapeCharacters("/" + parts.head) + constructRegexForAbove(parts.drop(1)) + ")?" + } + } + + //Constuct the regex to match any url above + val regularExpressionValue = FHIRUtil.escapeCharacters(initialPart) + constructRegexForAbove(parts.toIndexedSeq) + Filters.regex(path, "\\A" + regularExpressionValue + "$") + } + + /** + * Construct query for :below modifier + * @param value Supplied URL + * @param path Path to the element + * @return + */ + private def getQueryForBelowModifier(value: String, path: String):Bson = { + val url = Try(new URL(value)).toOption + if (url.isEmpty) + throw new InvalidParameterException(s"Modifier ${FHIR_PREFIXES_MODIFIERS.BELOW} is only supported for URLs not URNs or OIDs!") + // Escape characters for to have valid regular expression + val regularExpressionValue = FHIRUtil.escapeCharacters(value) + "(" + FHIRUtil.escapeCharacters("/") + ".*)*" + // Match at the beginning of the uri + Filters.regex(path, "\\A" + regularExpressionValue + "$") + } +} diff --git a/onfhir-server-r4/src/test/scala/io/onfhir/api/endpoint/FHIRSearchEndpointTest.scala b/onfhir-server-r4/src/test/scala/io/onfhir/api/endpoint/FHIRSearchEndpointTest.scala index 3e5f066f..6e25fecf 100644 --- a/onfhir-server-r4/src/test/scala/io/onfhir/api/endpoint/FHIRSearchEndpointTest.scala +++ b/onfhir-server-r4/src/test/scala/io/onfhir/api/endpoint/FHIRSearchEndpointTest.scala @@ -381,6 +381,20 @@ class FHIRSearchEndpointTest extends OnFhirTest with FHIREndpoint { val bundle = responseAs[Resource] checkSearchResult(bundle, "Patient", 2, Some(query)) } + //multiple not + query = "?code:not=http://loinc.org|15074-8,http://loinc.org|718-7" + Get("/" + OnfhirConfig.baseUri + "/" + resourceType + query) ~> fhirRoute ~> check { + status === OK + val bundle = responseAs[Resource] + checkSearchResult(bundle, resourceType, 0, Some(query)) + } + query = "?code:not=http://loinc.org|15074-8,http://loinc.org|748-7" + Get("/" + OnfhirConfig.baseUri + "/" + resourceType + query) ~> fhirRoute ~> check { + status === OK + val bundle = responseAs[Resource] + checkSearchResult(bundle, resourceType, 1, Some(query)) + (bundle \ "entry" \ "resource" \ "id").extract[Seq[String]] must contain(obsHemoglobinId) + } } "handle modifier 'in' and 'not-in' for token type" in { diff --git a/pom.xml b/pom.xml index e2aca5a4..77765bf7 100644 --- a/pom.xml +++ b/pom.xml @@ -52,6 +52,13 @@ SRDC Corp. https://www.srdc.com.tr + + dogukan10 + Dogukan Cavdaroglu + dogukan@srdc.com.tr + SRDC Corp. + https://www.srdc.com.tr + ozankose1992 Ozan Köse @@ -88,7 +95,7 @@ - 3.3-SNAPSHOT + 3.4-SNAPSHOT 4.8.1