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