From 9313b88082e3d235bb5a9235fdee243c987c736b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Jaouen?= Date: Thu, 16 Apr 2020 13:39:52 +0200 Subject: [PATCH] fix(gravsearch): Prevent duplicate results (#1626) --- .../05-internals/design/api-v2/gravsearch.md | 32 +- .../resourcemessages/ResourceMessagesV2.scala | 20 +- .../responders/v1/ValuesResponderV1.scala | 4 +- .../responders/v2/ResourcesResponderV2.scala | 2 + .../responders/v2/SearchResponderV2.scala | 75 ++-- .../responders/v2/StandoffResponderV2.scala | 1 + .../v2/search/MainQueryResultProcessor.scala | 236 ++--------- .../responders/v2/search/QueryTraverser.scala | 21 +- .../v2/search/SparqlTransformer.scala | 2 +- .../GravsearchMainQueryGenerator.scala | 190 +++++---- .../prequery/AbstractPrequeryGenerator.scala | 143 +++---- ...ravsearchToCountPrequeryTransformer.scala} | 44 +- ...pecificGravsearchToPrequeryGenerator.scala | 206 ---------- ...cificGravsearchToPrequeryTransformer.scala | 378 ++++++++++++++++++ .../GravsearchTypeInspectionResult.scala | 21 + .../types/GravsearchTypeInspectionUtil.scala | 20 +- .../webapi/util/ConstructResponseUtilV2.scala | 7 +- .../knora/webapi/util/StringFormatter.scala | 3 +- .../searchR2RV2/IncomingLinksForBook.jsonld | 7 +- .../searchR2RV2/LinkObjectsToBooks.jsonld | 17 +- .../searchR2RV2/RegionsForPage.jsonld | 9 +- .../ThingFromQueryWithUnion.jsonld | 104 +++++ .../regionsOfZeitgloecklein.jsonld | 37 +- .../webapi/e2e/v2/SearchRouteV2R2RSpec.scala | 57 ++- .../v2/ResourcesResponderV2Spec.scala | 14 +- ...earchToCountPrequeryTransformerSpec.scala} | 5 +- ...GravsearchToPrequeryTransformerSpec.scala} | 217 +++++----- .../util/ConstructResponseUtilV2Spec.scala | 1 + 28 files changed, 1032 insertions(+), 841 deletions(-) rename webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/{NonTriplestoreSpecificGravsearchToCountPrequeryGenerator.scala => NonTriplestoreSpecificGravsearchToCountPrequeryTransformer.scala} (63%) delete mode 100644 webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryGenerator.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformer.scala create mode 100644 webapi/src/test/resources/test-data/searchR2RV2/ThingFromQueryWithUnion.jsonld rename webapi/src/test/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/{NonTriplestoreSpecificGravsearchToCountPrequeryGeneratorSpec.scala => NonTriplestoreSpecificGravsearchToCountPrequeryTransformerSpec.scala} (98%) rename webapi/src/test/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/{NonTriplestoreSpecificGravsearchToPrequeryGeneratorSpec.scala => NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala} (98%) diff --git a/docs/src/paradox/05-internals/design/api-v2/gravsearch.md b/docs/src/paradox/05-internals/design/api-v2/gravsearch.md index 610e1af8c5..c289ee6d15 100644 --- a/docs/src/paradox/05-internals/design/api-v2/gravsearch.md +++ b/docs/src/paradox/05-internals/design/api-v2/gravsearch.md @@ -174,16 +174,10 @@ PREFIX knora-api: } ``` -The prequery's SELECT clause is built using the member variables defined in `AbstractPrequeryGenerator`. -State of member variables after transformation of the input query into the prequery: - -- `mainResourceVariable`: `QueryVariable(page)` -- `dependentResourceVariables`: `Set(QueryVariable(book))` -- `dependentResourceVariablesGroupConcat`: `Set(QueryVariable(book__Concat))` -- `valueObjectVariables`: `Set(QueryVariable(book__LinkValue), QueryVariable(seqnum))`: `?book` represents the dependent resource and `?book__LinkValue` the link value connecting `?page` and `?book`. -- `valueObjectVariablesGroupConcat`: `Set(QueryVariable(seqnum__Concat), QueryVariable(book__LinkValue__Concat))` - -The resulting SELECT clause of the prequery looks as follows: +The prequery's SELECT clause is built by +`NonTriplestoreSpecificGravsearchToPrequeryTransformer.getSelectColumns`, +based on the variables used in the input query's `CONSTRUCT` clause. +The resulting SELECT clause looks as follows: ```sparql SELECT DISTINCT @@ -219,6 +213,11 @@ is unbound, we concatenate an empty string. This is necessary because, in Apache triplestores), "If `GROUP_CONCAT` has an unbound value in the list of values to concat, the overall result is 'error'" (see [this Jena issue](https://issues.apache.org/jira/browse/JENA-1856)). +If the input query contains a `UNION`, and a variable is bound in one branch +of the `UNION` and not in another branch, it is possible that the prequery +will return more than one row per main resource. To deal with this situation, +`SearchResponderV2` merges rows that contain the same main resource IRI. + ### Main Query The purpose of the main query is to get all requested information about the main resource, dependent resources, and value objects. @@ -233,8 +232,17 @@ The classes involved in generating the main query can be found in The main query is a SPARQL CONSTRUCT query. Its generation is handled by the method `GravsearchMainQueryGenerator.createMainQuery`. -It takes three arguments: `mainResourceIris: Set[IriRef], dependentResourceIris: -Set[IriRef], valueObjectIris: Set[IRI]`. From the given Iris, statements are +It takes three arguments: +`mainResourceIris: Set[IriRef], dependentResourceIris: Set[IriRef], valueObjectIris: Set[IRI]`. + +These sets are constructed based on information about variables representing +dependent resources and value objects in the prequery, which is provided by +`NonTriplestoreSpecificGravsearchToPrequeryTransformer`: + +- `dependentResourceVariablesGroupConcat`: `Set(QueryVariable(book__Concat))` +- `valueObjectVariablesGroupConcat`: `Set(QueryVariable(seqnum__Concat), QueryVariable(book__LinkValue__Concat))` + +From the given Iris, statements are generated that ask for complete information on *exactly* these resources and values. For any given resource Iri, only the values present in `valueObjectIris` are to be queried. This is achieved by using SPARQL's diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index af5618a8f6..db0d247710 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -824,6 +824,17 @@ case class ReadResourcesSequenceV2(resources: Seq[ReadResourceV2], ) } + private def getOntologiesFromResource(resource: ReadResourceV2): Set[SmartIri] = { + val propertyIriOntologies: Set[SmartIri] = resource.values.keySet.map(_.getOntologyFromEntity) + + val valueOntologies: Set[SmartIri] = resource.values.values.flatten.collect { + case readLinkValueV2: ReadLinkValueV2 => + readLinkValueV2.valueContent.nestedResource.map(nested => getOntologiesFromResource(nested)) + }.flatten.flatten.toSet + + propertyIriOntologies ++ valueOntologies + resource.resourceClassIri.getOntologyFromEntity + } + // #generateJsonLD private def generateJsonLD(targetSchema: ApiV2Schema, settings: SettingsImpl, schemaOptions: Set[SchemaOption]): JsonLDDocument = { // #generateJsonLD @@ -843,14 +854,7 @@ case class ReadResourcesSequenceV2(resources: Seq[ReadResourceV2], // Make JSON-LD prefixes for the project-specific ontologies used in the response. val projectSpecificOntologiesUsed: Set[SmartIri] = resources.flatMap { - resource => - val resourceOntology = resource.resourceClassIri.getOntologyFromEntity - - val propertyOntologies = resource.values.keySet.map { - property => property.getOntologyFromEntity - } - - propertyOntologies + resourceOntology + resource => getOntologiesFromResource(resource) }.toSet.filter(!_.isKnoraBuiltInDefinitionIri) // Make the knora-api prefix for the target schema. diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v1/ValuesResponderV1.scala b/webapi/src/main/scala/org/knora/webapi/responders/v1/ValuesResponderV1.scala index 7a25442caf..7be1b5d35f 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v1/ValuesResponderV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v1/ValuesResponderV1.scala @@ -744,7 +744,7 @@ class ValuesResponderV1(responderData: ResponderData) extends Responder(responde // If we're updating a link, findResourceWithValueResult will contain the IRI of the property that points to the // knora-base:LinkValue, but we'll need the IRI of the corresponding link property. val propertyIri = changeValueRequest.value match { - case linkUpdateV1: LinkUpdateV1 => stringFormatter.linkValuePropertyIri2LinkPropertyIri(findResourceWithValueResult.propertyIri) + case linkUpdateV1: LinkUpdateV1 => stringFormatter.linkValuePropertyIriToLinkPropertyIri(findResourceWithValueResult.propertyIri) case _ => findResourceWithValueResult.propertyIri } @@ -1075,7 +1075,7 @@ class ValuesResponderV1(responderData: ResponderData) extends Responder(responde case (p, o) => p == OntologyConstants.KnoraBase.HasPermissions }.map(_._2).getOrElse(throw InconsistentTriplestoreDataException(s"Value ${deleteValueRequest.valueIri} has no permissions")) - val linkPropertyIri = stringFormatter.linkValuePropertyIri2LinkPropertyIri(findResourceWithValueResult.propertyIri) + val linkPropertyIri = stringFormatter.linkValuePropertyIriToLinkPropertyIri(findResourceWithValueResult.propertyIri) for { // Get project info diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index aba1e6ef1b..87b8ad4d1f 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -1210,6 +1210,7 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt apiResponse: ReadResourcesSequenceV2 <- ConstructResponseUtilV2.createApiResponse( mainResourcesAndValueRdfData = mainResourcesAndValueRdfData, orderByResourceIri = resourceIrisDistinct, + pageSizeBeforeFiltering = resourceIris.size, // doesn't matter because we're not doing paging mappings = mappingsAsMap, queryStandoff = queryStandoff, versionDate = versionDate, @@ -1264,6 +1265,7 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt apiResponse: ReadResourcesSequenceV2 <- ConstructResponseUtilV2.createApiResponse( mainResourcesAndValueRdfData = mainResourcesAndValueRdfData, orderByResourceIri = resourceIrisDistinct, + pageSizeBeforeFiltering = resourceIris.size, // doesn't matter because we're not doing paging mappings = Map.empty[IRI, MappingAndXSLTransformation], queryStandoff = false, versionDate = None, diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala index 6c5e2ee994..d265ad46e7 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala @@ -146,13 +146,13 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand // _ = println(searchSparql) prequeryResponseNotMerged: SparqlSelectResponse <- (storeManager ? SparqlSelectRequest(searchSparql)).mapTo[SparqlSelectResponse] + // _ = println(prequeryResponseNotMerged) mainResourceVar = QueryVariable("resource") + // Merge rows with the same resource IRI. prequeryResponse = mergePrequeryResults(prequeryResponseNotMerged, mainResourceVar) - // _ = println(prequeryResponse) - // a sequence of resource IRIs that match the search criteria // attention: no permission checking has been done so far resourceIris: Seq[IRI] = prequeryResponse.results.bindings.map { @@ -240,6 +240,7 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand apiResponse: ReadResourcesSequenceV2 <- ConstructResponseUtilV2.createApiResponse( mainResourcesAndValueRdfData = mainResourcesAndValueRdfData, orderByResourceIri = resourceIris, + pageSizeBeforeFiltering = resourceIris.size, mappings = mappingsAsMap, queryStandoff = queryStandoff, calculateMayHaveMoreResults = true, @@ -281,7 +282,8 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand // Create a Select prequery - nonTriplestoreSpecificConstructToSelectTransformer: NonTriplestoreSpecificGravsearchToCountPrequeryGenerator = new NonTriplestoreSpecificGravsearchToCountPrequeryGenerator( + nonTriplestoreSpecificConstructToSelectTransformer: NonTriplestoreSpecificGravsearchToCountPrequeryTransformer = new NonTriplestoreSpecificGravsearchToCountPrequeryTransformer( + constructClause = inputQuery.constructClause, typeInspectionResult = typeInspectionResult, querySchema = inputQuery.querySchema.getOrElse(throw AssertionException(s"WhereClause has no querySchema")) ) @@ -339,7 +341,6 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand targetSchema: ApiV2Schema, schemaOptions: Set[SchemaOption], requestingUser: UserADM): Future[ReadResourcesSequenceV2] = { - import org.knora.webapi.responders.v2.search.MainQueryResultProcessor import org.knora.webapi.responders.v2.search.gravsearch.mainquery.GravsearchMainQueryGenerator for { @@ -356,7 +357,8 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand // Create a Select prequery - nonTriplestoreSpecificConstructToSelectTransformer: NonTriplestoreSpecificGravsearchToPrequeryGenerator = new NonTriplestoreSpecificGravsearchToPrequeryGenerator( + nonTriplestoreSpecificConstructToSelectTransformer: NonTriplestoreSpecificGravsearchToPrequeryTransformer = new NonTriplestoreSpecificGravsearchToPrequeryTransformer( + constructClause = inputQuery.constructClause, typeInspectionResult = typeInspectionResult, querySchema = inputQuery.querySchema.getOrElse(throw AssertionException(s"WhereClause has no querySchema")), settings = settings @@ -366,11 +368,14 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand // TODO: if the ORDER BY criterion is a property whose occurrence is not 1, then the logic does not work correctly // TODO: the ORDER BY criterion has to be included in a GROUP BY statement, returning more than one row if property occurs more than once - nonTriplestoreSpecficPrequery: SelectQuery = QueryTraverser.transformConstructToSelect( + nonTriplestoreSpecificPrequery: SelectQuery = QueryTraverser.transformConstructToSelect( inputQuery = inputQuery.copy(whereClause = whereClauseWithoutAnnotations), transformer = nonTriplestoreSpecificConstructToSelectTransformer ) + // variable representing the main resources + mainResourceVar: QueryVariable = nonTriplestoreSpecificConstructToSelectTransformer.mainResourceVariable + // Convert the non-triplestore-specific query to a triplestore-specific one. triplestoreSpecificQueryPatternTransformerSelect: SelectToSelectTransformer = { if (settings.triplestoreType.startsWith("graphdb")) { @@ -384,17 +389,16 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand // Convert the preprocessed query to a non-triplestore-specific query. triplestoreSpecificPrequery = QueryTraverser.transformSelectToSelect( - inputQuery = nonTriplestoreSpecficPrequery, + inputQuery = nonTriplestoreSpecificPrequery, transformer = triplestoreSpecificQueryPatternTransformerSelect ) // _ = println(triplestoreSpecificPrequery.toSparql) prequeryResponseNotMerged: SparqlSelectResponse <- (storeManager ? SparqlSelectRequest(triplestoreSpecificPrequery.toSparql)).mapTo[SparqlSelectResponse] + pageSizeBeforeFiltering: Int = prequeryResponseNotMerged.results.bindings.size - // variable representing the main resources - mainResourceVar: QueryVariable = nonTriplestoreSpecificConstructToSelectTransformer.getMainResourceVariable - + // Merge rows with the same main resource IRI. This could happen if there are unbound variables in a UNION. prequeryResponse = mergePrequeryResults(prequeryResponseNotMerged = prequeryResponseNotMerged, mainResourceVar = mainResourceVar) // a sequence of resource IRIs that match the search criteria @@ -404,15 +408,21 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand resultRow.rowMap(mainResourceVar.variableName) } - queryResultsSeparatedWithFullGraphPattern: ConstructResponseUtilV2.MainResourcesAndValueRdfData <- if (mainResourceIris.nonEmpty) { + mainQueryResults: ConstructResponseUtilV2.MainResourcesAndValueRdfData <- if (mainResourceIris.nonEmpty) { // at least one resource matched the prequery // get all the IRIs for variables representing dependent resources per main resource - val dependentResourceIrisPerMainResource: MainQueryResultProcessor.DependentResourcesPerMainResource = MainQueryResultProcessor.getDependentResourceIrisPerMainResource(prequeryResponse, nonTriplestoreSpecificConstructToSelectTransformer, mainResourceVar) + val dependentResourceIrisPerMainResource: GravsearchMainQueryGenerator.DependentResourcesPerMainResource = + GravsearchMainQueryGenerator.getDependentResourceIrisPerMainResource( + prequeryResponse = prequeryResponse, + transformer = nonTriplestoreSpecificConstructToSelectTransformer, + mainResourceVar = mainResourceVar + ) // collect all variables representing resources val allResourceVariablesFromTypeInspection: Set[QueryVariable] = typeInspectionResult.entities.collect { - case (queryVar: TypeableVariable, nonPropTypeInfo: NonPropertyTypeInfo) if OntologyConstants.KnoraApi.isKnoraApiV2Resource(nonPropTypeInfo.typeIri) => QueryVariable(queryVar.variableName) + case (queryVar: TypeableVariable, nonPropTypeInfo: NonPropertyTypeInfo) if OntologyConstants.KnoraApi.isKnoraApiV2Resource(nonPropTypeInfo.typeIri) => + QueryVariable(queryVar.variableName) }.toSet // the user may have defined IRIs of dependent resources in the input query (type annotations) @@ -426,7 +436,11 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand val allDependentResourceIris: Set[IRI] = dependentResourceIrisPerMainResource.dependentResourcesPerMainResource.values.flatten.toSet ++ dependentResourceIrisFromTypeInspection // for each main resource, create a Map of value object variables and their Iris - val valueObjectVarsAndIrisPerMainResource: MainQueryResultProcessor.ValueObjectVariablesAndValueObjectIris = MainQueryResultProcessor.getValueObjectVarsAndIrisPerMainResource(prequeryResponse, nonTriplestoreSpecificConstructToSelectTransformer, mainResourceVar) + val valueObjectVarsAndIrisPerMainResource: GravsearchMainQueryGenerator.ValueObjectVariablesAndValueObjectIris = GravsearchMainQueryGenerator.getValueObjectVarsAndIrisPerMainResource( + prequeryResponse = prequeryResponse, + transformer = nonTriplestoreSpecificConstructToSelectTransformer, + mainResourceVar = mainResourceVar + ) // collect all value objects IRIs (for all main resources and for all value object variables) val allValueObjectIris: Set[IRI] = valueObjectVarsAndIrisPerMainResource.valueObjectVariablesAndValueObjectIris.values.foldLeft(Set.empty[IRI]) { @@ -485,7 +499,6 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand typeInspectionResult, inputQuery ) - } yield queryResultsFilteredForPermissions.copy( resources = queryResWithFullGraphPatternOnlyRequestedValues ) @@ -501,14 +514,15 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand // If we're querying standoff, get XML-to standoff mappings. mappingsAsMap: Map[IRI, MappingAndXSLTransformation] <- if (queryStandoff) { - getMappingsFromQueryResultsSeparated(queryResultsSeparatedWithFullGraphPattern.resources, requestingUser) + getMappingsFromQueryResultsSeparated(mainQueryResults.resources, requestingUser) } else { FastFuture.successful(Map.empty[IRI, MappingAndXSLTransformation]) } apiResponse: ReadResourcesSequenceV2 <- ConstructResponseUtilV2.createApiResponse( - mainResourcesAndValueRdfData = queryResultsSeparatedWithFullGraphPattern, + mainResourcesAndValueRdfData = mainQueryResults, orderByResourceIri = mainResourceIris, + pageSizeBeforeFiltering = pageSizeBeforeFiltering, mappings = mappingsAsMap, queryStandoff = queryStandoff, versionDate = None, @@ -642,6 +656,7 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand readResourcesSequence: ReadResourcesSequenceV2 <- ConstructResponseUtilV2.createApiResponse( mainResourcesAndValueRdfData = mainResourcesAndValueRdfData, orderByResourceIri = mainResourceIris, + pageSizeBeforeFiltering = mainResourceIris.size, mappings = mappings, queryStandoff = maybeStandoffMinStartIndex.nonEmpty, versionDate = None, @@ -765,6 +780,7 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand apiResponse: ReadResourcesSequenceV2 <- ConstructResponseUtilV2.createApiResponse( mainResourcesAndValueRdfData = mainResourcesAndValueRdfData, orderByResourceIri = mainResourceIris.toSeq.sorted, + pageSizeBeforeFiltering = mainResourceIris.size, queryStandoff = false, versionDate = None, calculateMayHaveMoreResults = true, @@ -779,28 +795,27 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand /** * Given a prequery result, merges rows with the same main resource IRI. This could happen if there are unbound - * variables in GROUP_CONCAT expressions. + * variables in `GROUP_CONCAT` expressions. * * @param prequeryResponseNotMerged the prequery response before merging. * @param mainResourceVar the name of the column representing the main resource. * @return the merged results. */ private def mergePrequeryResults(prequeryResponseNotMerged: SparqlSelectResponse, mainResourceVar: QueryVariable): SparqlSelectResponse = { - // a sequence of resource IRIs that match the search criteria - // attention: no permission checking has been done so far - val mainResourceIris: Seq[IRI] = prequeryResponseNotMerged.results.bindings.map { - resultRow: VariableResultsRow => - resultRow.rowMap(mainResourceVar.variableName) - }.distinct - + // Make a Map of merged results per main resource IRI. val prequeryRowsMergedMap: Map[IRI, VariableResultsRow] = prequeryResponseNotMerged.results.bindings.groupBy { - row => row.rowMap(mainResourceVar.variableName) + row => + // Get the rows for each main resource IRI. + row.rowMap(mainResourceVar.variableName) }.map { case (resourceIri: IRI, rows: Seq[VariableResultsRow]) => + // Make a Set of all the column names in the rows to be merged. val columnNamesToMerge: Set[String] = rows.flatMap(_.rowMap.keySet).toSet + // Make a Map of column names to merged values. val mergedRowMap: Map[String, String] = columnNamesToMerge.map { columnName => + // For each column name, get the values to be merged. val columnValues: Seq[String] = rows.flatMap(_.rowMap.get(columnName)) // Is this is the column containing the main resource IRI? @@ -819,6 +834,14 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand resourceIri -> VariableResultsRow(new ErrorHandlingMap(mergedRowMap, { key: String => s"No value found for SPARQL query variable '$key' in query result row" })) } + // Construct a sequence of the distinct main resource IRIs in the query results, preserving the + // order of the result rows. + val mainResourceIris: Seq[IRI] = prequeryResponseNotMerged.results.bindings.map { + resultRow: VariableResultsRow => + resultRow.rowMap(mainResourceVar.variableName) + }.distinct + + // Arrange the merged rows in the same order. val prequeryRowsMerged: Seq[VariableResultsRow] = mainResourceIris.map { resourceIri => prequeryRowsMergedMap(resourceIri) } diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala index 83286d6c33..d74572a449 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala @@ -106,6 +106,7 @@ class StandoffResponderV2(responderData: ResponderData) extends Responder(respon readResourcesSequenceV2: ReadResourcesSequenceV2 <- ConstructResponseUtilV2.createApiResponse( mainResourcesAndValueRdfData = mainResourcesAndValueRdfData, orderByResourceIri = Seq(getStandoffRequestV2.resourceIri), + pageSizeBeforeFiltering = 1, // doesn't matter because we're not doing paging mappings = Map.empty, queryStandoff = false, calculateMayHaveMoreResults = false, diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/MainQueryResultProcessor.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/MainQueryResultProcessor.scala index e776118d7b..793e7f4b64 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/MainQueryResultProcessor.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/MainQueryResultProcessor.scala @@ -20,205 +20,32 @@ package org.knora.webapi.responders.v2.search import org.knora.webapi._ -import org.knora.webapi.messages.store.triplestoremessages.{SparqlSelectResponse, VariableResultsRow} -import org.knora.webapi.responders.v2.search.gravsearch.mainquery.GravsearchMainQueryGenerator -import org.knora.webapi.responders.v2.search.gravsearch.prequery.{AbstractPrequeryGenerator, NonTriplestoreSpecificGravsearchToPrequeryGenerator} +import org.knora.webapi.responders.v2.search.gravsearch.mainquery.GravsearchMainQueryGenerator.ValueObjectVariablesAndValueObjectIris +import org.knora.webapi.responders.v2.search.gravsearch.prequery.NonTriplestoreSpecificGravsearchToPrequeryTransformer import org.knora.webapi.responders.v2.search.gravsearch.types.GravsearchTypeInspectionResult import org.knora.webapi.util.ConstructResponseUtilV2.{RdfPropertyValues, RdfResources, ResourceWithValueRdfData, ValueRdfData} import org.knora.webapi.util.IriConversions._ -import org.knora.webapi.util.{ConstructResponseUtilV2, ErrorHandlingMap, SmartIri, StringFormatter} +import org.knora.webapi.util.{ConstructResponseUtilV2, SmartIri, StringFormatter} object MainQueryResultProcessor { /** - * Represents the IRIs of resources and value objects. - * - * @param resourceIris resource IRIs. - * @param valueObjectIris value object IRIs. - */ - case class ResourceIrisAndValueObjectIris(resourceIris: Set[IRI], valueObjectIris: Set[IRI]) - - /** - * Traverses value property assertions and returns the IRIs of the value objects and the dependent resources, recursively traversing their value properties as well. - * This is method is needed in order to determine if the whole graph pattern is still present in the results after permissions checking handled in [[ConstructResponseUtilV2.splitMainResourcesAndValueRdfData]]. - * Due to insufficient permissions, some of the resources (both main and dependent resources) and/or values may have been filtered out. - * - * @param valuePropertyAssertions the assertions to be traversed. - * @return a [[ResourceIrisAndValueObjectIris]] representing all resource and value object IRIs that have been found in `valuePropertyAssertions`. - */ - def collectResourceIrisAndValueObjectIrisFromMainQueryResult(valuePropertyAssertions: RdfPropertyValues)(implicit stringFormatter: StringFormatter): ResourceIrisAndValueObjectIris = { - - // look at the value objects and ignore the property IRIs (we are only interested in value instances) - val resAndValObjIris: Seq[ResourceIrisAndValueObjectIris] = valuePropertyAssertions.values.flatten.foldLeft(Seq.empty[ResourceIrisAndValueObjectIris]) { - (acc: Seq[ResourceIrisAndValueObjectIris], valueRdfData: ValueRdfData) => - - if (valueRdfData.nestedResource.nonEmpty) { - // this is a link value - // recursively traverse the dependent resource's values - - val dependentRes: ResourceWithValueRdfData = valueRdfData.nestedResource.get - - // recursively traverse the link value's nested resource and its assertions - val resAndValObjIrisForDependentRes: ResourceIrisAndValueObjectIris = collectResourceIrisAndValueObjectIrisFromMainQueryResult(dependentRes.valuePropertyAssertions) - - // get the dependent resource's IRI from the current link value's rdf:object, or rdf:subject in case of an incoming link - val dependentResIri: IRI = if (valueRdfData.isIncomingLink) { - valueRdfData.requireIriObject(OntologyConstants.Rdf.Subject.toSmartIri) - } else { - valueRdfData.requireIriObject(OntologyConstants.Rdf.Object.toSmartIri) - } - - // append results from recursion and current value object - ResourceIrisAndValueObjectIris( - resourceIris = resAndValObjIrisForDependentRes.resourceIris + dependentResIri, - valueObjectIris = resAndValObjIrisForDependentRes.valueObjectIris + valueRdfData.subjectIri - ) +: acc - } else { - // not a link value or no dependent resource given (in order to avoid infinite recursion) - // no dependent resource present - // append results for current value object - ResourceIrisAndValueObjectIris( - resourceIris = Set.empty[IRI], - valueObjectIris = Set(valueRdfData.subjectIri) - ) +: acc - } - } - - // convert the collection of `ResourceIrisAndValueObjectIris` into one - ResourceIrisAndValueObjectIris( - resourceIris = resAndValObjIris.flatMap(_.resourceIris).toSet, - valueObjectIris = resAndValObjIris.flatMap(_.valueObjectIris).toSet - ) - - } - - - /** - * Collects the Iris of dependent resources per main resource from the results returned by the prequery. - * Dependent resource Iris are grouped by main resource. - * - * @param prequeryResponse the results returned by the prequery. - * @param transformer the transformer that was used to turn the Gravsearch query into the prequery. - * @param mainResourceVar the variable representing the main resource. - * @return a [[DependentResourcesPerMainResource]]. - */ - def getDependentResourceIrisPerMainResource(prequeryResponse: SparqlSelectResponse, - transformer: NonTriplestoreSpecificGravsearchToPrequeryGenerator, - mainResourceVar: QueryVariable): DependentResourcesPerMainResource = { - - // variables representing dependent resources - val dependentResourceVariablesGroupConcat: Set[QueryVariable] = transformer.getDependentResourceVariablesGroupConcat - - val dependentResourcesPerMainRes = prequeryResponse.results.bindings.foldLeft(Map.empty[IRI, Set[IRI]]) { - case (acc: Map[IRI, Set[IRI]], resultRow: VariableResultsRow) => - // collect all the dependent resource Iris for the current main resource from prequery's response - - // the main resource's Iri - val mainResIri: String = resultRow.rowMap(mainResourceVar.variableName) - - // get the Iris of all the dependent resources for the given main resource - val dependentResIris: Set[IRI] = dependentResourceVariablesGroupConcat.flatMap { - dependentResVar: QueryVariable => - - // check if key exists: the variable representing dependent resources - // could be contained in an OPTIONAL or a UNION and be unbound - // It would be suppressed by `VariableResultsRow` in that case. - // - // Example: the query contains a dependent resource variable ?book within an OPTIONAL or a UNION. - // If the query returns results for the dependent resource ?book (Iris of resources that match the given criteria), - // those would be accessible via the variable ?book__Concat containing the aggregated results (Iris). - val dependentResIriOption: Option[IRI] = resultRow.rowMap.get(dependentResVar.variableName) - - dependentResIriOption match { - case Some(depResIri: IRI) => - - // IRIs are concatenated by GROUP_CONCAT using a separator, split them. Ignore empty - // strings, which could result from unbound variables in the query. - depResIri.split(AbstractPrequeryGenerator.groupConcatSeparator).toSeq.filter(_.nonEmpty) - - case None => Set.empty[IRI] // no Iri present since variable was inside aan OPTIONAL or UNION - } - - } - - acc + (mainResIri -> dependentResIris) - } - - DependentResourcesPerMainResource(new ErrorHandlingMap(dependentResourcesPerMainRes, { key => throw GravsearchException(s"main resource not found: $key") })) - } - - /** - * Collects object variables and their values per main resource from the results returned by the prequery. - * Value objects variables and their Iris are grouped by main resource. - * - * @param prequeryResponse the results returned by the prequery. - * @param transformer the transformer that was used to turn the Gravsearch query into the prequery. - * @param mainResourceVar the variable representing the main resource. - * @return [[ValueObjectVariablesAndValueObjectIris]]. - */ - def getValueObjectVarsAndIrisPerMainResource(prequeryResponse: SparqlSelectResponse, - transformer: NonTriplestoreSpecificGravsearchToPrequeryGenerator, - mainResourceVar: QueryVariable): ValueObjectVariablesAndValueObjectIris = { - - // value objects variables present in the prequery's WHERE clause - val valueObjectVariablesConcat = transformer.getValueObjectVarsGroupConcat - - val valueObjVarsAndIris: Map[IRI, Map[QueryVariable, Set[IRI]]] = prequeryResponse.results.bindings.foldLeft(Map.empty[IRI, Map[QueryVariable, Set[IRI]]]) { - (acc: Map[IRI, Map[QueryVariable, Set[IRI]]], resultRow: VariableResultsRow) => - - // the main resource's Iri - val mainResIri: String = resultRow.rowMap(mainResourceVar.variableName) - - // the the variables representing value objects and their Iris - val valueObjVarToIris: Map[QueryVariable, Set[IRI]] = valueObjectVariablesConcat.map { - valueObjVarConcat: QueryVariable => - - // check if key exists: the variable representing value objects - // could be contained in an OPTIONAL or a UNION and be unbound - // It would be suppressed by `VariableResultsRow` in that case. - - // this logic works like in the case of dependent resources, see `getDependentResourceIrisPerMainResource` above. - val valueObjIrisOption: Option[IRI] = resultRow.rowMap.get(valueObjVarConcat.variableName) - - val valueObjIris: Set[IRI] = valueObjIrisOption match { - - case Some(valObjIris) => - - // IRIs are concatenated by GROUP_CONCAT using a separator, split them. Ignore empty - // strings, which could result from unbound variables in the query. - valObjIris.split(AbstractPrequeryGenerator.groupConcatSeparator).toSet.filter(_.nonEmpty) - - case None => Set.empty[IRI] // since variable was inside aan OPTIONAL or UNION - - } - - valueObjVarConcat -> valueObjIris - }.toMap - - val valueObjVarToIrisErrorHandlingMap = new ErrorHandlingMap(valueObjVarToIris, { key: QueryVariable => throw GravsearchException(s"variable not found: $key") }) - acc + (mainResIri -> valueObjVarToIrisErrorHandlingMap) - } - - ValueObjectVariablesAndValueObjectIris(new ErrorHandlingMap(valueObjVarsAndIris, { key => throw GravsearchException(s"main resource not found: $key") })) - } - - /** - * Given the results of the main query, filters out all values that the user did not ask for in the input query, - * i.e that are not present in its CONSTRUCT clause. - * - * @param queryResultsWithFullGraphPattern results with full graph pattern (that user has sufficient permissions on). - * @param valueObjectVarsAndIrisPerMainResource value object variables and their Iris per main resource. - * @param allResourceVariablesFromTypeInspection all variables representing resources. - * @param dependentResourceIrisFromTypeInspection Iris of dependent resources used in the input query. - * @param transformer the transformer that was used to turn the input query into the prequery. - * @param typeInspectionResult results of type inspection of the input query. - * @return results with only the values the user asked for in the input query's CONSTRUCT clause. - */ + * Given the results of the main query, filters out all values that the user did not ask for in the input query, + * i.e that are not present in its CONSTRUCT clause. + * + * @param queryResultsWithFullGraphPattern results with full graph pattern (that user has sufficient permissions on). + * @param valueObjectVarsAndIrisPerMainResource value object variables and their Iris per main resource. + * @param allResourceVariablesFromTypeInspection all variables representing resources. + * @param dependentResourceIrisFromTypeInspection Iris of dependent resources used in the input query. + * @param transformer the transformer that was used to turn the input query into the prequery. + * @param typeInspectionResult results of type inspection of the input query. + * @return results with only the values the user asked for in the input query's CONSTRUCT clause. + */ def getRequestedValuesFromResultsWithFullGraphPattern(queryResultsWithFullGraphPattern: RdfResources, valueObjectVarsAndIrisPerMainResource: ValueObjectVariablesAndValueObjectIris, allResourceVariablesFromTypeInspection: Set[QueryVariable], dependentResourceIrisFromTypeInspection: Set[IRI], - transformer: NonTriplestoreSpecificGravsearchToPrequeryGenerator, + transformer: NonTriplestoreSpecificGravsearchToPrequeryTransformer, typeInspectionResult: GravsearchTypeInspectionResult, inputQuery: ConstructQuery): RdfResources = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -233,8 +60,7 @@ object MainQueryResultProcessor { // Example: the statement "?page incunabula:seqnum ?seqnum ." is contained in the input query's CONSTRUCT clause. // ?seqnum (?seqnum__Concat) is a requested value and is associated with the resource variable ?page. val requestedValueObjectVariablesForAllResVars: Set[QueryVariable] = allResourceVariablesFromTypeInspection.flatMap { - resVar => - GravsearchMainQueryGenerator.collectValueVariablesForResource(inputQuery.constructClause, resVar, typeInspectionResult, transformer.groupConcatVariableSuffix) + resVar => transformer.getValueGroupConcatVariablesForResource(resVar) } // for each resource Iri (only dependent resources), @@ -244,8 +70,7 @@ object MainQueryResultProcessor { // Example: the statement " incunabula:title ?title ." is contained in the input query's CONSTRUCT clause. // ?title (?title__Concat) is a requested value and is associated with the dependent resource Iri . val requestedValueObjectVariablesForDependentResIris: Set[QueryVariable] = dependentResourceIrisFromTypeInspection.flatMap { - depResIri => - GravsearchMainQueryGenerator.collectValueVariablesForResource(inputQuery.constructClause, IriRef(iri = depResIri.toSmartIri), typeInspectionResult, transformer.groupConcatVariableSuffix) + depResIri => transformer.getValueGroupConcatVariablesForResource(IriRef(iri = depResIri.toSmartIri)) } // combine all value object variables into one set @@ -275,12 +100,12 @@ object MainQueryResultProcessor { val valueObjIrisRequestedForRes: Set[IRI] = requestedValObjIrisPerMainResource.getOrElse(mainResIri, throw AssertionException(s"key $mainResIri is absent in requested value object IRIs collection for resource $mainResIri")) /** - * Recursively filters out those values that the user does not want to see. - * Starts with the values of the main resource and also processes link values, possibly containing dependent resources with values. - * - * @param values the values to be filtered. - * @return filtered values. - */ + * Recursively filters out those values that the user does not want to see. + * Starts with the values of the main resource and also processes link values, possibly containing dependent resources with values. + * + * @param values the values to be filtered. + * @return filtered values. + */ def traverseAndFilterValues(values: ResourceWithValueRdfData): RdfPropertyValues = { values.valuePropertyAssertions.foldLeft(ConstructResponseUtilV2.emptyRdfPropertyValues) { case (acc, (propIri: SmartIri, values: Seq[ValueRdfData])) => @@ -335,19 +160,4 @@ object MainQueryResultProcessor { ) } } - - /** - * Represents dependent resources organized by main resource. - * - * @param dependentResourcesPerMainResource a set of dependent resource Iris organized by main resource. - */ - case class DependentResourcesPerMainResource(dependentResourcesPerMainResource: Map[IRI, Set[IRI]]) - - /** - * Represents value object variables and value object Iris organized by main resource. - * - * @param valueObjectVariablesAndValueObjectIris a set of value object Iris organized by value object variable and main resource. - */ - case class ValueObjectVariablesAndValueObjectIris(valueObjectVariablesAndValueObjectIris: Map[IRI, Map[QueryVariable, Set[IRI]]]) - } diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/QueryTraverser.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/QueryTraverser.scala index c7e5abbe41..7f2477bea0 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/QueryTraverser.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/QueryTraverser.scala @@ -127,20 +127,9 @@ case class TransformedOrderBy(statementPatterns: Seq[StatementPattern] = Vector. */ trait ConstructToSelectTransformer extends WhereTransformer { /** - * Collects information from a statement pattern in the CONSTRUCT clause of the input query, e.g. variables - * that need to be returned by the SELECT. - * - * @param statementPattern the statement to be handled. - */ - def handleStatementInConstruct(statementPattern: StatementPattern): Unit - - /** - * Returns the variables that should be included in the results of the SELECT query. This method will be called - * by [[QueryTraverser]] after the whole input query has been traversed. - * - * @return the variables that should be returned by the SELECT. + * Returns the columns to be specified in the SELECT query. */ - def getSelectVariables: Seq[SelectQueryColumn] + def getSelectColumns: Seq[SelectQueryColumn] /** * Returns the variables that the query result rows are grouped by (aggregating rows into one). @@ -321,10 +310,6 @@ object QueryTraverser { */ def transformConstructToSelect(inputQuery: ConstructQuery, transformer: ConstructToSelectTransformer): SelectQuery = { - for (statement <- inputQuery.constructClause.statements) { - transformer.handleStatementInConstruct(statement) - } - val transformedWherePatterns = transformWherePatterns( patterns = inputQuery.whereClause.patterns, inputOrderBy = inputQuery.orderBy, @@ -340,7 +325,7 @@ object QueryTraverser { val offset = transformer.getOffset(inputQuery.offset, limit) SelectQuery( - variables = transformer.getSelectVariables, + variables = transformer.getSelectColumns, whereClause = WhereClause(patterns = transformedWherePatterns ++ transformedOrderBy.statementPatterns), groupBy = groupBy, orderBy = transformedOrderBy.orderBy, diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/SparqlTransformer.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/SparqlTransformer.scala index 2b4a0eada5..3fb9031565 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/SparqlTransformer.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/SparqlTransformer.scala @@ -114,7 +114,7 @@ object SparqlTransformer { def escapeEntityForVariable(entity: Entity): String = { val entityStr = entity match { case QueryVariable(varName) => varName - case IriRef(iriLiteral, _) => iriLiteral.toString + case IriRef(iriLiteral, _) => iriLiteral.toOntologySchema(InternalSchema).toString case XsdLiteral(stringLiteral, _) => stringLiteral case _ => throw GravsearchException(s"A unique variable name could not be made for ${entity.toSparql}") } diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/mainquery/GravsearchMainQueryGenerator.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/mainquery/GravsearchMainQueryGenerator.scala index 267f89300c..ac3da70fed 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/mainquery/GravsearchMainQueryGenerator.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/mainquery/GravsearchMainQueryGenerator.scala @@ -20,18 +20,19 @@ package org.knora.webapi.responders.v2.search.gravsearch.mainquery import org.knora.webapi._ +import org.knora.webapi.messages.store.triplestoremessages.{SparqlSelectResponse, VariableResultsRow} import org.knora.webapi.responders.v2.search._ -import org.knora.webapi.responders.v2.search.gravsearch.types._ +import org.knora.webapi.responders.v2.search.gravsearch.prequery.{AbstractPrequeryGenerator, NonTriplestoreSpecificGravsearchToPrequeryTransformer} import org.knora.webapi.util.IriConversions._ -import org.knora.webapi.util.StringFormatter +import org.knora.webapi.util.{ErrorHandlingMap, StringFormatter} object GravsearchMainQueryGenerator { /** - * Constants used in the processing of Gravsearch queries. - * - * These constants are used to create SPARQL CONSTRUCT queries to be executed by the triplestore and to process the results that are returned. - */ + * Constants used in the processing of Gravsearch queries. + * + * These constants are used to create SPARQL CONSTRUCT queries to be executed by the triplestore and to process the results that are returned. + */ private object GravsearchConstants { // SPARQL variable representing the main resource and its properties @@ -85,94 +86,139 @@ object GravsearchMainQueryGenerator { } /** - * - * Collects variables representing values that are present in the CONSTRUCT clause of the input query for the given [[Entity]] representing a resource. - * - * @param constructClause the Construct clause to be looked at. - * @param resource the [[Entity]] representing the resource whose properties are to be collected - * @param typeInspectionResult results of type inspection. - * @param variableConcatSuffix the suffix appended to variable names in prequery results. - * @return a Set of [[PropertyTypeInfo]] representing the value and link value properties to be returned to the client. - */ - def collectValueVariablesForResource(constructClause: ConstructClause, resource: Entity, typeInspectionResult: GravsearchTypeInspectionResult, variableConcatSuffix: String): Set[QueryVariable] = { - - // make sure resource is a query variable or an IRI - resource match { - case queryVar: QueryVariable => () - case iri: IriRef => () - case literal: XsdLiteral => throw GravsearchException(s"${literal.toSparql} cannot represent a resource") - case other => throw GravsearchException(s"${other.toSparql} cannot represent a resource") - } + * Represents dependent resources organized by main resource. + * + * @param dependentResourcesPerMainResource a set of dependent resource Iris organized by main resource. + */ + case class DependentResourcesPerMainResource(dependentResourcesPerMainResource: Map[IRI, Set[IRI]]) + + /** + * Represents value object variables and value object Iris organized by main resource. + * + * @param valueObjectVariablesAndValueObjectIris a set of value object Iris organized by value object variable and main resource. + */ + case class ValueObjectVariablesAndValueObjectIris(valueObjectVariablesAndValueObjectIris: Map[IRI, Map[QueryVariable, Set[IRI]]]) - // TODO: check in type information that resource represents a resource + /** + * Collects the Iris of dependent resources per main resource from the results returned by the prequery. + * Dependent resource Iris are grouped by main resource. + * + * @param prequeryResponse the results returned by the prequery. + * @param transformer the transformer that was used to turn the Gravsearch query into the prequery. + * @param mainResourceVar the variable representing the main resource. + * @return a [[DependentResourcesPerMainResource]]. + */ + def getDependentResourceIrisPerMainResource(prequeryResponse: SparqlSelectResponse, + transformer: NonTriplestoreSpecificGravsearchToPrequeryTransformer, + mainResourceVar: QueryVariable): DependentResourcesPerMainResource = { + + // variables representing dependent resources + val dependentResourceVariablesGroupConcat: Set[QueryVariable] = transformer.dependentResourceVariablesGroupConcat + + val dependentResourcesPerMainRes = prequeryResponse.results.bindings.foldLeft(Map.empty[IRI, Set[IRI]]) { + case (acc: Map[IRI, Set[IRI]], resultRow: VariableResultsRow) => + // collect all the dependent resource Iris for the current main resource from prequery's response + + // the main resource's Iri + val mainResIri: String = resultRow.rowMap(mainResourceVar.variableName) + + // get the Iris of all the dependent resources for the given main resource + val dependentResIris: Set[IRI] = dependentResourceVariablesGroupConcat.flatMap { + dependentResVar: QueryVariable => + + // check if key exists: the variable representing dependent resources + // could be contained in an OPTIONAL or a UNION and be unbound + // It would be suppressed by `VariableResultsRow` in that case. + // + // Example: the query contains a dependent resource variable ?book within an OPTIONAL or a UNION. + // If the query returns results for the dependent resource ?book (Iris of resources that match the given criteria), + // those would be accessible via the variable ?book__Concat containing the aggregated results (Iris). + val dependentResIriOption: Option[IRI] = resultRow.rowMap.get(dependentResVar.variableName) + + dependentResIriOption match { + case Some(depResIri: IRI) => + + // IRIs are concatenated by GROUP_CONCAT using a separator, split them. + // Ignore empty strings, which could result from unbound variables in a UNION. + depResIri.split(AbstractPrequeryGenerator.groupConcatSeparator).toSeq.filter(_.nonEmpty) + + case None => Set.empty[IRI] // no Iri present since variable was inside aan OPTIONAL or UNION + } + + } - // get statements with the main resource as a subject - val statementsWithResourceAsSubject: Seq[StatementPattern] = constructClause.statements.filter { - statementPattern: StatementPattern => statementPattern.subj == resource + acc + (mainResIri -> dependentResIris) } - statementsWithResourceAsSubject.foldLeft(Set.empty[QueryVariable]) { - (acc: Set[QueryVariable], statementPattern: StatementPattern) => + DependentResourcesPerMainResource(new ErrorHandlingMap(dependentResourcesPerMainRes, { key => throw GravsearchException(s"main resource not found: $key") })) + } - // check if the predicate is a Knora value or linking property + /** + * Collects object variables and their values per main resource from the results returned by the prequery. + * Value objects variables and their Iris are grouped by main resource. + * + * @param prequeryResponse the results returned by the prequery. + * @param transformer the transformer that was used to turn the Gravsearch query into the prequery. + * @param mainResourceVar the variable representing the main resource. + * @return [[ValueObjectVariablesAndValueObjectIris]]. + */ + def getValueObjectVarsAndIrisPerMainResource(prequeryResponse: SparqlSelectResponse, + transformer: NonTriplestoreSpecificGravsearchToPrequeryTransformer, + mainResourceVar: QueryVariable): ValueObjectVariablesAndValueObjectIris = { - // create a key for the type annotations map - val typeableEntity: TypeableEntity = statementPattern.pred match { - case iriRef: IriRef => TypeableIri(iriRef.iri) - case variable: QueryVariable => TypeableVariable(variable.variableName) - case other => throw GravsearchException(s"Expected an IRI or a variable as the predicate of a statement, but ${other.toSparql} given") - } + // value objects variables present in the prequery's WHERE clause + val valueObjectVariablesConcat = transformer.valueObjectVariablesGroupConcat - // if the given key exists in the type annotations map, add it to the collection - if (typeInspectionResult.entities.contains(typeableEntity)) { + val valueObjVarsAndIris: Map[IRI, Map[QueryVariable, Set[IRI]]] = prequeryResponse.results.bindings.foldLeft(Map.empty[IRI, Map[QueryVariable, Set[IRI]]]) { + (acc: Map[IRI, Map[QueryVariable, Set[IRI]]], resultRow: VariableResultsRow) => - val propTypeInfo: PropertyTypeInfo = typeInspectionResult.entities(typeableEntity) match { - case propType: PropertyTypeInfo => propType + // the main resource's Iri + val mainResIri: String = resultRow.rowMap(mainResourceVar.variableName) - case _: NonPropertyTypeInfo => - throw GravsearchException(s"Expected a property: ${statementPattern.pred.toSparql}") + // the the variables representing value objects and their Iris + val valueObjVarToIris: Map[QueryVariable, Set[IRI]] = valueObjectVariablesConcat.map { + valueObjVarConcat: QueryVariable => - } + // check if key exists: the variable representing value objects + // could be contained in an OPTIONAL or a UNION and be unbound + // It would be suppressed by `VariableResultsRow` in that case. - val valueObjectVariable: Set[QueryVariable] = if (OntologyConstants.KnoraApi.isKnoraApiV2Resource(propTypeInfo.objectTypeIri)) { + // this logic works like in the case of dependent resources, see `getDependentResourceIrisPerMainResource` above. + val valueObjIrisOption: Option[IRI] = resultRow.rowMap.get(valueObjVarConcat.variableName) - // linking prop: get value object var and information which values are requested for dependent resource + val valueObjIris: Set[IRI] = valueObjIrisOption match { - // link value object variable - val valObjVar = SparqlTransformer.createUniqueVariableFromStatementForLinkValue( - baseStatement = statementPattern - ) + case Some(valObjIris) => - // return link value object variable and value objects requested for the dependent resource - Set(QueryVariable(valObjVar.variableName + variableConcatSuffix)) + // IRIs are concatenated by GROUP_CONCAT using a separator, split them. + // Ignore empty strings, which could result from unbound variables in a UNION. + valObjIris.split(AbstractPrequeryGenerator.groupConcatSeparator).toSet.filter(_.nonEmpty) + + case None => Set.empty[IRI] // since variable was inside aan OPTIONAL or UNION - } else { - statementPattern.obj match { - case queryVar: QueryVariable => Set(QueryVariable(queryVar.variableName + variableConcatSuffix)) - case other => throw GravsearchException(s"Expected a variable: ${other.toSparql}") } - } - acc ++ valueObjectVariable + valueObjVarConcat -> valueObjIris + }.toMap - } else { - // not a knora-api property - acc - } + val valueObjVarToIrisErrorHandlingMap = new ErrorHandlingMap(valueObjVarToIris, { key: QueryVariable => throw GravsearchException(s"variable not found: $key") }) + acc + (mainResIri -> valueObjVarToIrisErrorHandlingMap) } + + ValueObjectVariablesAndValueObjectIris(new ErrorHandlingMap(valueObjVarsAndIris, { key => throw GravsearchException(s"main resource not found: $key") })) } /** - * Creates the main query to be sent to the triplestore. - * Requests two sets of information: about the main resources and the dependent resources. - * - * @param mainResourceIris IRIs of main resources to be queried. - * @param dependentResourceIris IRIs of dependent resources to be queried. - * @param valueObjectIris IRIs of value objects to be queried (for both main and dependent resources) - * @param targetSchema the target API schema. - * @param schemaOptions the schema options submitted with the request. - * @return the main [[ConstructQuery]] query to be executed. - */ + * Creates the main query to be sent to the triplestore. + * Requests two sets of information: about the main resources and the dependent resources. + * + * @param mainResourceIris IRIs of main resources to be queried. + * @param dependentResourceIris IRIs of dependent resources to be queried. + * @param valueObjectIris IRIs of value objects to be queried (for both main and dependent resources) + * @param targetSchema the target API schema. + * @param schemaOptions the schema options submitted with the request. + * @return the main [[ConstructQuery]] query to be executed. + */ def createMainQuery(mainResourceIris: Set[IriRef], dependentResourceIris: Set[IriRef], valueObjectIris: Set[IRI], targetSchema: ApiV2Schema, schemaOptions: Set[SchemaOption], settings: SettingsImpl): ConstructQuery = { import GravsearchConstants._ diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/AbstractPrequeryGenerator.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/AbstractPrequeryGenerator.scala index 2b1b81c8ae..034e354306 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/AbstractPrequeryGenerator.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/AbstractPrequeryGenerator.scala @@ -41,40 +41,17 @@ object AbstractPrequeryGenerator { * @param typeInspectionResult the result of running type inspection on the Gravsearch input. * @param querySchema the ontology schema used in the input Gravsearch query. */ -abstract class AbstractPrequeryGenerator(typeInspectionResult: GravsearchTypeInspectionResult, querySchema: ApiV2Schema) extends WhereTransformer { - +abstract class AbstractPrequeryGenerator(constructClause: ConstructClause, + typeInspectionResult: GravsearchTypeInspectionResult, + querySchema: ApiV2Schema) extends WhereTransformer { protected implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - // Contains the variable representing the input query's main resource: knora-base:isMainResource - protected var mainResourceVariable: Option[QueryVariable] = None - - // getter method for public access - def getMainResourceVariable: QueryVariable = mainResourceVariable.getOrElse(throw GravsearchException("Could not get main resource variable from transformer")) - // a Set containing all `TypeableEntity` (keys of `typeInspectionResult`) that have already been processed // in order to prevent duplicates - protected val processedTypeInformationKeysWhereClause = mutable.Set.empty[TypeableEntity] - - // variables representing dependent resources - protected var dependentResourceVariables: mutable.Set[QueryVariable] = mutable.Set.empty - - // variables representing aggregated dependent resources - protected var dependentResourceVariablesGroupConcat: Set[QueryVariable] = Set.empty - - // getter method for public access - def getDependentResourceVariablesGroupConcat: Set[QueryVariable] = dependentResourceVariablesGroupConcat - - // variables representing value objects (including those for link values) - protected var valueObjectVariables: mutable.Set[QueryVariable] = mutable.Set.empty - - // variables representing aggregated value objects - protected var valueObjectVarsGroupConcat: Set[QueryVariable] = Set.empty - - // getter method for public access - def getValueObjectVarsGroupConcat: Set[QueryVariable] = valueObjectVarsGroupConcat + private val processedTypeInformationKeysWhereClause = mutable.Set.empty[TypeableEntity] // suffix appended to variables that are returned by a SPARQL aggregation function. - val groupConcatVariableSuffix = "__Concat" + protected val groupConcatVariableSuffix = "__Concat" // A set of types that can be treated as dates by the knora-api:toSimpleDate function. private val dateTypes: Set[IRI] = Set(OntologyConstants.KnoraApiV2Complex.DateValue, OntologyConstants.KnoraApiV2Complex.StandoffTag) @@ -99,7 +76,7 @@ abstract class AbstractPrequeryGenerator(typeInspectionResult: GravsearchTypeIns * @param useInOrderBy if `true`, the generated variable can be used in ORDER BY. * @return `true` if the generated variable was saved, `false` if it had already been saved. */ - protected def addGeneratedVariableForValueLiteral(valueVar: QueryVariable, generatedVar: QueryVariable, useInOrderBy: Boolean = true): Boolean = { + private def addGeneratedVariableForValueLiteral(valueVar: QueryVariable, generatedVar: QueryVariable, useInOrderBy: Boolean = true): Boolean = { val currentGeneratedVars = valueVariablesAutomaticallyGenerated.getOrElse(valueVar, Set.empty[GeneratedQueryVariable]) if (!currentGeneratedVars.exists(currentGeneratedVar => currentGeneratedVar.variable == generatedVar)) { @@ -132,39 +109,49 @@ abstract class AbstractPrequeryGenerator(typeInspectionResult: GravsearchTypeIns } // Generated statements for date literals, so we don't generate the same statements twice. - protected val generatedDateStatements = mutable.Set.empty[StatementPattern] + private val generatedDateStatements = mutable.Set.empty[StatementPattern] // Variables generated to represent marked-up text in standoff, so we don't generate the same variables twice. - protected val standoffMarkedUpVariables = mutable.Set.empty[QueryVariable] + private val standoffMarkedUpVariables = mutable.Set.empty[QueryVariable] /** - * Checks if a statement represents the knora-base:isMainResource statement and returns the query variable representing the main resource if so. - * - * @param statementPattern the statement pattern to be checked. - * @return query variable representing the main resource or None. + * The variable in the CONSTRUCT clause that represents the main resource. */ - protected def isMainResourceVariable(statementPattern: StatementPattern): Option[QueryVariable] = { - statementPattern.pred match { - case IriRef(iri, _) => - - val iriStr = iri.toString - - if (iriStr == OntologyConstants.KnoraApiV2Simple.IsMainResource || iriStr == OntologyConstants.KnoraApiV2Complex.IsMainResource) { - statementPattern.obj match { - case XsdLiteral(value, SmartIri(OntologyConstants.Xsd.Boolean)) if value.toBoolean => - statementPattern.subj match { - case queryVariable: QueryVariable => Some(queryVariable) - case _ => throw GravsearchException(s"The subject of ${iri.toSparql} must be a variable") + val mainResourceVariable: QueryVariable = { + val mainResourceQueryVariables = constructClause.statements.foldLeft(Set.empty[QueryVariable]) { + case (acc: Set[QueryVariable], statementPattern) => + statementPattern.pred match { + case IriRef(iri, _) => + + val iriStr = iri.toString + + if (iriStr == OntologyConstants.KnoraApiV2Simple.IsMainResource || iriStr == OntologyConstants.KnoraApiV2Complex.IsMainResource) { + statementPattern.obj match { + case XsdLiteral(value, SmartIri(OntologyConstants.Xsd.Boolean)) if value.toBoolean => + statementPattern.subj match { + case queryVariable: QueryVariable => acc + queryVariable + case _ => throw GravsearchException(s"The subject of knora-api:isMainResource must be a variable") + } + + case _ => acc } + } else { + acc + } - case _ => None - } - } else { - None + case _ => acc } + } - case _ => None + if (mainResourceQueryVariables.isEmpty) { + throw GravsearchException("CONSTRUCT clause contains no knora-api:isMainResource") + } + + if (mainResourceQueryVariables.size > 1) { + throw GravsearchException("CONSTRUCT clause contains more than one knora-api:isMainResource") } + + mainResourceQueryVariables.head } /** @@ -174,31 +161,12 @@ abstract class AbstractPrequeryGenerator(typeInspectionResult: GravsearchTypeIns * @param inputEntity the [[Entity]] to make the statements about. * @return a sequence of [[QueryPattern]] representing the additional statements. */ - protected def createAdditionalStatementsForNonPropertyType(nonPropertyTypeInfo: NonPropertyTypeInfo, inputEntity: Entity): Seq[QueryPattern] = { - if (OntologyConstants.KnoraApi.isKnoraApiV2Resource(nonPropertyTypeInfo.typeIri)) { + private def createAdditionalStatementsForNonPropertyType(nonPropertyTypeInfo: NonPropertyTypeInfo, inputEntity: Entity): Seq[QueryPattern] = { + if (nonPropertyTypeInfo.isResourceType) { // inputEntity is either source or target of a linking property // create additional statements in order to query permissions and other information for a resource - // add the inputEntity (a variable representing a resource) to the SELECT - inputEntity match { - case queryVar: QueryVariable => - // make sure that this is not the mainVar - mainResourceVariable match { - case Some(mainVar: QueryVariable) => - - if (mainVar != queryVar) { - // it is a variable representing a dependent resource - dependentResourceVariables += queryVar - } - - case None => () // TODO: What happens if the main resource variable has not been processed yet (Option would be None)? Shall rather an error be thrown here? - - } - - case _ => () - } - Seq( StatementPattern.makeInferred(subj = inputEntity, pred = IriRef(OntologyConstants.Rdf.Type.toSmartIri), obj = IriRef(OntologyConstants.KnoraBase.Resource.toSmartIri)), StatementPattern.makeExplicit(subj = inputEntity, pred = IriRef(OntologyConstants.KnoraBase.IsDeleted.toSmartIri), obj = XsdLiteral(value = "false", datatype = OntologyConstants.Xsd.Boolean.toSmartIri)) @@ -229,9 +197,6 @@ abstract class AbstractPrequeryGenerator(typeInspectionResult: GravsearchTypeIns ) ) - // add variable to collection representing value objects - valueObjectVariables += linkValueObjVar - // create an Entity that connects the subject of the linking property with the link value object val linkValueProp: Entity = linkPred match { case linkingPropQueryVar: QueryVariable => @@ -261,7 +226,7 @@ abstract class AbstractPrequeryGenerator(typeInspectionResult: GravsearchTypeIns ) } - protected def convertStatementForPropertyType(inputOrderBy: Seq[OrderCriterion])(propertyTypeInfo: PropertyTypeInfo, statementPattern: StatementPattern, typeInspectionResult: GravsearchTypeInspectionResult): Seq[QueryPattern] = { + private def convertStatementForPropertyType(inputOrderBy: Seq[OrderCriterion])(propertyTypeInfo: PropertyTypeInfo, statementPattern: StatementPattern, typeInspectionResult: GravsearchTypeInspectionResult): Seq[QueryPattern] = { /** * Ensures that if the object of a statement is a variable, and is used in the ORDER BY clause of the input query, the subject of the statement * is the main resource. Throws an exception otherwise. @@ -271,7 +236,7 @@ abstract class AbstractPrequeryGenerator(typeInspectionResult: GravsearchTypeIns def checkSubjectInOrderBy(objectVar: QueryVariable): Unit = { statementPattern.subj match { case subjectVar: QueryVariable => - if (!mainResourceVariable.contains(subjectVar) && inputOrderBy.exists(criterion => criterion.queryVariable == objectVar)) { + if (mainResourceVariable != subjectVar && inputOrderBy.exists(criterion => criterion.queryVariable == objectVar)) { throw GravsearchException(s"Variable ${objectVar.toSparql} is used in ORDER BY, but does not represent a value of the main resource") } @@ -317,12 +282,11 @@ abstract class AbstractPrequeryGenerator(typeInspectionResult: GravsearchTypeIns } val subjectIsResource: Boolean = maybeSubjectTypeIri.exists(iri => OntologyConstants.KnoraApi.isKnoraApiV2Resource(iri)) - val objectIsResource: Boolean = OntologyConstants.KnoraApi.isKnoraApiV2Resource(propertyTypeInfo.objectTypeIri) // Is the subject of the statement a resource? if (subjectIsResource) { // Yes. Is the object of the statement also a resource? - if (objectIsResource) { + if (propertyTypeInfo.objectIsResourceType) { // Yes. This is a link property. Make sure that the object is either an IRI or a variable (cannot be a literal). statementPattern.obj match { case _: IriRef => () @@ -357,8 +321,7 @@ abstract class AbstractPrequeryGenerator(typeInspectionResult: GravsearchTypeIns val objectVarIsValueObject = querySchema == ApiV2Simple || OntologyConstants.KnoraApiV2Complex.ValueClasses.contains(propertyTypeInfo.objectTypeIri.toString) if (objectVarIsValueObject) { - // The variable refers to a value object. Add it to the collection representing value objects. - valueObjectVariables += objectVar + // The variable refers to a value object. // Convert the statement to the internal schema, and add a statement to check that the value object is not marked as deleted. val valueObjectIsNotDeleted = StatementPattern.makeExplicit(subj = statementPattern.obj, pred = IriRef(OntologyConstants.KnoraBase.IsDeleted.toSmartIri), obj = XsdLiteral(value = "false", datatype = OntologyConstants.Xsd.Boolean.toSmartIri)) @@ -413,7 +376,7 @@ abstract class AbstractPrequeryGenerator(typeInspectionResult: GravsearchTypeIns if (querySchema == ApiV2Complex) { // Yes. If the subject is a standoff tag and the object is a resource, that's an error, because the client // has to use the knora-api:standoffLink function instead. - if (maybeSubjectTypeIri.contains(OntologyConstants.KnoraApiV2Complex.StandoffTag.toSmartIri) && objectIsResource) { + if (maybeSubjectTypeIri.contains(OntologyConstants.KnoraApiV2Complex.StandoffTag.toSmartIri) && propertyTypeInfo.objectIsResourceType) { throw GravsearchException(s"Invalid statement pattern (use the knora-api:standoffLink function instead): ${statementPattern.toSparql.trim}") } else { // Is the object of the statement a list node? @@ -476,7 +439,7 @@ abstract class AbstractPrequeryGenerator(typeInspectionResult: GravsearchTypeIns * @param conversionFuncForNonPropertyType the function to use to create additional statements. * @return a sequence of [[QueryPattern]] representing the additional statements. */ - protected def checkForNonPropertyTypeInfoForEntity(entity: Entity, typeInspectionResult: GravsearchTypeInspectionResult, processedTypeInfo: mutable.Set[TypeableEntity], conversionFuncForNonPropertyType: (NonPropertyTypeInfo, Entity) => Seq[QueryPattern]): Seq[QueryPattern] = { + private def checkForNonPropertyTypeInfoForEntity(entity: Entity, typeInspectionResult: GravsearchTypeInspectionResult, processedTypeInfo: mutable.Set[TypeableEntity], conversionFuncForNonPropertyType: (NonPropertyTypeInfo, Entity) => Seq[QueryPattern]): Seq[QueryPattern] = { val typesNotYetProcessed = typeInspectionResult.copy(entities = typeInspectionResult.entities -- processedTypeInfo) typesNotYetProcessed.getTypeOfEntity(entity) match { @@ -499,7 +462,7 @@ abstract class AbstractPrequeryGenerator(typeInspectionResult: GravsearchTypeIns * @param conversionFuncForPropertyType the function to use for the conversion. * @return a sequence of [[QueryPattern]] representing the converted statement. */ - protected def checkForPropertyTypeInfoForStatement(statementPattern: StatementPattern, typeInspectionResult: GravsearchTypeInspectionResult, conversionFuncForPropertyType: (PropertyTypeInfo, StatementPattern, GravsearchTypeInspectionResult) => Seq[QueryPattern]): Seq[QueryPattern] = { + private def checkForPropertyTypeInfoForStatement(statementPattern: StatementPattern, typeInspectionResult: GravsearchTypeInspectionResult, conversionFuncForPropertyType: (PropertyTypeInfo, StatementPattern, GravsearchTypeInspectionResult) => Seq[QueryPattern]): Seq[QueryPattern] = { typeInspectionResult.getTypeOfEntity(statementPattern.pred) match { case Some(propInfo: PropertyTypeInfo) => // process type information for the predicate into additional statements @@ -516,7 +479,7 @@ abstract class AbstractPrequeryGenerator(typeInspectionResult: GravsearchTypeIns // A Map of knora-api value types (both complex and simple) to the corresponding knora-base value predicates // that point to literals. This is used only for generating additional statements for ORDER BY clauses, so it only needs to include // types that have a meaningful order. - protected val valueTypesToValuePredsForOrderBy: Map[IRI, IRI] = Map( + private val valueTypesToValuePredsForOrderBy: Map[IRI, IRI] = Map( OntologyConstants.Xsd.Integer -> OntologyConstants.KnoraBase.ValueHasInteger, OntologyConstants.Xsd.Decimal -> OntologyConstants.KnoraBase.ValueHasDecimal, OntologyConstants.Xsd.Boolean -> OntologyConstants.KnoraBase.ValueHasBoolean, @@ -541,7 +504,7 @@ abstract class AbstractPrequeryGenerator(typeInspectionResult: GravsearchTypeIns * @param typeInspectionResult the type inspection result. * @return the converted statement pattern. */ - protected def statementPatternToInternalSchema(statementPattern: StatementPattern, typeInspectionResult: GravsearchTypeInspectionResult): StatementPattern = { + private def statementPatternToInternalSchema(statementPattern: StatementPattern, typeInspectionResult: GravsearchTypeInspectionResult): StatementPattern = { GravsearchQueryChecker.checkStatement( statementPattern = statementPattern, querySchema = querySchema, @@ -557,7 +520,7 @@ abstract class AbstractPrequeryGenerator(typeInspectionResult: GravsearchTypeIns * @param linkingPropertyQueryVariable variable representing a linking property. * @return variable representing the corresponding link value property. */ - protected def createlinkValuePropertyVariableFromLinkingPropertyVariable(linkingPropertyQueryVariable: QueryVariable): QueryVariable = { + private def createlinkValuePropertyVariableFromLinkingPropertyVariable(linkingPropertyQueryVariable: QueryVariable): QueryVariable = { SparqlTransformer.createUniqueVariableNameFromEntityAndProperty( base = linkingPropertyQueryVariable, propertyIri = OntologyConstants.KnoraBase.HasLinkToValue @@ -1617,7 +1580,7 @@ abstract class AbstractPrequeryGenerator(typeInspectionResult: GravsearchTypeIns } typeInspectionResult.getTypeOfEntity(linkSourceEntity) match { - case Some(NonPropertyTypeInfo(typeIri)) if OntologyConstants.KnoraApi.isKnoraApiV2Resource(typeIri) => () + case Some(nonPropertyTypeInfo: NonPropertyTypeInfo) if nonPropertyTypeInfo.isResourceType => () case _ => throw GravsearchException(s"The first argument of ${functionIri.toSparql} must represent a knora-api:Resource") } @@ -1636,7 +1599,7 @@ abstract class AbstractPrequeryGenerator(typeInspectionResult: GravsearchTypeIns } val statementsForTargetResource: Seq[QueryPattern] = typeInspectionResult.getTypeOfEntity(linkTargetEntity) match { - case Some(nonPropertyTpeInfo: NonPropertyTypeInfo) if OntologyConstants.KnoraApi.isKnoraApiV2Resource(nonPropertyTpeInfo.typeIri) => + case Some(nonPropertyTpeInfo: NonPropertyTypeInfo) if nonPropertyTpeInfo.isResourceType => // process the entity representing the target of the link createAdditionalStatementsForNonPropertyType(nonPropertyTpeInfo, linkTargetEntity) diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToCountPrequeryGenerator.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToCountPrequeryTransformer.scala similarity index 63% rename from webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToCountPrequeryGenerator.scala rename to webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToCountPrequeryTransformer.scala index e7f6ac9ac2..aaa709cec4 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToCountPrequeryGenerator.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToCountPrequeryTransformer.scala @@ -19,28 +19,25 @@ package org.knora.webapi.responders.v2.search.gravsearch.prequery +import org.knora.webapi.ApiV2Schema import org.knora.webapi.responders.v2.search._ import org.knora.webapi.responders.v2.search.gravsearch.types._ -import org.knora.webapi.util.IriConversions._ -import org.knora.webapi.{ApiV2Schema, GravsearchException, OntologyConstants} /** - * Transforms a preprocessed CONSTRUCT query into a SELECT query that returns only the IRIs and sort order of the main resources that matched - * the search criteria. This query will be used to get resource IRIs for a single page of results. These IRIs will be included in a CONSTRUCT - * query to get the actual results for the page. - * - * @param typeInspectionResult the result of type inspection of the original query. - */ -class NonTriplestoreSpecificGravsearchToCountPrequeryGenerator(typeInspectionResult: GravsearchTypeInspectionResult, querySchema: ApiV2Schema) extends AbstractPrequeryGenerator(typeInspectionResult, querySchema) with ConstructToSelectTransformer { - - def handleStatementInConstruct(statementPattern: StatementPattern): Unit = { - // Just identify the main resource variable and put it in mainResourceVariable. - - isMainResourceVariable(statementPattern) match { - case Some(queryVariable: QueryVariable) => mainResourceVariable = Some(queryVariable) - case None => () - } - } + * Transforms a preprocessed CONSTRUCT query into a SELECT query that returns only the IRIs and sort order of the main resources that matched + * the search criteria. This query will be used to get resource IRIs for a single page of results. These IRIs will be included in a CONSTRUCT + * query to get the actual results for the page. + * + * @param constructClause the CONSTRUCT clause from the input query. + * @param typeInspectionResult the result of type inspection of the input query. + * @param querySchema the ontology schema used in the input query. + */ +class NonTriplestoreSpecificGravsearchToCountPrequeryTransformer(constructClause: ConstructClause, + typeInspectionResult: GravsearchTypeInspectionResult, + querySchema: ApiV2Schema) + extends AbstractPrequeryGenerator(constructClause = constructClause, + typeInspectionResult = typeInspectionResult, + querySchema = querySchema) with ConstructToSelectTransformer { def transformStatementInWhere(statementPattern: StatementPattern, inputOrderBy: Seq[OrderCriterion]): Seq[QueryPattern] = { @@ -65,16 +62,9 @@ class NonTriplestoreSpecificGravsearchToCountPrequeryGenerator(typeInspectionRes } - def getSelectVariables: Seq[SelectQueryColumn] = { - - val mainResVar = mainResourceVariable match { - case Some(mainVar: QueryVariable) => mainVar - - case None => throw GravsearchException(s"No ${OntologyConstants.KnoraBase.IsMainResource.toSmartIri.toSparql} found in CONSTRUCT query.") - } - + def getSelectColumns: Seq[SelectQueryColumn] = { // return count aggregation function for main variable - Seq(Count(inputVariable = mainResVar, distinct = true, outputVariableName = "count")) + Seq(Count(inputVariable = mainResourceVariable, distinct = true, outputVariableName = "count")) } def getGroupBy(orderByCriteria: TransformedOrderBy): Seq[QueryVariable] = { diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryGenerator.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryGenerator.scala deleted file mode 100644 index 2d9d7dd7d7..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryGenerator.scala +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright © 2015-2019 the contributors (see Contributors.md). - * - * This file is part of Knora. - * - * Knora is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Knora is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public - * License along with Knora. If not, see . - */ - -package org.knora.webapi.responders.v2.search.gravsearch.prequery - -import org.knora.webapi._ -import org.knora.webapi.responders.v2.search._ -import org.knora.webapi.responders.v2.search.gravsearch.types._ -import org.knora.webapi.util.IriConversions._ - -/** - * Transforms a preprocessed CONSTRUCT query into a SELECT query that returns only the IRIs and sort order of the main resources that matched - * the search criteria. This query will be used to get resource IRIs for a single page of results. These IRIs will be included in a CONSTRUCT - * query to get the actual results for the page. - * - * @param typeInspectionResult the result of type inspection of the original query. - * @param querySchema ontology schema used in the input query. - * @param settings application settings. - */ -class NonTriplestoreSpecificGravsearchToPrequeryGenerator(typeInspectionResult: GravsearchTypeInspectionResult, querySchema: ApiV2Schema, settings: SettingsImpl) - extends AbstractPrequeryGenerator(typeInspectionResult, querySchema) with ConstructToSelectTransformer { - - /** - * Collects information from a statement pattern in the CONSTRUCT clause of the input query, e.g. variables - * that need to be returned by the SELECT. - * - * @param statementPattern the statement to be handled. - */ - override def handleStatementInConstruct(statementPattern: StatementPattern): Unit = { - // Just identify the main resource variable and put it in mainResourceVariable. - - isMainResourceVariable(statementPattern) match { - case Some(queryVariable: QueryVariable) => mainResourceVariable = Some(queryVariable) - case None => () - } - - } - - /** - * Transforms a [[StatementPattern]] in a WHERE clause into zero or more query patterns. - * - * @param statementPattern the statement to be transformed. - * @param inputOrderBy the ORDER BY clause in the input query. - * @return the result of the transformation. - */ - override def transformStatementInWhere(statementPattern: StatementPattern, inputOrderBy: Seq[OrderCriterion]): Seq[QueryPattern] = { - // Include any statements needed to meet the user's search criteria, but not statements that would be needed for permission checking or - // other information about the matching resources or values. - - processStatementPatternFromWhereClause( - statementPattern = statementPattern, - inputOrderBy = inputOrderBy - ) - } - - /** - * Transforms a [[FilterPattern]] in a WHERE clause into zero or more statement patterns. - * - * @param filterPattern the filter to be transformed. - * @return the result of the transformation. - */ - override def transformFilter(filterPattern: FilterPattern): Seq[QueryPattern] = { - - val filterExpression: TransformedFilterPattern = transformFilterPattern(filterPattern.expression, typeInspectionResult = typeInspectionResult, isTopLevel = true) - - filterExpression.expression match { - case Some(expression: Expression) => filterExpression.additionalPatterns :+ FilterPattern(expression) - - case None => filterExpression.additionalPatterns // no FILTER expression given - } - - } - - /** - * Returns the variables that should be included in the results of the SELECT query. This method will be called - * by [[QueryTraverser]] after the whole input query has been traversed. - * - * @return the variables that should be returned by the SELECT. - */ - override def getSelectVariables: Seq[SelectQueryColumn] = { - // Return the main resource variable and the generated variable that we're using for ordering. - - val dependentResourceGroupConcat: Set[GroupConcat] = dependentResourceVariables.map { - dependentResVar: QueryVariable => - GroupConcat(inputVariable = dependentResVar, - separator = AbstractPrequeryGenerator.groupConcatSeparator, - outputVariableName = dependentResVar.variableName + groupConcatVariableSuffix) - }.toSet - - dependentResourceVariablesGroupConcat = dependentResourceGroupConcat.map(_.outputVariable) - - val valueObjectGroupConcat = valueObjectVariables.map { - valueObjVar: QueryVariable => - GroupConcat(inputVariable = valueObjVar, - separator = AbstractPrequeryGenerator.groupConcatSeparator, - outputVariableName = valueObjVar.variableName + groupConcatVariableSuffix) - }.toSet - - valueObjectVarsGroupConcat = valueObjectGroupConcat.map(_.outputVariable) - - mainResourceVariable match { - case Some(mainVar: QueryVariable) => Seq(mainVar) ++ dependentResourceGroupConcat ++ valueObjectGroupConcat - - case None => throw GravsearchException(s"No ${OntologyConstants.KnoraBase.IsMainResource.toSmartIri.toSparql} found in CONSTRUCT query.") - } - - } - - /** - * Returns the criteria, if any, that should be used in the ORDER BY clause of the SELECT query. This method will be called - * by [[QueryTraverser]] after the whole input query has been traversed. - * - * @return the ORDER BY criteria, if any. - */ - override def getOrderBy(inputOrderBy: Seq[OrderCriterion]): TransformedOrderBy = { - - val transformedOrderBy = inputOrderBy.foldLeft(TransformedOrderBy()) { - case (acc, criterion) => - // A unique variable for the literal value of this value object should already have been created - - getGeneratedVariableForValueLiteralInOrderBy(criterion.queryVariable) match { - case Some(generatedVariable) => - // Yes. Use the already generated variable in the ORDER BY. - acc.copy( - orderBy = acc.orderBy :+ OrderCriterion(queryVariable = generatedVariable, isAscending = criterion.isAscending) - ) - - case None => - // No. - throw GravsearchException(s"Not value literal variable was automatically generated for ${criterion.queryVariable}") - - } - } - - // main resource variable as order by criterion - val orderByMainResVar: OrderCriterion = OrderCriterion( - queryVariable = mainResourceVariable.getOrElse(throw GravsearchException(s"No ${OntologyConstants.KnoraBase.IsMainResource.toSmartIri.toSparql} found in CONSTRUCT query")), - isAscending = true - ) - - // order by: user provided variables and main resource variable - // all variables present in the GROUP BY must be included in the order by statements to make the results predictable for paging - transformedOrderBy.copy( - orderBy = transformedOrderBy.orderBy :+ orderByMainResVar - ) - } - - /** - * Creates the GROUP BY statement based on the ORDER BY statement. - * - * @param orderByCriteria the criteria used to sort the query results. They have to be included in the GROUP BY statement, otherwise they are unbound. - * @return a list of variables that the result rows are grouped by. - */ - def getGroupBy(orderByCriteria: TransformedOrderBy): Seq[QueryVariable] = { - // get they query variables form the order by criteria and return them in reverse order: - // main resource variable first, followed by other sorting criteria, if any. - orderByCriteria.orderBy.map(_.queryVariable).reverse - } - - /** - * Gets the maximal amount of result rows to be returned by the prequery. - * - * @return the LIMIT, if any. - */ - def getLimit: Int = { - // get LIMIT from settings - settings.v2ResultsPerPage - } - - /** - * Gets the OFFSET to be used in the prequery (needed for paging). - * - * @param inputQueryOffset the OFFSET provided in the input query. - * @param limit the maximum amount of result rows to be returned by the prequery. - * @return the OFFSET. - */ - def getOffset(inputQueryOffset: Long, limit: Int): Long = { - - if (inputQueryOffset < 0) throw AssertionException("Negative OFFSET is illegal.") - - // determine offset for paging -> multiply given offset with limit (indicating the maximum amount of results per page). - inputQueryOffset * limit - - } - - override def optimiseQueryPatternOrder(patterns: Seq[QueryPattern]): Seq[QueryPattern] = patterns - - override def transformLuceneQueryPattern(luceneQueryPattern: LuceneQueryPattern): Seq[QueryPattern] = Seq(luceneQueryPattern) -} - diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformer.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformer.scala new file mode 100644 index 0000000000..4e9bc5aba4 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformer.scala @@ -0,0 +1,378 @@ +/* + * Copyright © 2015-2019 the contributors (see Contributors.md). + * + * This file is part of Knora. + * + * Knora is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Knora is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with Knora. If not, see . + */ + +package org.knora.webapi.responders.v2.search.gravsearch.prequery + +import org.knora.webapi._ +import org.knora.webapi.responders.v2.search._ +import org.knora.webapi.responders.v2.search.gravsearch.types._ + +/** + * Transforms a preprocessed CONSTRUCT query into a SELECT query that returns only the IRIs and sort order of the main resources that matched + * the search criteria and are requested by client in the input query's WHERE clause. This query will be used to get resource IRIs for a single + * page of results. These IRIs will be included in a CONSTRUCT query to get the actual results for the page. + * + * @param constructClause the CONSTRUCT clause from the input query. + * @param typeInspectionResult the result of type inspection of the input query. + * @param querySchema the ontology schema used in the input query. + * @param settings application settings. + */ +class NonTriplestoreSpecificGravsearchToPrequeryTransformer(constructClause: ConstructClause, + typeInspectionResult: GravsearchTypeInspectionResult, + querySchema: ApiV2Schema, + settings: SettingsImpl) + extends AbstractPrequeryGenerator( + constructClause = constructClause, + typeInspectionResult = typeInspectionResult, + querySchema = querySchema + ) with ConstructToSelectTransformer { + + import AbstractPrequeryGenerator._ + + /** + * Transforms a [[StatementPattern]] in a WHERE clause into zero or more query patterns. + * + * @param statementPattern the statement to be transformed. + * @param inputOrderBy the ORDER BY clause in the input query. + * @return the result of the transformation. + */ + override def transformStatementInWhere(statementPattern: StatementPattern, inputOrderBy: Seq[OrderCriterion]): Seq[QueryPattern] = { + // Include any statements needed to meet the user's search criteria, but not statements that would be needed for permission checking or + // other information about the matching resources or values. + + processStatementPatternFromWhereClause( + statementPattern = statementPattern, + inputOrderBy = inputOrderBy + ) + } + + /** + * Transforms a [[FilterPattern]] in a WHERE clause into zero or more statement patterns. + * + * @param filterPattern the filter to be transformed. + * @return the result of the transformation. + */ + override def transformFilter(filterPattern: FilterPattern): Seq[QueryPattern] = { + + val filterExpression: TransformedFilterPattern = transformFilterPattern(filterPattern.expression, typeInspectionResult = typeInspectionResult, isTopLevel = true) + + filterExpression.expression match { + case Some(expression: Expression) => filterExpression.additionalPatterns :+ FilterPattern(expression) + + case None => filterExpression.additionalPatterns // no FILTER expression given + } + + } + + /** + * Determines whether an entity has a property type that meets the specified condition. + * + * @param entity the entity. + * @param condition the condition. + * @return `true` if the variable has a property type and the condition is met. + */ + private def entityHasPropertyType(entity: Entity, condition: PropertyTypeInfo => Boolean): Boolean = { + GravsearchTypeInspectionUtil.maybeTypeableEntity(entity) match { + case Some(typeableEntity) => + typeInspectionResult.entities.get(typeableEntity) match { + case Some(propertyTypeInfo: PropertyTypeInfo) => condition(propertyTypeInfo) + case Some(_: NonPropertyTypeInfo) => false + case None => false + } + + case None => false + } + } + + /** + * Determines whether an entity has a non-property type that meets the specified condition. + * + * @param entity the entity. + * @param condition the condition. + * @return `true` if the variable has a non-property type and the condition is met. + */ + private def entityHasNonPropertyType(entity: Entity, condition: NonPropertyTypeInfo => Boolean): Boolean = { + GravsearchTypeInspectionUtil.maybeTypeableEntity(entity) match { + case Some(typeableEntity) => + typeInspectionResult.entities.get(typeableEntity) match { + case Some(nonPropertyTypeInfo: NonPropertyTypeInfo) => condition(nonPropertyTypeInfo) + case Some(_: PropertyTypeInfo) => false + case None => false + } + + case None => false + } + } + + /** + * Checks that an [[Entity]] is a [[QueryVariable]]. + * + * @param entity the entity. + * @return the entity as a [[QueryVariable]]. + */ + private def entityToQueryVariable(entity: Entity): QueryVariable = { + entity match { + case queryVariable: QueryVariable => queryVariable + case other => throw GravsearchException(s"Expected a variable in CONSTRUCT clause, but found ${other.toSparql}") + } + } + + /** + * All the variables used in the Gravsearch CONSTRUCT clause. + */ + private val variablesInConstruct: Set[QueryVariable] = constructClause.statements.flatMap { + statementPattern: StatementPattern => + Seq(statementPattern.subj, statementPattern.obj).flatMap { + case queryVariable: QueryVariable => Some(queryVariable) + case _ => None + } + }.toSet + + /** + * The variables representing values in the CONSTRUCT clause, grouped by resource. + */ + private val valueVariablesPerResourceInConstruct: Map[Entity, Set[QueryVariable]] = + constructClause.statements.filter { + statementPattern: StatementPattern => + // Find statements in which the subject is a resource, the predicate is a value property, + // and the object is a value. + entityHasNonPropertyType(entity = statementPattern.subj, condition = _.isResourceType) && + entityHasPropertyType(entity = statementPattern.pred, condition = _.objectIsValueType) && + entityHasNonPropertyType(entity = statementPattern.obj, condition = _.isValueType) + }.map { + statementPattern => statementPattern.subj -> entityToQueryVariable(statementPattern.obj) + }.groupBy { + case (resourceEntity, _) => resourceEntity + }.map { + case (resourceEntity, resourceValueTuples) => + // Simplify the result of groupBy by replacing each tuple with its second element. + resourceEntity -> resourceValueTuples.map(_._2).toSet + } + + /** + * The variables representing resources in the CONSTRUCT clause. + */ + private val resourceVariablesInConstruct: Set[QueryVariable] = variablesInConstruct.filter { + queryVariable => entityHasNonPropertyType(entity = queryVariable, condition = _.isResourceType) + } + + // If a variable is used as the subject or object of a statement pattern in the CONSTRUCT clause, and it + // doesn't represent a resource or a value, that's an error. + + private val valueVariablesInConstruct: Set[QueryVariable] = valueVariablesPerResourceInConstruct.values.flatten.toSet + + private val invalidVariablesInConstruct: Set[QueryVariable] = variablesInConstruct -- valueVariablesInConstruct -- resourceVariablesInConstruct + + if (invalidVariablesInConstruct.nonEmpty) { + val invalidVariablesWithTypes: Set[String] = invalidVariablesInConstruct.map { + queryVariable => + val typeableEntity = GravsearchTypeInspectionUtil.toTypeableEntity(queryVariable) + + val typeName = typeInspectionResult.entities.get(typeableEntity).map { + case _: PropertyTypeInfo => "property" + case nonPropertyTypeInfo: NonPropertyTypeInfo => s"<${nonPropertyTypeInfo.typeIri}>" + }.getOrElse("unknown type") + + s"${queryVariable.toSparql} ($typeName)" + } + + throw GravsearchException(s"One or more variables in the Gravsearch CONSTRUCT clause have unknown or invalid types: ${invalidVariablesWithTypes.mkString(", ")}") + } + + /** + * The [[GroupConcat]] expressions generated for values in the prequery, grouped by resource entity. + */ + private val valueGroupConcatsPerResource: Map[Entity, Set[GroupConcat]] = { + // Generate variables representing link values and group them by containing resource entity. + val linkValueVariablesPerResourceGeneratedForConstruct: Map[Entity, Set[QueryVariable]] = constructClause.statements.filter { + statementPattern: StatementPattern => + // Find statements in which the subject is a resource, the predicate is a link property, + // and the object is a resource. + entityHasNonPropertyType(entity = statementPattern.subj, condition = _.isResourceType) && + entityHasPropertyType(entity = statementPattern.pred, condition = _.objectIsResourceType) && + entityHasNonPropertyType(entity = statementPattern.obj, condition = _.isResourceType) + }.map { + statementPattern => + // For each of those statements, make a variable representing a link value. + statementPattern.subj -> SparqlTransformer.createUniqueVariableFromStatementForLinkValue( + baseStatement = statementPattern + ) + }.groupBy { + case (resourceEntity, _) => resourceEntity + }.map { + case (resourceEntity, resourceValueTuples) => + // Simplify the result of groupBy by replacing each tuple with its second element. + resourceEntity -> resourceValueTuples.map(_._2).toSet + } + + // Make a GroupConcat for each value variable. + (valueVariablesPerResourceInConstruct.keySet ++ linkValueVariablesPerResourceGeneratedForConstruct.keySet).map { + resourceEntity: Entity => + val valueVariables: Set[QueryVariable] = valueVariablesPerResourceInConstruct.getOrElse(resourceEntity, Set.empty) ++ + linkValueVariablesPerResourceGeneratedForConstruct.getOrElse(resourceEntity, Set.empty) + + val groupConcats: Set[GroupConcat] = valueVariables.map { + valueObjVar: QueryVariable => + GroupConcat(inputVariable = valueObjVar, + separator = groupConcatSeparator, + outputVariableName = valueObjVar.variableName + groupConcatVariableSuffix) + } + + resourceEntity -> groupConcats + } + }.toMap + + /** + * The variables used in [[GroupConcat]] expressions in the prequery, grouped by resource entity. + */ + private val valueGroupConcatVariablesPerResource: Map[Entity, Set[QueryVariable]] = { + valueGroupConcatsPerResource.map { + case (resourceEntity: Entity, groupConcats: Set[GroupConcat]) => + resourceEntity -> groupConcats.map(_.outputVariable) + } + } + + /** + * A GROUP_CONCAT expression for each value variable. + */ + private val valueObjectGroupConcat: Set[GroupConcat] = valueGroupConcatsPerResource.values.flatten.toSet + + /** + * Variables representing dependent resources in the CONSTRUCT clause. + */ + private val dependentResourceVariablesInConstruct: Set[QueryVariable] = resourceVariablesInConstruct - mainResourceVariable + + /** + * A GROUP_CONCAT expression for each dependent resource variable. + */ + private val dependentResourceGroupConcat: Set[GroupConcat] = dependentResourceVariablesInConstruct.map { + dependentResVar: QueryVariable => + GroupConcat( + inputVariable = dependentResVar, + separator = groupConcatSeparator, + outputVariableName = dependentResVar.variableName + groupConcatVariableSuffix + ) + } + + /** + * The variable names used in the GROUP_CONCAT expressions for dependent resources. + */ + val dependentResourceVariablesGroupConcat: Set[QueryVariable] = dependentResourceGroupConcat.map(_.outputVariable) + + /** + * The variable names used in the GROUP_CONCAT expressions for values. + */ + val valueObjectVariablesGroupConcat: Set[QueryVariable] = valueGroupConcatVariablesPerResource.values.flatten.toSet + + /** + * Returns the columns to be specified in the SELECT query. + */ + override def getSelectColumns: Seq[SelectQueryColumn] = { + Seq(mainResourceVariable) ++ dependentResourceGroupConcat ++ valueObjectGroupConcat + } + + /** + * Returns the variables that were used in [[GroupConcat]] expressions in the prequery to represent values + * that were mentioned in the CONSTRUCT clause of the input query, for the given entity representing a resource. + */ + def getValueGroupConcatVariablesForResource(resourceEntity: Entity): Set[QueryVariable] = { + valueGroupConcatVariablesPerResource.getOrElse(resourceEntity, Set.empty) + } + + /** + * Returns the criteria, if any, that should be used in the ORDER BY clause of the SELECT query. This method will be called + * by [[QueryTraverser]] after the whole input query has been traversed. + * + * @return the ORDER BY criteria, if any. + */ + override def getOrderBy(inputOrderBy: Seq[OrderCriterion]): TransformedOrderBy = { + + val transformedOrderBy = inputOrderBy.foldLeft(TransformedOrderBy()) { + case (acc, criterion) => + // A unique variable for the literal value of this value object should already have been created + + getGeneratedVariableForValueLiteralInOrderBy(criterion.queryVariable) match { + case Some(generatedVariable) => + // Yes. Use the already generated variable in the ORDER BY. + acc.copy( + orderBy = acc.orderBy :+ OrderCriterion(queryVariable = generatedVariable, isAscending = criterion.isAscending) + ) + + case None => + // No. + throw GravsearchException(s"Not value literal variable was automatically generated for ${criterion.queryVariable}") + + } + } + + // main resource variable as order by criterion + val orderByMainResVar: OrderCriterion = OrderCriterion( + queryVariable = mainResourceVariable, + isAscending = true + ) + + // order by: user provided variables and main resource variable + // all variables present in the GROUP BY must be included in the order by statements to make the results predictable for paging + transformedOrderBy.copy( + orderBy = transformedOrderBy.orderBy :+ orderByMainResVar + ) + } + + /** + * Creates the GROUP BY statement based on the ORDER BY statement. + * + * @param orderByCriteria the criteria used to sort the query results. They have to be included in the GROUP BY statement, otherwise they are unbound. + * @return a list of variables that the result rows are grouped by. + */ + def getGroupBy(orderByCriteria: TransformedOrderBy): Seq[QueryVariable] = { + // get they query variables form the order by criteria and return them in reverse order: + // main resource variable first, followed by other sorting criteria, if any. + orderByCriteria.orderBy.map(_.queryVariable).reverse + } + + /** + * Gets the maximal amount of result rows to be returned by the prequery. + * + * @return the LIMIT, if any. + */ + def getLimit: Int = { + // get LIMIT from settings + settings.v2ResultsPerPage + } + + /** + * Gets the OFFSET to be used in the prequery (needed for paging). + * + * @param inputQueryOffset the OFFSET provided in the input query. + * @param limit the maximum amount of result rows to be returned by the prequery. + * @return the OFFSET. + */ + def getOffset(inputQueryOffset: Long, limit: Int): Long = { + + if (inputQueryOffset < 0) throw AssertionException("Negative OFFSET is illegal.") + + // determine offset for paging -> multiply given offset with limit (indicating the maximum amount of results per page). + inputQueryOffset * limit + + } + + override def transformLuceneQueryPattern(luceneQueryPattern: LuceneQueryPattern): Seq[QueryPattern] = Seq(luceneQueryPattern) + + override def optimiseQueryPatternOrder(patterns: Seq[QueryPattern]): Seq[QueryPattern] = patterns +} diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/types/GravsearchTypeInspectionResult.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/types/GravsearchTypeInspectionResult.scala index b27e96cfd0..fb96d34853 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/types/GravsearchTypeInspectionResult.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/types/GravsearchTypeInspectionResult.scala @@ -19,6 +19,7 @@ package org.knora.webapi.responders.v2.search.gravsearch.types +import org.knora.webapi.OntologyConstants import org.knora.webapi.responders.v2.search.{Entity, IriRef, QueryVariable} import org.knora.webapi.util.SmartIri @@ -34,6 +35,16 @@ sealed trait GravsearchEntityTypeInfo */ case class PropertyTypeInfo(objectTypeIri: SmartIri) extends GravsearchEntityTypeInfo { override def toString: String = s"knora-api:objectType ${IriRef(objectTypeIri).toSparql}" + + /** + * `true` if the property's object type is a resource type. + */ + val objectIsResourceType: Boolean = OntologyConstants.KnoraApi.isKnoraApiV2Resource(objectTypeIri) + + /** + * `true` if the property's object type is a value type. + */ + val objectIsValueType: Boolean = GravsearchTypeInspectionUtil.GravsearchValueTypeIris.contains(objectTypeIri.toString) } /** @@ -44,6 +55,16 @@ case class PropertyTypeInfo(objectTypeIri: SmartIri) extends GravsearchEntityTyp */ case class NonPropertyTypeInfo(typeIri: SmartIri) extends GravsearchEntityTypeInfo { override def toString: String = s"rdf:type ${IriRef(typeIri).toSparql}" + + /** + * `true` if this is a resource type. + */ + val isResourceType: Boolean = OntologyConstants.KnoraApi.isKnoraApiV2Resource(typeIri) + + /** + * `true` if this is a value type. + */ + val isValueType: Boolean = GravsearchTypeInspectionUtil.GravsearchValueTypeIris.contains(typeIri.toString) } /** diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/types/GravsearchTypeInspectionUtil.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/types/GravsearchTypeInspectionUtil.scala index dca5ef3428..3ce6cff2f7 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/types/GravsearchTypeInspectionUtil.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/types/GravsearchTypeInspectionUtil.scala @@ -69,16 +69,15 @@ object GravsearchTypeInspectionUtil { } /** - * The IRIs of non-property types that Gravsearch type inspectors return. - */ - val GravsearchTypeIris: Set[IRI] = Set( + * The IRIs of value types that Gravsearch type inspectors return. + */ + val GravsearchValueTypeIris: Set[IRI] = Set( OntologyConstants.Xsd.Boolean, OntologyConstants.Xsd.String, OntologyConstants.Xsd.Integer, OntologyConstants.Xsd.Decimal, OntologyConstants.Xsd.Uri, OntologyConstants.Xsd.DateTimeStamp, - OntologyConstants.KnoraApiV2Simple.Resource, OntologyConstants.KnoraApiV2Simple.Date, OntologyConstants.KnoraApiV2Simple.Geom, OntologyConstants.KnoraApiV2Simple.Geoname, @@ -86,8 +85,6 @@ object GravsearchTypeInspectionUtil { OntologyConstants.KnoraApiV2Simple.Color, OntologyConstants.KnoraApiV2Simple.File, OntologyConstants.KnoraApiV2Simple.ListNode, - OntologyConstants.KnoraApiV2Complex.Resource, - OntologyConstants.KnoraApiV2Complex.StandoffTag, OntologyConstants.KnoraApiV2Complex.BooleanValue, OntologyConstants.KnoraApiV2Complex.TextValue, OntologyConstants.KnoraApiV2Complex.IntValue, @@ -101,7 +98,16 @@ object GravsearchTypeInspectionUtil { OntologyConstants.KnoraApiV2Complex.ColorValue, OntologyConstants.KnoraApiV2Complex.IntervalValue, OntologyConstants.KnoraApiV2Complex.TimeValue, - OntologyConstants.KnoraApiV2Complex.FileValue, + OntologyConstants.KnoraApiV2Complex.FileValue + ) + + /** + * The IRIs of non-property types that Gravsearch type inspectors return. + */ + val GravsearchTypeIris: Set[IRI] = GravsearchValueTypeIris ++ Set( + OntologyConstants.KnoraApiV2Simple.Resource, + OntologyConstants.KnoraApiV2Complex.Resource, + OntologyConstants.KnoraApiV2Complex.StandoffTag, OntologyConstants.KnoraApiV2Complex.KnoraProject ) diff --git a/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala b/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala index d0ed7674e9..988af56bbb 100644 --- a/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala @@ -1321,9 +1321,11 @@ object ConstructResponseUtilV2 { * @param mainResourcesAndValueRdfData the query results. * @param orderByResourceIri the order in which the resources should be returned. This sequence * contains the resource IRIs received from the triplestore before filtering - * for permissions. + * for permissions, but after filtering for duplicates. + * @param pageSizeBeforeFiltering the number of resources returned before filtering for permissions and duplicates. * @param mappings the mappings to convert standoff to XML, if any. * @param queryStandoff if `true`, make separate queries to get the standoff for text values. + * @param calculateMayHaveMoreResults if `true`, calculate whether there may be more results for the query. * @param versionDate if defined, represents the requested time in the the resources' version history. * @param responderManager the Knora responder manager. * @param targetSchema the schema of response. @@ -1333,6 +1335,7 @@ object ConstructResponseUtilV2 { */ def createApiResponse(mainResourcesAndValueRdfData: MainResourcesAndValueRdfData, orderByResourceIri: Seq[IRI], + pageSizeBeforeFiltering: Int, mappings: Map[IRI, MappingAndXSLTransformation] = Map.empty[IRI, MappingAndXSLTransformation], queryStandoff: Boolean, calculateMayHaveMoreResults: Boolean, @@ -1366,7 +1369,7 @@ object ConstructResponseUtilV2 { // If we got a full page of results from the triplestore (before filtering for permissions), there // might be at least one more page of results that the user could request. - mayHaveMoreResults = calculateMayHaveMoreResults && orderByResourceIri.size == settings.v2ResultsPerPage + mayHaveMoreResults = calculateMayHaveMoreResults && pageSizeBeforeFiltering == settings.v2ResultsPerPage } yield ReadResourcesSequenceV2( resources = resources, hiddenResourceIris = mainResourcesAndValueRdfData.hiddenResourceIris, diff --git a/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala b/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala index c86cac85d1..c9d8797cd0 100644 --- a/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala +++ b/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala @@ -40,6 +40,7 @@ import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM import org.knora.webapi.messages.store.triplestoremessages.{SparqlAskRequest, SparqlAskResponse} import org.knora.webapi.messages.v1.responder.projectmessages.ProjectInfoV1 import org.knora.webapi.messages.v2.responder.KnoraContentV2 +import org.knora.webapi.messages.v2.responder.resourcemessages.ReadResourceV2 import org.knora.webapi.messages.v2.responder.standoffmessages._ import org.knora.webapi.util.IriConversions._ import org.knora.webapi.util.JavaUtil.Optional @@ -3003,7 +3004,7 @@ class StringFormatter private(val maybeSettings: Option[SettingsImpl] = None, ma * @param linkValuePropertyIri the IRI of the property that points to the `LinkValue`. * @return the IRI of the corresponding link property. */ - def linkValuePropertyIri2LinkPropertyIri(linkValuePropertyIri: IRI): IRI = { + def linkValuePropertyIriToLinkPropertyIri(linkValuePropertyIri: IRI): IRI = { implicit val stringFormatter: StringFormatter = this linkValuePropertyIri.toSmartIri.fromLinkValuePropToLinkProp.toString diff --git a/webapi/src/test/resources/test-data/searchR2RV2/IncomingLinksForBook.jsonld b/webapi/src/test/resources/test-data/searchR2RV2/IncomingLinksForBook.jsonld index d9bdfb0ff4..b7cf57de74 100644 --- a/webapi/src/test/resources/test-data/searchR2RV2/IncomingLinksForBook.jsonld +++ b/webapi/src/test/resources/test-data/searchR2RV2/IncomingLinksForBook.jsonld @@ -28,7 +28,7 @@ "knora-api:hasPermissions" : "CR knora-admin:Creator|V knora-admin:UnknownUser,knora-admin:KnownUser,knora-admin:ProjectMember", "knora-api:linkValueHasTarget" : { "@id" : "http://rdfh.ch/0803/8be1b7cf7103", - "@type" : "http://0.0.0.0:3333/ontology/0803/incunabula/v2#book", + "@type" : "incunabula:book", "knora-api:arkUrl" : { "@type" : "xsd:anyURI", "@value" : "http://0.0.0.0:3336/ark:/72163/1/0803/8be1b7cf7103G" @@ -71,8 +71,9 @@ "rdfs:label" : "Lateinische Übersetzung", "@context" : { "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", "rdfs" : "http://www.w3.org/2000/01/rdf-schema#", - "xsd" : "http://www.w3.org/2001/XMLSchema#", - "knora-api" : "http://api.knora.org/ontology/knora-api/v2#" + "incunabula" : "http://0.0.0.0:3333/ontology/0803/incunabula/v2#", + "xsd" : "http://www.w3.org/2001/XMLSchema#" } } \ No newline at end of file diff --git a/webapi/src/test/resources/test-data/searchR2RV2/LinkObjectsToBooks.jsonld b/webapi/src/test/resources/test-data/searchR2RV2/LinkObjectsToBooks.jsonld index fca0dbc717..5238bf809e 100644 --- a/webapi/src/test/resources/test-data/searchR2RV2/LinkObjectsToBooks.jsonld +++ b/webapi/src/test/resources/test-data/searchR2RV2/LinkObjectsToBooks.jsonld @@ -29,7 +29,7 @@ "knora-api:hasPermissions" : "CR knora-admin:Creator|V knora-admin:UnknownUser,knora-admin:KnownUser,knora-admin:ProjectMember", "knora-api:linkValueHasTarget" : { "@id" : "http://rdfh.ch/0803/8be1b7cf7103", - "@type" : "http://0.0.0.0:3333/ontology/0803/incunabula/v2#book", + "@type" : "incunabula:book", "knora-api:arkUrl" : { "@type" : "xsd:anyURI", "@value" : "http://0.0.0.0:3336/ark:/72163/1/0803/8be1b7cf7103G" @@ -75,7 +75,7 @@ "knora-api:hasPermissions" : "CR knora-admin:Creator|V knora-admin:UnknownUser,knora-admin:KnownUser,knora-admin:ProjectMember", "knora-api:linkValueHasTarget" : { "@id" : "http://rdfh.ch/0803/5e77e98d2603", - "@type" : "http://0.0.0.0:3333/ontology/0803/incunabula/v2#book", + "@type" : "incunabula:book", "knora-api:arkUrl" : { "@type" : "xsd:anyURI", "@value" : "http://0.0.0.0:3336/ark:/72163/1/0803/5e77e98d2603U" @@ -146,7 +146,7 @@ "knora-api:hasPermissions" : "CR knora-admin:Creator|V knora-admin:UnknownUser,knora-admin:KnownUser,knora-admin:ProjectMember", "knora-api:linkValueHasTarget" : { "@id" : "http://rdfh.ch/0803/c5058f3a", - "@type" : "http://0.0.0.0:3333/ontology/0803/incunabula/v2#book", + "@type" : "incunabula:book", "knora-api:arkUrl" : { "@type" : "xsd:anyURI", "@value" : "http://0.0.0.0:3336/ark:/72163/1/0803/c5058f3a5" @@ -217,7 +217,7 @@ "knora-api:hasPermissions" : "CR knora-admin:Creator|V knora-admin:UnknownUser,knora-admin:KnownUser,knora-admin:ProjectMember", "knora-api:linkValueHasTarget" : { "@id" : "http://rdfh.ch/0803/21abac2162", - "@type" : "http://0.0.0.0:3333/ontology/0803/incunabula/v2#book", + "@type" : "incunabula:book", "knora-api:arkUrl" : { "@type" : "xsd:anyURI", "@value" : "http://0.0.0.0:3336/ark:/72163/1/0803/21abac2162A" @@ -263,7 +263,7 @@ "knora-api:hasPermissions" : "CR knora-admin:Creator|V knora-admin:UnknownUser,knora-admin:KnownUser,knora-admin:ProjectMember", "knora-api:linkValueHasTarget" : { "@id" : "http://rdfh.ch/0803/e41ab5695c", - "@type" : "http://0.0.0.0:3333/ontology/0803/incunabula/v2#book", + "@type" : "incunabula:book", "knora-api:arkUrl" : { "@type" : "xsd:anyURI", "@value" : "http://0.0.0.0:3336/ark:/72163/1/0803/e41ab5695cN" @@ -309,7 +309,7 @@ "knora-api:hasPermissions" : "CR knora-admin:Creator|V knora-admin:UnknownUser,knora-admin:KnownUser,knora-admin:ProjectMember", "knora-api:linkValueHasTarget" : { "@id" : "http://rdfh.ch/0803/c5058f3a", - "@type" : "http://0.0.0.0:3333/ontology/0803/incunabula/v2#book", + "@type" : "incunabula:book", "knora-api:arkUrl" : { "@type" : "xsd:anyURI", "@value" : "http://0.0.0.0:3336/ark:/72163/1/0803/c5058f3a5" @@ -353,8 +353,9 @@ } ], "@context" : { "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", "rdfs" : "http://www.w3.org/2000/01/rdf-schema#", - "xsd" : "http://www.w3.org/2001/XMLSchema#", - "knora-api" : "http://api.knora.org/ontology/knora-api/v2#" + "incunabula" : "http://0.0.0.0:3333/ontology/0803/incunabula/v2#", + "xsd" : "http://www.w3.org/2001/XMLSchema#" } } \ No newline at end of file diff --git a/webapi/src/test/resources/test-data/searchR2RV2/RegionsForPage.jsonld b/webapi/src/test/resources/test-data/searchR2RV2/RegionsForPage.jsonld index 29910b3b1f..471432e5a1 100644 --- a/webapi/src/test/resources/test-data/searchR2RV2/RegionsForPage.jsonld +++ b/webapi/src/test/resources/test-data/searchR2RV2/RegionsForPage.jsonld @@ -99,7 +99,7 @@ "knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:UnknownUser,knora-admin:KnownUser", "knora-api:linkValueHasTarget" : { "@id" : "http://rdfh.ch/0803/9d626dc76c03", - "@type" : "http://0.0.0.0:3333/ontology/0803/incunabula/v2#page", + "@type" : "incunabula:page", "knora-api:arkUrl" : { "@type" : "xsd:anyURI", "@value" : "http://0.0.0.0:3336/ark:/72163/1/0803/9d626dc76c039" @@ -239,7 +239,7 @@ "knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:UnknownUser,knora-admin:KnownUser", "knora-api:linkValueHasTarget" : { "@id" : "http://rdfh.ch/0803/9d626dc76c03", - "@type" : "http://0.0.0.0:3333/ontology/0803/incunabula/v2#page", + "@type" : "incunabula:page", "knora-api:arkUrl" : { "@type" : "xsd:anyURI", "@value" : "http://0.0.0.0:3336/ark:/72163/1/0803/9d626dc76c039" @@ -282,8 +282,9 @@ } ], "@context" : { "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", "rdfs" : "http://www.w3.org/2000/01/rdf-schema#", - "xsd" : "http://www.w3.org/2001/XMLSchema#", - "knora-api" : "http://api.knora.org/ontology/knora-api/v2#" + "incunabula" : "http://0.0.0.0:3333/ontology/0803/incunabula/v2#", + "xsd" : "http://www.w3.org/2001/XMLSchema#" } } \ No newline at end of file diff --git a/webapi/src/test/resources/test-data/searchR2RV2/ThingFromQueryWithUnion.jsonld b/webapi/src/test/resources/test-data/searchR2RV2/ThingFromQueryWithUnion.jsonld new file mode 100644 index 0000000000..7751693784 --- /dev/null +++ b/webapi/src/test/resources/test-data/searchR2RV2/ThingFromQueryWithUnion.jsonld @@ -0,0 +1,104 @@ +{ + "@id" : "http://rdfh.ch/0001/H6gBWUuJSuuO-CilHV8kQw", + "@type" : "anything:Thing", + "anything:hasInteger" : { + "@id" : "http://rdfh.ch/0001/H6gBWUuJSuuO-CilHV8kQw/values/dJ1ES8QTQNepFKF5-EAqdg", + "@type" : "knora-api:IntValue", + "knora-api:arkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0001/H6gBWUuJSuuO=CilHV8kQwk/dJ1ES8QTQNepFKF5=EAqdg3" + }, + "knora-api:attachedToUser" : { + "@id" : "http://rdfh.ch/users/BhkfBc3hTeS_IDo-JgXRbQ" + }, + "knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|RV knora-admin:UnknownUser", + "knora-api:intValueAsInt" : 1, + "knora-api:userHasPermission" : "RV", + "knora-api:valueCreationDate" : { + "@type" : "xsd:dateTimeStamp", + "@value" : "2018-05-28T15:52:03.897Z" + }, + "knora-api:valueHasUUID" : "dJ1ES8QTQNepFKF5-EAqdg", + "knora-api:versionArkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0001/H6gBWUuJSuuO=CilHV8kQwk/dJ1ES8QTQNepFKF5=EAqdg3.20180528T155203897Z" + } + }, + "anything:hasRichtext" : { + "@id" : "http://rdfh.ch/0001/H6gBWUuJSuuO-CilHV8kQw/values/rvB4eQ5MTF-Qxq0YgkwaDg", + "@type" : "knora-api:TextValue", + "knora-api:arkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0001/H6gBWUuJSuuO=CilHV8kQwk/rvB4eQ5MTF=Qxq0YgkwaDgM" + }, + "knora-api:attachedToUser" : { + "@id" : "http://rdfh.ch/users/BhkfBc3hTeS_IDo-JgXRbQ" + }, + "knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|RV knora-admin:UnknownUser", + "knora-api:textValueAsXml" : "\n

test with markup

", + "knora-api:textValueHasMapping" : { + "@id" : "http://rdfh.ch/standoff/mappings/StandardMapping" + }, + "knora-api:userHasPermission" : "RV", + "knora-api:valueCreationDate" : { + "@type" : "xsd:dateTimeStamp", + "@value" : "2018-05-28T15:52:03.897Z" + }, + "knora-api:valueHasUUID" : "rvB4eQ5MTF-Qxq0YgkwaDg", + "knora-api:versionArkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0001/H6gBWUuJSuuO=CilHV8kQwk/rvB4eQ5MTF=Qxq0YgkwaDgM.20180528T155203897Z" + } + }, + "anything:hasText" : { + "@id" : "http://rdfh.ch/0001/H6gBWUuJSuuO-CilHV8kQw/values/SZyeLLmOTcCCuS3B0VksHQ", + "@type" : "knora-api:TextValue", + "knora-api:arkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0001/H6gBWUuJSuuO=CilHV8kQwk/SZyeLLmOTcCCuS3B0VksHQO" + }, + "knora-api:attachedToUser" : { + "@id" : "http://rdfh.ch/users/BhkfBc3hTeS_IDo-JgXRbQ" + }, + "knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|RV knora-admin:UnknownUser", + "knora-api:userHasPermission" : "RV", + "knora-api:valueAsString" : "test", + "knora-api:valueCreationDate" : { + "@type" : "xsd:dateTimeStamp", + "@value" : "2018-05-28T15:52:03.897Z" + }, + "knora-api:valueHasUUID" : "SZyeLLmOTcCCuS3B0VksHQ", + "knora-api:versionArkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0001/H6gBWUuJSuuO=CilHV8kQwk/SZyeLLmOTcCCuS3B0VksHQO.20180528T155203897Z" + } + }, + "knora-api:arkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0001/H6gBWUuJSuuO=CilHV8kQwk" + }, + "knora-api:attachedToProject" : { + "@id" : "http://rdfh.ch/projects/0001" + }, + "knora-api:attachedToUser" : { + "@id" : "http://rdfh.ch/users/BhkfBc3hTeS_IDo-JgXRbQ" + }, + "knora-api:creationDate" : { + "@type" : "xsd:dateTimeStamp", + "@value" : "2018-05-28T15:52:03.897Z" + }, + "knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|RV knora-admin:UnknownUser", + "knora-api:userHasPermission" : "RV", + "knora-api:versionArkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0001/H6gBWUuJSuuO=CilHV8kQwk.20180528T155203897Z" + }, + "rdfs:label" : "testding", + "@context" : { + "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", + "rdfs" : "http://www.w3.org/2000/01/rdf-schema#", + "xsd" : "http://www.w3.org/2001/XMLSchema#", + "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#" + } +} \ No newline at end of file diff --git a/webapi/src/test/resources/test-data/searchR2RV2/regionsOfZeitgloecklein.jsonld b/webapi/src/test/resources/test-data/searchR2RV2/regionsOfZeitgloecklein.jsonld index 1a63c0d783..d952fe26ef 100644 --- a/webapi/src/test/resources/test-data/searchR2RV2/regionsOfZeitgloecklein.jsonld +++ b/webapi/src/test/resources/test-data/searchR2RV2/regionsOfZeitgloecklein.jsonld @@ -30,8 +30,8 @@ "knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:UnknownUser,knora-admin:KnownUser", "knora-api:linkValueHasTarget" : { "@id" : "http://rdfh.ch/0803/3f89a693a501", - "@type" : "http://0.0.0.0:3333/ontology/0803/incunabula/v2#page", - "http://0.0.0.0:3333/ontology/0803/incunabula/v2#partOfValue" : { + "@type" : "incunabula:page", + "incunabula:partOfValue" : { "@id" : "http://rdfh.ch/0803/3f89a693a501/values/133ea4ce-b9a5-4c66-a3ab-38be07ccd201", "@type" : "knora-api:LinkValue", "knora-api:arkUrl" : { @@ -44,8 +44,8 @@ "knora-api:hasPermissions" : "CR knora-admin:Creator|V knora-admin:UnknownUser,knora-admin:KnownUser,knora-admin:ProjectMember", "knora-api:linkValueHasTarget" : { "@id" : "http://rdfh.ch/0803/ff17e5ef9601", - "@type" : "http://0.0.0.0:3333/ontology/0803/incunabula/v2#book", - "http://0.0.0.0:3333/ontology/0803/incunabula/v2#title" : { + "@type" : "incunabula:book", + "incunabula:title" : { "@id" : "http://rdfh.ch/0803/ff17e5ef9601/values/d9a522845006", "@type" : "knora-api:TextValue", "knora-api:arkUrl" : { @@ -171,8 +171,8 @@ "knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:UnknownUser,knora-admin:KnownUser", "knora-api:linkValueHasTarget" : { "@id" : "http://rdfh.ch/0803/6240ce899801", - "@type" : "http://0.0.0.0:3333/ontology/0803/incunabula/v2#page", - "http://0.0.0.0:3333/ontology/0803/incunabula/v2#partOfValue" : { + "@type" : "incunabula:page", + "incunabula:partOfValue" : { "@id" : "http://rdfh.ch/0803/6240ce899801/values/e29da7bf-bfa5-4842-a4fc-e455c72dbb0a", "@type" : "knora-api:LinkValue", "knora-api:arkUrl" : { @@ -185,8 +185,8 @@ "knora-api:hasPermissions" : "CR knora-admin:Creator|V knora-admin:UnknownUser,knora-admin:KnownUser,knora-admin:ProjectMember", "knora-api:linkValueHasTarget" : { "@id" : "http://rdfh.ch/0803/ff17e5ef9601", - "@type" : "http://0.0.0.0:3333/ontology/0803/incunabula/v2#book", - "http://0.0.0.0:3333/ontology/0803/incunabula/v2#title" : { + "@type" : "incunabula:book", + "incunabula:title" : { "@id" : "http://rdfh.ch/0803/ff17e5ef9601/values/d9a522845006", "@type" : "knora-api:TextValue", "knora-api:arkUrl" : { @@ -312,8 +312,8 @@ "knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:UnknownUser,knora-admin:KnownUser", "knora-api:linkValueHasTarget" : { "@id" : "http://rdfh.ch/0803/c568b7239a01", - "@type" : "http://0.0.0.0:3333/ontology/0803/incunabula/v2#page", - "http://0.0.0.0:3333/ontology/0803/incunabula/v2#partOfValue" : { + "@type" : "incunabula:page", + "incunabula:partOfValue" : { "@id" : "http://rdfh.ch/0803/c568b7239a01/values/46951e64-8f06-47fb-a07a-c8b19c146447", "@type" : "knora-api:LinkValue", "knora-api:arkUrl" : { @@ -326,8 +326,8 @@ "knora-api:hasPermissions" : "CR knora-admin:Creator|V knora-admin:UnknownUser,knora-admin:KnownUser,knora-admin:ProjectMember", "knora-api:linkValueHasTarget" : { "@id" : "http://rdfh.ch/0803/ff17e5ef9601", - "@type" : "http://0.0.0.0:3333/ontology/0803/incunabula/v2#book", - "http://0.0.0.0:3333/ontology/0803/incunabula/v2#title" : { + "@type" : "incunabula:book", + "incunabula:title" : { "@id" : "http://rdfh.ch/0803/ff17e5ef9601/values/d9a522845006", "@type" : "knora-api:TextValue", "knora-api:arkUrl" : { @@ -453,8 +453,8 @@ "knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:UnknownUser,knora-admin:KnownUser", "knora-api:linkValueHasTarget" : { "@id" : "http://rdfh.ch/0803/6240ce899801", - "@type" : "http://0.0.0.0:3333/ontology/0803/incunabula/v2#page", - "http://0.0.0.0:3333/ontology/0803/incunabula/v2#partOfValue" : { + "@type" : "incunabula:page", + "incunabula:partOfValue" : { "@id" : "http://rdfh.ch/0803/6240ce899801/values/e29da7bf-bfa5-4842-a4fc-e455c72dbb0a", "@type" : "knora-api:LinkValue", "knora-api:arkUrl" : { @@ -467,8 +467,8 @@ "knora-api:hasPermissions" : "CR knora-admin:Creator|V knora-admin:UnknownUser,knora-admin:KnownUser,knora-admin:ProjectMember", "knora-api:linkValueHasTarget" : { "@id" : "http://rdfh.ch/0803/ff17e5ef9601", - "@type" : "http://0.0.0.0:3333/ontology/0803/incunabula/v2#book", - "http://0.0.0.0:3333/ontology/0803/incunabula/v2#title" : { + "@type" : "incunabula:book", + "incunabula:title" : { "@id" : "http://rdfh.ch/0803/ff17e5ef9601/values/d9a522845006", "@type" : "knora-api:TextValue", "knora-api:arkUrl" : { @@ -566,8 +566,9 @@ } ], "@context" : { "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", "rdfs" : "http://www.w3.org/2000/01/rdf-schema#", - "xsd" : "http://www.w3.org/2001/XMLSchema#", - "knora-api" : "http://api.knora.org/ontology/knora-api/v2#" + "incunabula" : "http://0.0.0.0:3333/ontology/0803/incunabula/v2#", + "xsd" : "http://www.w3.org/2001/XMLSchema#" } } \ No newline at end of file diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala index 9fc133fe5b..6576886b7d 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala @@ -81,7 +81,7 @@ class SearchRouteV2R2RSpec extends R2RSpec { private val timeTagResourceIri = new MutableTestIri // If true, writes all API responses to test data files. If false, compares the API responses to the existing test data files. - private val writeTestDataFiles = false + private val writeTestDataFiles = true override lazy val rdfDataObjects: List[RdfDataObject] = List( RdfDataObject(path = "_test_data/demo_data/images-demo-data.ttl", name = "http://www.knora.org/data/00FF/images"), @@ -3169,8 +3169,7 @@ class SearchRouteV2R2RSpec extends R2RSpec { | |CONSTRUCT { | - | ?book knora-api:isMainResource true ; - | incunabula:title ?title . + | ?book knora-api:isMainResource true . | | ?page incunabula:partOf ?book ; | incunabula:seqnum ?seqnum . @@ -5460,8 +5459,7 @@ class SearchRouteV2R2RSpec extends R2RSpec { | |CONSTRUCT { | - | ?book knora-api:isMainResource true ; - | incunabula:title ?title . + | ?book knora-api:isMainResource true . | | ?page incunabula:partOf ?book ; | incunabula:seqnum ?seqnum . @@ -8056,6 +8054,55 @@ class SearchRouteV2R2RSpec extends R2RSpec { } } + "not return duplicate results when there are unbound variables in one or more UNION branches" in { + val gravsearchQuery = + s"""PREFIX knora-api: + |PREFIX anything: + |CONSTRUCT { + | ?thing knora-api:isMainResource true . + | ?thing anything:hasInteger ?int . + | ?thing anything:hasRichtext ?richtext . + | ?thing anything:hasText ?text . + |} WHERE { + | ?thing a knora-api:Resource . + | ?thing a anything:Thing . + | { + | ?thing anything:hasRichtext ?richtext . + | ?richtext knora-api:valueAsString ?richtextLiteral + | FILTER knora-api:match(?richtextLiteral, "test") + | + | ?thing anything:hasInteger ?int . + | ?int knora-api:intValueAsInt 1 + | } + | UNION + | { + | ?thing anything:hasText ?text . + | ?text knora-api:valueAsString ?textLiteral + | FILTER knora-api:match(?textLiteral, "test") + | + | ?thing anything:hasInteger ?int . + | ?int knora-api:intValueAsInt 1 + | } + |} + |order by (?int)""".stripMargin + + val expectedCount = 1 + + Post("/v2/searchextended/count", HttpEntity(SparqlQueryConstants.`application/sparql-query`, gravsearchQuery)) ~> searchPath ~> check { + val searchResponseStr = responseAs[String] + assert(status == StatusCodes.OK, searchResponseStr) + checkCountResponse(searchResponseStr, expectedCount) + } + + Post("/v2/searchextended", HttpEntity(SparqlQueryConstants.`application/sparql-query`, gravsearchQuery)) ~> searchPath ~> check { + val searchResponseStr = responseAs[String] + assert(status == StatusCodes.OK, searchResponseStr) + checkSearchResponseNumberOfResults(searchResponseStr, expectedCount) + val expectedAnswerJSONLD = readOrWriteTextFile(searchResponseStr, new File("src/test/resources/test-data/searchR2RV2/ThingFromQueryWithUnion.jsonld"), writeTestDataFiles) + compareJSONLDForResourcesResponse(expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = searchResponseStr) + } + } + "search for a resource containing a time value tag" in { // Create a resource containing a time value. diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala index d347d564a1..78c9bb71e5 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala @@ -881,7 +881,7 @@ class ResourcesResponderV2Spec extends CoreSpec() with ImplicitSender { apiRequestID = UUID.randomUUID ) - expectMsgType[ReadResourcesSequenceV2] + expectMsgType[ReadResourcesSequenceV2](timeout) // Get the resource from the triplestore and check it. @@ -1038,7 +1038,7 @@ class ResourcesResponderV2Spec extends CoreSpec() with ImplicitSender { apiRequestID = UUID.randomUUID ) - expectMsgType[ReadResourcesSequenceV2] + expectMsgType[ReadResourcesSequenceV2](timeout) // Get the resource from the triplestore and check it. @@ -1090,7 +1090,7 @@ class ResourcesResponderV2Spec extends CoreSpec() with ImplicitSender { apiRequestID = UUID.randomUUID ) - expectMsgType[ReadResourcesSequenceV2] + expectMsgType[ReadResourcesSequenceV2](timeout) // Get the resource from the triplestore and check it. @@ -1620,7 +1620,7 @@ class ResourcesResponderV2Spec extends CoreSpec() with ImplicitSender { responderManager ! updateRequest - expectMsgType[SuccessResponseV2] + expectMsgType[SuccessResponseV2](timeout) // Get the resource from the triplestore and check it. @@ -1678,7 +1678,7 @@ class ResourcesResponderV2Spec extends CoreSpec() with ImplicitSender { responderManager ! updateRequest - expectMsgType[SuccessResponseV2] + expectMsgType[SuccessResponseV2](timeout) // Get the resource from the triplestore and check it. @@ -1720,7 +1720,7 @@ class ResourcesResponderV2Spec extends CoreSpec() with ImplicitSender { responderManager ! updateRequest - expectMsgType[SuccessResponseV2] + expectMsgType[SuccessResponseV2](timeout) // Get the resource from the triplestore and check it. @@ -1742,7 +1742,7 @@ class ResourcesResponderV2Spec extends CoreSpec() with ImplicitSender { responderManager ! deleteRequest - expectMsgType[SuccessResponseV2] + expectMsgType[SuccessResponseV2](timeout) // We should now be unable to request the resource. diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToCountPrequeryGeneratorSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToCountPrequeryTransformerSpec.scala similarity index 98% rename from webapi/src/test/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToCountPrequeryGeneratorSpec.scala rename to webapi/src/test/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToCountPrequeryTransformerSpec.scala index 2a8a899c47..5e60bde56c 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToCountPrequeryGeneratorSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToCountPrequeryTransformerSpec.scala @@ -38,7 +38,8 @@ private object CountQueryHandler { // Create a Select prequery - val nonTriplestoreSpecificConstructToSelectTransformer: NonTriplestoreSpecificGravsearchToCountPrequeryGenerator = new NonTriplestoreSpecificGravsearchToCountPrequeryGenerator( + val nonTriplestoreSpecificConstructToSelectTransformer: NonTriplestoreSpecificGravsearchToCountPrequeryTransformer = new NonTriplestoreSpecificGravsearchToCountPrequeryTransformer( + constructClause = constructQuery.constructClause, typeInspectionResult = typeInspectionResult, querySchema = constructQuery.querySchema.getOrElse(throw AssertionException(s"WhereClause has no querySchema")) ) @@ -56,7 +57,7 @@ private object CountQueryHandler { } -class NonTriplestoreSpecificGravsearchToCountPrequeryGeneratorSpec extends CoreSpec() { +class NonTriplestoreSpecificGravsearchToCountPrequeryTransformerSpec extends CoreSpec() { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryGeneratorSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala similarity index 98% rename from webapi/src/test/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryGeneratorSpec.scala rename to webapi/src/test/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala index 70345cbfbb..6dc64191aa 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryGeneratorSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala @@ -39,7 +39,8 @@ private object QueryHandler { // Create a Select prequery - val nonTriplestoreSpecificConstructToSelectTransformer: NonTriplestoreSpecificGravsearchToPrequeryGenerator = new NonTriplestoreSpecificGravsearchToPrequeryGenerator( + val nonTriplestoreSpecificConstructToSelectTransformer: NonTriplestoreSpecificGravsearchToPrequeryTransformer = new NonTriplestoreSpecificGravsearchToPrequeryTransformer( + constructClause = constructQuery.constructClause, typeInspectionResult = typeInspectionResult, querySchema = constructQuery.querySchema.getOrElse(throw AssertionException(s"WhereClause has no querySchema")), settings = settings @@ -55,108 +56,10 @@ private object QueryHandler { } -class NonTriplestoreSpecificGravsearchToPrequeryGeneratorSpec extends CoreSpec() { +class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec() { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - "The NonTriplestoreSpecificGravsearchToPrequeryGenerator object" should { - - "transform an input query with a date as a non optional sort criterion" in { - - val transformedQuery = QueryHandler.transformQuery(inputQueryWithDateNonOptionalSortCriterion, responderData, settings) - - assert(transformedQuery === transformedQueryWithDateNonOptionalSortCriterion) - - } - - "transform an input query with a date as a non optional sort criterion (submitted in complex schema)" in { - - val transformedQuery = QueryHandler.transformQuery(inputQueryWithDateNonOptionalSortCriterionComplex, responderData, settings) - - assert(transformedQuery === transformedQueryWithDateNonOptionalSortCriterion) - - } - - "transform an input query with a date as non optional sort criterion and a filter" in { - - val transformedQuery = QueryHandler.transformQuery(inputQueryWithDateNonOptionalSortCriterionAndFilter, responderData, settings) - - assert(transformedQuery === transformedQueryWithDateNonOptionalSortCriterionAndFilter) - - } - - "transform an input query with a date as non optional sort criterion and a filter (submitted in complex schema)" in { - - val transformedQuery = QueryHandler.transformQuery(inputQueryWithDateNonOptionalSortCriterionAndFilterComplex, responderData, settings) - - assert(transformedQuery === transformedQueryWithDateNonOptionalSortCriterionAndFilter) - - } - - "transform an input query with a date as an optional sort criterion" in { - - val transformedQuery = QueryHandler.transformQuery(inputQueryWithDateOptionalSortCriterion, responderData, settings) - - assert(transformedQuery === transformedQueryWithDateOptionalSortCriterion) - - } - - "transform an input query with a date as an optional sort criterion (submitted in complex schema)" in { - - val transformedQuery = QueryHandler.transformQuery(inputQueryWithDateOptionalSortCriterionComplex, responderData, settings) - - assert(transformedQuery === transformedQueryWithDateOptionalSortCriterion) - - } - - "transform an input query with a date as an optional sort criterion and a filter" in { - - val transformedQuery = QueryHandler.transformQuery(inputQueryWithDateOptionalSortCriterionAndFilter, responderData, settings) - - assert(transformedQuery === transformedQueryWithDateOptionalSortCriterionAndFilter) - - } - - "transform an input query with a date as an optional sort criterion and a filter (submitted in complex schema)" in { - - val transformedQuery = QueryHandler.transformQuery(inputQueryWithDateOptionalSortCriterionAndFilterComplex, responderData, settings) - - assert(transformedQuery === transformedQueryWithDateOptionalSortCriterionAndFilter) - - } - - - "transform an input query with a decimal as an optional sort criterion" in { - - val transformedQuery = QueryHandler.transformQuery(inputQueryWithDecimalOptionalSortCriterion, responderData, settings) - - assert(transformedQuery === transformedQueryWithDecimalOptionalSortCriterion) - } - - "transform an input query with a decimal as an optional sort criterion (submitted in complex schema)" in { - - val transformedQuery = QueryHandler.transformQuery(inputQueryWithDecimalOptionalSortCriterionComplex, responderData, settings) - - assert(transformedQuery === transformedQueryWithDecimalOptionalSortCriterion) - } - - "transform an input query with a decimal as an optional sort criterion and a filter" in { - - val transformedQuery = QueryHandler.transformQuery(inputQueryWithDecimalOptionalSortCriterionAndFilter, responderData, settings) - - assert(transformedQuery === transformedQueryWithDecimalOptionalSortCriterionAndFilter) - } - - "transform an input query with a decimal as an optional sort criterion and a filter (submitted in complex schema)" in { - - val transformedQuery = QueryHandler.transformQuery(inputQueryWithDecimalOptionalSortCriterionAndFilterComplex, responderData, settings) - - // TODO: user provided statements and statement generated for sorting should be unified (https://github.com/dhlab-basel/Knora/issues/1195) - assert(transformedQuery === transformedQueryWithDecimalOptionalSortCriterionAndFilterComplex) - } - - } - val inputQueryWithDateNonOptionalSortCriterion: String = """ |PREFIX knora-api: @@ -197,7 +100,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryGeneratorSpec extends CoreSpec() |ORDER BY DESC(?date) """.stripMargin - val transformedQueryWithDateNonOptionalSortCriterion = + val transformedQueryWithDateNonOptionalSortCriterion: SelectQuery = SelectQuery( variables = Vector( QueryVariable(variableName = "thing"), @@ -681,7 +584,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryGeneratorSpec extends CoreSpec() |ORDER BY DESC(?date) """.stripMargin - val transformedQueryWithDateOptionalSortCriterionAndFilter = + val transformedQueryWithDateOptionalSortCriterionAndFilter: SelectQuery = SelectQuery( variables = Vector( QueryVariable(variableName = "thing"), @@ -801,7 +704,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryGeneratorSpec extends CoreSpec() useDistinct = true ) - val inputQueryWithDecimalOptionalSortCriterion = + val inputQueryWithDecimalOptionalSortCriterion: String = """ |PREFIX anything: |PREFIX knora-api: @@ -824,7 +727,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryGeneratorSpec extends CoreSpec() |} ORDER BY ASC(?decimal) """.stripMargin - val inputQueryWithDecimalOptionalSortCriterionComplex = + val inputQueryWithDecimalOptionalSortCriterionComplex: String = """ |PREFIX anything: |PREFIX knora-api: @@ -844,7 +747,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryGeneratorSpec extends CoreSpec() |} ORDER BY ASC(?decimal) """.stripMargin - val transformedQueryWithDecimalOptionalSortCriterion = + val transformedQueryWithDecimalOptionalSortCriterion: SelectQuery = SelectQuery( variables = Vector( QueryVariable(variableName = "thing"), @@ -956,7 +859,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryGeneratorSpec extends CoreSpec() useDistinct = true ) - val inputQueryWithDecimalOptionalSortCriterionAndFilter = + val inputQueryWithDecimalOptionalSortCriterionAndFilter: String = """ |PREFIX anything: |PREFIX knora-api: @@ -981,7 +884,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryGeneratorSpec extends CoreSpec() |} ORDER BY ASC(?decimal) """.stripMargin - val inputQueryWithDecimalOptionalSortCriterionAndFilterComplex = + val inputQueryWithDecimalOptionalSortCriterionAndFilterComplex: String = """ |PREFIX anything: |PREFIX knora-api: @@ -1005,7 +908,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryGeneratorSpec extends CoreSpec() |} ORDER BY ASC(?decimal) """.stripMargin - val transformedQueryWithDecimalOptionalSortCriterionAndFilter = + val transformedQueryWithDecimalOptionalSortCriterionAndFilter: SelectQuery = SelectQuery( variables = Vector( QueryVariable(variableName = "thing"), @@ -1125,7 +1028,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryGeneratorSpec extends CoreSpec() useDistinct = true ) - val transformedQueryWithDecimalOptionalSortCriterionAndFilterComplex = + val transformedQueryWithDecimalOptionalSortCriterionAndFilterComplex: SelectQuery = SelectQuery( variables = Vector( QueryVariable(variableName = "thing"), @@ -1254,5 +1157,101 @@ class NonTriplestoreSpecificGravsearchToPrequeryGeneratorSpec extends CoreSpec() useDistinct = true ) + "The NonTriplestoreSpecificGravsearchToPrequeryGenerator object" should { + + "transform an input query with a date as a non optional sort criterion" in { + val transformedQuery = QueryHandler.transformQuery(inputQueryWithDateNonOptionalSortCriterion, responderData, settings) + + assert(transformedQuery === transformedQueryWithDateNonOptionalSortCriterion) + + } + + "transform an input query with a date as a non optional sort criterion (submitted in complex schema)" in { + + val transformedQuery = QueryHandler.transformQuery(inputQueryWithDateNonOptionalSortCriterionComplex, responderData, settings) + + assert(transformedQuery === transformedQueryWithDateNonOptionalSortCriterion) + + } + + "transform an input query with a date as non optional sort criterion and a filter" in { + + val transformedQuery = QueryHandler.transformQuery(inputQueryWithDateNonOptionalSortCriterionAndFilter, responderData, settings) + + assert(transformedQuery === transformedQueryWithDateNonOptionalSortCriterionAndFilter) + + } + + "transform an input query with a date as non optional sort criterion and a filter (submitted in complex schema)" in { + + val transformedQuery = QueryHandler.transformQuery(inputQueryWithDateNonOptionalSortCriterionAndFilterComplex, responderData, settings) + + assert(transformedQuery === transformedQueryWithDateNonOptionalSortCriterionAndFilter) + + } + + "transform an input query with a date as an optional sort criterion" in { + + val transformedQuery = QueryHandler.transformQuery(inputQueryWithDateOptionalSortCriterion, responderData, settings) + + assert(transformedQuery === transformedQueryWithDateOptionalSortCriterion) + + } + + "transform an input query with a date as an optional sort criterion (submitted in complex schema)" in { + + val transformedQuery = QueryHandler.transformQuery(inputQueryWithDateOptionalSortCriterionComplex, responderData, settings) + + assert(transformedQuery === transformedQueryWithDateOptionalSortCriterion) + + } + + "transform an input query with a date as an optional sort criterion and a filter" in { + + val transformedQuery = QueryHandler.transformQuery(inputQueryWithDateOptionalSortCriterionAndFilter, responderData, settings) + + assert(transformedQuery === transformedQueryWithDateOptionalSortCriterionAndFilter) + + } + + "transform an input query with a date as an optional sort criterion and a filter (submitted in complex schema)" in { + + val transformedQuery = QueryHandler.transformQuery(inputQueryWithDateOptionalSortCriterionAndFilterComplex, responderData, settings) + + assert(transformedQuery === transformedQueryWithDateOptionalSortCriterionAndFilter) + + } + + + "transform an input query with a decimal as an optional sort criterion" in { + + val transformedQuery = QueryHandler.transformQuery(inputQueryWithDecimalOptionalSortCriterion, responderData, settings) + + assert(transformedQuery === transformedQueryWithDecimalOptionalSortCriterion) + } + + "transform an input query with a decimal as an optional sort criterion (submitted in complex schema)" in { + + val transformedQuery = QueryHandler.transformQuery(inputQueryWithDecimalOptionalSortCriterionComplex, responderData, settings) + + assert(transformedQuery === transformedQueryWithDecimalOptionalSortCriterion) + } + + "transform an input query with a decimal as an optional sort criterion and a filter" in { + + val transformedQuery = QueryHandler.transformQuery(inputQueryWithDecimalOptionalSortCriterionAndFilter, responderData, settings) + + assert(transformedQuery === transformedQueryWithDecimalOptionalSortCriterionAndFilter) + } + + "transform an input query with a decimal as an optional sort criterion and a filter (submitted in complex schema)" in { + + val transformedQuery = QueryHandler.transformQuery(inputQueryWithDecimalOptionalSortCriterionAndFilterComplex, responderData, settings) + + // TODO: user provided statements and statement generated for sorting should be unified (https://github.com/dhlab-basel/Knora/issues/1195) + assert(transformedQuery === transformedQueryWithDecimalOptionalSortCriterionAndFilterComplex) + } + + } } \ No newline at end of file diff --git a/webapi/src/test/scala/org/knora/webapi/util/ConstructResponseUtilV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/util/ConstructResponseUtilV2Spec.scala index a618b7bb81..f6ff13ef93 100644 --- a/webapi/src/test/scala/org/knora/webapi/util/ConstructResponseUtilV2Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/util/ConstructResponseUtilV2Spec.scala @@ -55,6 +55,7 @@ class ConstructResponseUtilV2Spec extends CoreSpec() with ImplicitSender { val apiResponseFuture: Future[ReadResourcesSequenceV2] = ConstructResponseUtilV2.createApiResponse( mainResourcesAndValueRdfData = mainResourcesAndValueRdfData, orderByResourceIri = Seq(resourceIri), + pageSizeBeforeFiltering = 1, mappings = Map.empty, queryStandoff = false, versionDate = None,