diff --git a/docs/src/paradox/03-apis/api-v2/query-language.md b/docs/src/paradox/03-apis/api-v2/query-language.md index 6fbaf974b2..c2db4caac3 100644 --- a/docs/src/paradox/03-apis/api-v2/query-language.md +++ b/docs/src/paradox/03-apis/api-v2/query-language.md @@ -616,6 +616,39 @@ CONSTRUCT { } ``` +### Filtering on `rdfs:label` + +The `rdfs:label` of a resource is not a Knora value, but you can still search for it. +This can be done in the same ways in the simple or complex schema: + +Using a string literal object: + +``` +?book rdfs:label "Zeitglöcklein des Lebens und Leidens Christi" . +``` + +Using a variable and a FILTER: + +``` +?book rdfs:label ?label . +FILTER(?label = "Zeitglöcklein des Lebens und Leidens Christi") +``` + +Using the `regex` function: + +``` +?book rdfs:label ?bookLabel . +FILTER regex(?bookLabel, "Zeit", "i") +``` + +To match words in an `rdfs:label` using the full-text search index, use the +`knora-api:matchLabel` function, which works like `knora-api:matchText`, +except that the first argument is a variable representing a resource: + +``` +FILTER knora-api:matchLabel(?book, "Zeitglöcklein") +``` + ### CONSTRUCT Clause In the `CONSTRUCT` clause of a Gravsearch query, the variable representing the diff --git a/webapi/src/main/scala/org/knora/webapi/OntologyConstants.scala b/webapi/src/main/scala/org/knora/webapi/OntologyConstants.scala index 98c5172131..405685bdce 100644 --- a/webapi/src/main/scala/org/knora/webapi/OntologyConstants.scala +++ b/webapi/src/main/scala/org/knora/webapi/OntologyConstants.scala @@ -133,6 +133,13 @@ object OntologyConstants { val InternalOntologyStart = "http://www.knora.org/ontology" } + /** + * The object types of resource metadata properties. + */ + val ResourceMetadataPropertyAxioms: Map[IRI, IRI] = Map( + OntologyConstants.Rdfs.Label -> OntologyConstants.Xsd.String + ) + /** * Ontology labels that are reserved for built-in ontologies. */ @@ -922,6 +929,7 @@ object OntologyConstants { val MatchTextFunction: IRI = KnoraApiV2PrefixExpansion + "matchText" val MatchInStandoffFunction: IRI = KnoraApiV2PrefixExpansion + "matchInStandoff" val MatchTextInStandoffFunction: IRI = KnoraApiV2PrefixExpansion + "matchTextInStandoff" + val MatchLabelFunction: IRI = KnoraApiV2PrefixExpansion + "matchLabel" val StandoffLinkFunction: IRI = KnoraApiV2PrefixExpansion + "standoffLink" } @@ -961,6 +969,7 @@ object OntologyConstants { val MatchesTextIndex: IRI = KnoraApiV2PrefixExpansion + "matchesTextIndex" // virtual property to be replaced by a triplestore-specific one val MatchFunction: IRI = KnoraApiV2PrefixExpansion + "match" val MatchTextFunction: IRI = KnoraApiV2PrefixExpansion + "matchText" + val MatchLabelFunction: IRI = KnoraApiV2PrefixExpansion + "matchLabel" val ResourceProperty: IRI = KnoraApiV2PrefixExpansion + "resourceProperty" @@ -1135,5 +1144,4 @@ object OntologyConstants { val KnoraExplicitNamedGraph: IRI = "http://www.knora.org/explicit" val GraphDBExplicitNamedGraph: IRI = "http://www.ontotext.com/explicit" } - } diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/GravsearchQueryChecker.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/GravsearchQueryChecker.scala index de75a70b19..8bad95577f 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/GravsearchQueryChecker.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/GravsearchQueryChecker.scala @@ -135,7 +135,6 @@ object GravsearchQueryChecker { // A set of predicates that aren't allowed in Gravsearch. val forbiddenPredicates: Set[IRI] = Set( - OntologyConstants.Rdfs.Label, OntologyConstants.KnoraApiV2Complex.AttachedToUser, OntologyConstants.KnoraApiV2Complex.HasPermissions, OntologyConstants.KnoraApiV2Complex.CreationDate, 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 034e354306..ec17676691 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 @@ -68,6 +68,9 @@ abstract class AbstractPrequeryGenerator(constructClause: ConstructClause, // they represent the value of a literal pointed to by a value object private val valueVariablesAutomaticallyGenerated = mutable.Map.empty[QueryVariable, Set[GeneratedQueryVariable]] + // variables the represent resource metadata + private val resourceMetadataVariables = mutable.Set.empty[QueryVariable] + /** * Saves a generated variable representing a value literal, if it hasn't been saved already. * @@ -202,7 +205,7 @@ abstract class AbstractPrequeryGenerator(constructClause: ConstructClause, case linkingPropQueryVar: QueryVariable => // Generate a variable name representing the link value property // in case FILTER patterns are given restricting the linking property's possible IRIs, the same variable will recreated when processing FILTER patterns - createlinkValuePropertyVariableFromLinkingPropertyVariable(linkingPropQueryVar) + createLinkValuePropertyVariableFromLinkingPropertyVariable(linkingPropQueryVar) case propIri: IriRef => // convert the given linking property IRI to the corresponding link value property IRI @@ -305,71 +308,90 @@ abstract class AbstractPrequeryGenerator(constructClause: ConstructClause, statementPatternToInternalSchema(statementPattern, typeInspectionResult) +: linkValueStatements } else { - // The subject is a resource, but the object isn't, so this isn't a link property. Make sure that the object of the property is a variable. - val objectVar: QueryVariable = statementPattern.obj match { - case queryVar: QueryVariable => - checkSubjectInOrderBy(queryVar) - queryVar + // The subject is a resource, but the object isn't, so this isn't a link property. + // Is the property a resource metadata property? + statementPattern.pred match { + case iriRef: IriRef if OntologyConstants.ResourceMetadataPropertyAxioms.contains(iriRef.iri.toString) => + // Yes. Store the variable if provided. + val maybeObjectVar: Option[QueryVariable] = statementPattern.obj match { + case queryVar: QueryVariable => + checkSubjectInOrderBy(queryVar) + Some(queryVar) + + case _ => None + } - case other => throw GravsearchException(s"Object of a value property statement must be a QueryVariable, but ${other.toSparql} given.") - } + resourceMetadataVariables ++= maybeObjectVar - // Does the variable refer to a Knora value object? We assume it does if the query just uses the - // simple schema. If the query uses the complex schema, check whether the property's object type is - // a Knora API v2 value class. + // Just convert the statement pattern to the internal schema + Seq(statementPatternToInternalSchema(statementPattern, typeInspectionResult)) - val objectVarIsValueObject = querySchema == ApiV2Simple || OntologyConstants.KnoraApiV2Complex.ValueClasses.contains(propertyTypeInfo.objectTypeIri.toString) + case _ => + // The property is not a resource metadata property. Make sure the object is a variable. + val objectVar: QueryVariable = statementPattern.obj match { + case queryVar: QueryVariable => + checkSubjectInOrderBy(queryVar) + queryVar - if (objectVarIsValueObject) { - // The variable refers to a value object. + case other => throw GravsearchException(s"Object of a value property statement must be a QueryVariable, but ${other.toSparql} given.") + } - // 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)) + // Does the variable refer to a Knora value object? We assume it does if the query just uses the + // simple schema. If the query uses the complex schema, check whether the property's object type is a + // Knora API v2 value class. - // check if the object var is used as a sort criterion - val objectVarAsSortCriterionMaybe = inputOrderBy.find(criterion => criterion.queryVariable == objectVar) + val objectVarIsValueObject = querySchema == ApiV2Simple || + OntologyConstants.KnoraApiV2Complex.ValueClasses.contains(propertyTypeInfo.objectTypeIri.toString) - val orderByStatement: Option[QueryPattern] = if (objectVarAsSortCriterionMaybe.nonEmpty) { - // it is used as a sort criterion, create an additional statement to get the literal value + if (objectVarIsValueObject) { + // The variable refers to a value object. - val criterion = objectVarAsSortCriterionMaybe.get + // 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)) - val propertyIri: SmartIri = typeInspectionResult.getTypeOfEntity(criterion.queryVariable) match { - case Some(nonPropertyTypeInfo: NonPropertyTypeInfo) => - valueTypesToValuePredsForOrderBy.getOrElse(nonPropertyTypeInfo.typeIri.toString, throw GravsearchException(s"${criterion.queryVariable.toSparql} cannot be used in ORDER BY")).toSmartIri + // check if the object var is used as a sort criterion + val objectVarAsSortCriterionMaybe = inputOrderBy.find(criterion => criterion.queryVariable == objectVar) - case Some(_) => throw GravsearchException(s"Variable ${criterion.queryVariable.toSparql} represents a property, and therefore cannot be used in ORDER BY") + val orderByStatement: Option[QueryPattern] = if (objectVarAsSortCriterionMaybe.nonEmpty) { + // it is used as a sort criterion, create an additional statement to get the literal value - case None => throw GravsearchException(s"No type information found for ${criterion.queryVariable.toSparql}") - } + val criterion = objectVarAsSortCriterionMaybe.get - // Generate the variable name. - val variableForLiteral: QueryVariable = SparqlTransformer.createUniqueVariableNameFromEntityAndProperty(criterion.queryVariable, propertyIri.toString) + val propertyIri: SmartIri = typeInspectionResult.getTypeOfEntity(criterion.queryVariable) match { + case Some(nonPropertyTypeInfo: NonPropertyTypeInfo) => + valueTypesToValuePredsForOrderBy.getOrElse(nonPropertyTypeInfo.typeIri.toString, throw GravsearchException(s"${criterion.queryVariable.toSparql} cannot be used in ORDER BY")).toSmartIri - // put the generated variable into a collection so it can be reused in `NonTriplestoreSpecificGravsearchToPrequeryGenerator.getOrderBy` - // set to true when the variable already exists - val variableForLiteralExists = !addGeneratedVariableForValueLiteral(criterion.queryVariable, variableForLiteral) + case Some(_) => throw GravsearchException(s"Variable ${criterion.queryVariable.toSparql} represents a property, and therefore cannot be used in ORDER BY") - if (!variableForLiteralExists) { - // Generate a statement to get the literal value - val statementPatternForSortCriterion = StatementPattern.makeExplicit(subj = criterion.queryVariable, pred = IriRef(propertyIri), obj = variableForLiteral) - Some(statementPatternForSortCriterion) - } else { - // statement has already been created - None - } - } else { - // it is not a sort criterion - None - } + case None => throw GravsearchException(s"No type information found for ${criterion.queryVariable.toSparql}") + } - Seq(statementPatternToInternalSchema(statementPattern, typeInspectionResult), valueObjectIsNotDeleted) ++ orderByStatement - } else { - // The variable doesn't refer to a value object. Just convert the statement pattern to the internal schema. - Seq(statementPatternToInternalSchema(statementPattern, typeInspectionResult)) - } + // Generate the variable name. + val variableForLiteral: QueryVariable = SparqlTransformer.createUniqueVariableNameFromEntityAndProperty(criterion.queryVariable, propertyIri.toString) + // put the generated variable into a collection so it can be reused in `NonTriplestoreSpecificGravsearchToPrequeryGenerator.getOrderBy` + // set to true when the variable already exists + val variableForLiteralExists = !addGeneratedVariableForValueLiteral(criterion.queryVariable, variableForLiteral) + if (!variableForLiteralExists) { + // Generate a statement to get the literal value + val statementPatternForSortCriterion = StatementPattern.makeExplicit(subj = criterion.queryVariable, pred = IriRef(propertyIri), obj = variableForLiteral) + Some(statementPatternForSortCriterion) + } else { + // statement has already been created + None + } + } else { + // it is not a sort criterion + None + } + + Seq(statementPatternToInternalSchema(statementPattern, typeInspectionResult), valueObjectIsNotDeleted) ++ orderByStatement + } else { + // The variable doesn't refer to a value object. Just convert the statement pattern to the internal schema. + Seq(statementPatternToInternalSchema(statementPattern, typeInspectionResult)) + } + } } } else { // The subject isn't a resource, so it must be a value object or standoff node. Is the query in the complex schema? @@ -520,7 +542,7 @@ abstract class AbstractPrequeryGenerator(constructClause: ConstructClause, * @param linkingPropertyQueryVariable variable representing a linking property. * @return variable representing the corresponding link value property. */ - private def createlinkValuePropertyVariableFromLinkingPropertyVariable(linkingPropertyQueryVariable: QueryVariable): QueryVariable = { + private def createLinkValuePropertyVariableFromLinkingPropertyVariable(linkingPropertyQueryVariable: QueryVariable): QueryVariable = { SparqlTransformer.createUniqueVariableNameFromEntityAndProperty( base = linkingPropertyQueryVariable, propertyIri = OntologyConstants.KnoraBase.HasLinkToValue @@ -562,7 +584,7 @@ abstract class AbstractPrequeryGenerator(constructClause: ConstructClause, if (OntologyConstants.KnoraApi.isKnoraApiV2Resource(propInfo.objectTypeIri)) { // it is a linking property, restrict the link value property val restrictionForLinkValueProp = CompareExpression( - leftArg = createlinkValuePropertyVariableFromLinkingPropertyVariable(queryVar), // the same variable was created during statement processing in WHERE clause in `convertStatementForPropertyType` + leftArg = createLinkValuePropertyVariableFromLinkingPropertyVariable(queryVar), // the same variable was created during statement processing in WHERE clause in `convertStatementForPropertyType` operator = comparisonOperator, rightArg = IriRef(internalIriRef.iri.fromLinkPropToLinkValueProp)) // create link value property from linking property @@ -649,29 +671,38 @@ abstract class AbstractPrequeryGenerator(constructClause: ConstructClause, if (validComparisonOperators.nonEmpty && !validComparisonOperators(comparisonOperator)) throw GravsearchException(s"Invalid operator '$comparisonOperator' in expression (allowed operators in this context are ${validComparisonOperators.map(op => "'" + op + "'").mkString(", ")})") - // Generate a variable name representing the literal attached to the value object - val valueObjectLiteralVar: QueryVariable = SparqlTransformer.createUniqueVariableNameFromEntityAndProperty( - base = queryVar, - propertyIri = valueHasProperty - ) - - // Add a statement to assign the literal to a variable, which we'll use in the transformed FILTER expression, - // if that statement hasn't been added already. - - val statementToAddForValueHas: Seq[StatementPattern] = if (addGeneratedVariableForValueLiteral(queryVar, valueObjectLiteralVar)) { - Seq( - // connects the query variable with the value object (internal structure: values are represented as objects) - StatementPattern.makeExplicit(subj = queryVar, pred = IriRef(valueHasProperty.toSmartIri), valueObjectLiteralVar) + // Does the variable refer to resource metadata? + if (resourceMetadataVariables.contains(queryVar)) { + // Yes. Leave the expression as is. + TransformedFilterPattern( + Some(CompareExpression(queryVar, comparisonOperator, literal)), + Seq.empty ) } else { - Seq.empty[StatementPattern] - } + // The variable does not refer to resource metadata. + // Generate a variable name representing the literal attached to the value object. + val valueObjectLiteralVar: QueryVariable = SparqlTransformer.createUniqueVariableNameFromEntityAndProperty( + base = queryVar, + propertyIri = valueHasProperty + ) - TransformedFilterPattern( - Some(CompareExpression(valueObjectLiteralVar, comparisonOperator, literal)), // compares the provided literal to the value object's literal value - statementToAddForValueHas - ) + // Add a statement to assign the literal to a variable, which we'll use in the transformed FILTER expression, + // if that statement hasn't been added already. + + val statementToAddForValueHas: Seq[StatementPattern] = if (addGeneratedVariableForValueLiteral(queryVar, valueObjectLiteralVar)) { + Seq( + // connects the query variable with the value object (internal structure: values are represented as objects) + StatementPattern.makeExplicit(subj = queryVar, pred = IriRef(valueHasProperty.toSmartIri), valueObjectLiteralVar) + ) + } else { + Seq.empty[StatementPattern] + } + TransformedFilterPattern( + Some(CompareExpression(valueObjectLiteralVar, comparisonOperator, literal)), // compares the provided literal to the value object's literal value + statementToAddForValueHas + ) + } } /** @@ -1036,7 +1067,7 @@ abstract class AbstractPrequeryGenerator(constructClause: ConstructClause, case _ => throw GravsearchException(s"First argument of regex function must be a variable") } - // make sure that the query variable (first argument of regex function) represents a text value + // make sure that the query variable (first argument of regex function) represents string literal typeInspectionResult.getTypeOfEntity(regexQueryVar) match { case Some(typeInfo) => typeInfo match { @@ -1057,28 +1088,35 @@ abstract class AbstractPrequeryGenerator(constructClause: ConstructClause, throw GravsearchException(s"No type information found about ${regexQueryVar.toSparql}") } - // Generate a variable name representing the string literal - val textValHasString: QueryVariable = SparqlTransformer.createUniqueVariableNameFromEntityAndProperty(base = regexQueryVar, propertyIri = OntologyConstants.KnoraBase.ValueHasString) - - // Add a statement to assign the literal to a variable, which we'll use in the transformed FILTER expression, - // if that statement hasn't been added already. - val statementToAddForValueHasString: Seq[StatementPattern] = if (addGeneratedVariableForValueLiteral(regexQueryVar, textValHasString)) { - Seq( - // connects the value object with the value literal - StatementPattern.makeExplicit(subj = regexQueryVar, pred = IriRef(OntologyConstants.KnoraBase.ValueHasString.toSmartIri), textValHasString) + // Does the variable refer to resource metadata? + if (resourceMetadataVariables.contains(regexQueryVar)) { + // Yes. Leave the expression as is. + TransformedFilterPattern( + Some(RegexFunction(regexQueryVar, regexFunctionCall.pattern, regexFunctionCall.modifier)), + Seq.empty ) } else { - Seq.empty[StatementPattern] - } - + // No, it refers to a TextValue. Generate a variable name representing the string literal. + val textValHasString: QueryVariable = SparqlTransformer.createUniqueVariableNameFromEntityAndProperty(base = regexQueryVar, propertyIri = OntologyConstants.KnoraBase.ValueHasString) + + // Add a statement to assign the literal to a variable, which we'll use in the transformed FILTER expression, + // if that statement hasn't been added already. + val statementToAddForValueHasString: Seq[StatementPattern] = if (addGeneratedVariableForValueLiteral(regexQueryVar, textValHasString)) { + Seq( + // connects the value object with the value literal + StatementPattern.makeExplicit(subj = regexQueryVar, pred = IriRef(OntologyConstants.KnoraBase.ValueHasString.toSmartIri), textValHasString) + ) + } else { + Seq.empty[StatementPattern] + } - TransformedFilterPattern( - Some(RegexFunction(textValHasString, regexFunctionCall.pattern, regexFunctionCall.modifier)), - statementToAddForValueHasString - ) + TransformedFilterPattern( + Some(RegexFunction(textValHasString, regexFunctionCall.pattern, regexFunctionCall.modifier)), + statementToAddForValueHasString + ) + } } - } /** @@ -1161,7 +1199,7 @@ abstract class AbstractPrequeryGenerator(constructClause: ConstructClause, val functionIri: SmartIri = functionCallExpression.functionIri.iri if (querySchema == ApiV2Complex) { - throw GravsearchException(s"Function ${functionIri.toSparql} cannot be used in a Gravsearch query written in the complex schema; use ${OntologyConstants.KnoraApiV2Complex.MatchFunction.toSmartIri.toSparql} instead") + throw GravsearchException(s"Function ${functionIri.toSparql} cannot be used in a Gravsearch query written in the complex schema; use ${OntologyConstants.KnoraApiV2Complex.MatchTextFunction.toSmartIri.toSparql} instead") } // The match function must be the top-level expression, otherwise boolean logic won't work properly. @@ -1226,7 +1264,6 @@ abstract class AbstractPrequeryGenerator(constructClause: ConstructClause, private def handleMatchFunctionInComplexSchema(functionCallExpression: FunctionCallExpression, typeInspectionResult: GravsearchTypeInspectionResult, isTopLevel: Boolean): TransformedFilterPattern = { val functionIri: SmartIri = functionCallExpression.functionIri.iri - if (querySchema == ApiV2Simple) { throw GravsearchException(s"Function ${functionIri.toSparql} cannot be used in a Gravsearch query written in the simple schema; use ${OntologyConstants.KnoraApiV2Simple.MatchFunction.toSmartIri.toSparql} instead") } @@ -1273,7 +1310,7 @@ abstract class AbstractPrequeryGenerator(constructClause: ConstructClause, val functionIri: SmartIri = functionCallExpression.functionIri.iri if (querySchema == ApiV2Simple) { - throw GravsearchException(s"Function ${functionIri.toSparql} cannot be used in a Gravsearch query written in the simple schema; use ${OntologyConstants.KnoraApiV2Simple.MatchFunction.toSmartIri.toSparql} instead") + throw GravsearchException(s"Function ${functionIri.toSparql} cannot be used in a Gravsearch query written in the simple schema; use ${OntologyConstants.KnoraApiV2Simple.MatchTextFunction.toSmartIri.toSparql} instead") } // The match function must be the top-level expression, otherwise boolean logic won't work properly. @@ -1546,6 +1583,106 @@ abstract class AbstractPrequeryGenerator(constructClause: ConstructClause, ) } + + /** + * Checks that the query is in the simple schema, then calls `handleMatchLabelFunction`. + * + * @param functionCallExpression the function call to be handled. + * @param typeInspectionResult the type inspection results. + * @param isTopLevel if `true`, this is the top-level expression in the `FILTER`. + * @return a [[TransformedFilterPattern]]. + */ + private def handleMatchLabelFunctionInSimpleSchema(functionCallExpression: FunctionCallExpression, typeInspectionResult: GravsearchTypeInspectionResult, isTopLevel: Boolean): TransformedFilterPattern = { + val functionIri: SmartIri = functionCallExpression.functionIri.iri + + if (querySchema == ApiV2Complex) { + throw GravsearchException(s"Function ${functionIri.toSparql} cannot be used in a Gravsearch query written in the complex schema; use ${OntologyConstants.KnoraApiV2Complex.MatchLabelFunction.toSmartIri.toSparql} instead") + } + + handleMatchLabelFunction( + functionCallExpression = functionCallExpression, + typeInspectionResult = typeInspectionResult, + isTopLevel = isTopLevel + ) + } + + /** + * Checks that the query is in the complex schema, then calls `handleMatchLabelFunction`. + * + * @param functionCallExpression the function call to be handled. + * @param typeInspectionResult the type inspection results. + * @param isTopLevel if `true`, this is the top-level expression in the `FILTER`. + * @return a [[TransformedFilterPattern]]. + */ + private def handleMatchLabelFunctionInComplexSchema(functionCallExpression: FunctionCallExpression, typeInspectionResult: GravsearchTypeInspectionResult, isTopLevel: Boolean): TransformedFilterPattern = { + val functionIri: SmartIri = functionCallExpression.functionIri.iri + + if (querySchema == ApiV2Simple) { + throw GravsearchException(s"Function ${functionIri.toSparql} cannot be used in a Gravsearch query written in the simple schema; use ${OntologyConstants.KnoraApiV2Simple.MatchLabelFunction.toSmartIri.toSparql} instead") + } + + handleMatchLabelFunction( + functionCallExpression = functionCallExpression, + typeInspectionResult = typeInspectionResult, + isTopLevel = isTopLevel + ) + } + + /** + * Handles the function `knora-api:matchLabel` in either schema. + * + * @param functionCallExpression the function call to be handled. + * @param typeInspectionResult the type inspection results. + * @param isTopLevel if `true`, this is the top-level expression in the `FILTER`. + * @return a [[TransformedFilterPattern]]. + */ + private def handleMatchLabelFunction(functionCallExpression: FunctionCallExpression, typeInspectionResult: GravsearchTypeInspectionResult, isTopLevel: Boolean): TransformedFilterPattern = { + val functionIri: SmartIri = functionCallExpression.functionIri.iri + + // The matchLabel function must be the top-level expression, otherwise boolean logic won't work properly. + if (!isTopLevel) { + throw GravsearchException(s"Function ${functionIri.toSparql} must be the top-level expression in a FILTER") + } + + // Two arguments are expected: + // 1. a variable representing a resource + // 2. a string literal + + if (functionCallExpression.args.size != 2) throw GravsearchException(s"Two arguments are expected for ${functionIri.toSparql}") + + // A QueryVariable expected to represent a resource. + val resourceVar: QueryVariable = functionCallExpression.getArgAsQueryVar(pos = 0) + + typeInspectionResult.getTypeOfEntity(resourceVar) match { + case Some(nonPropInfo: NonPropertyTypeInfo) if OntologyConstants.KnoraApi.isKnoraApiV2Resource(nonPropInfo.typeIri) => () + case _ => throw GravsearchException(s"${resourceVar.toSparql} must be a knora-api:Resource") + } + + // Add a statement to assign the literal to a variable, which we'll use in the transformed FILTER expression, + // if that statement hasn't been added already. + + val rdfsLabelVar: QueryVariable = SparqlTransformer.createUniqueVariableNameFromEntityAndProperty(base = resourceVar, propertyIri = OntologyConstants.Rdfs.Label) + + val rdfsLabelStatement = if (addGeneratedVariableForValueLiteral(resourceVar, rdfsLabelVar)) { + Seq(StatementPattern.makeExplicit(subj = resourceVar, pred = IriRef(OntologyConstants.Rdfs.Label.toSmartIri), rdfsLabelVar)) + } else { + Seq.empty[StatementPattern] + } + + val searchTerm: XsdLiteral = functionCallExpression.getArgAsLiteral(1, xsdDatatype = OntologyConstants.Xsd.String.toSmartIri) + val luceneQueryString: LuceneQueryString = LuceneQueryString(searchTerm.value) + + // Replace the filter with a LuceneQueryPattern. + TransformedFilterPattern( + None, // The FILTER has been replaced by statements. + rdfsLabelStatement :+ LuceneQueryPattern( + subj = resourceVar, + obj = rdfsLabelVar, + queryString = luceneQueryString + ) + ) + } + /** * Handles the function `knora-api:StandoffLink`. * @@ -1640,66 +1777,29 @@ abstract class AbstractPrequeryGenerator(constructClause: ConstructClause, private def handleKnoraFunctionCall(functionCallExpression: FunctionCallExpression, typeInspectionResult: GravsearchTypeInspectionResult, isTopLevel: Boolean): TransformedFilterPattern = { val functionIri: SmartIri = functionCallExpression.functionIri.iri - functionIri.toString match { - - case OntologyConstants.KnoraApiV2Simple.MatchFunction => - // deprecated - handleMatchFunctionInSimpleSchema( - functionCallExpression = functionCallExpression, - typeInspectionResult = typeInspectionResult, - isTopLevel = isTopLevel - ) - - case OntologyConstants.KnoraApiV2Complex.MatchFunction => - // deprecated - handleMatchFunctionInComplexSchema( - functionCallExpression = functionCallExpression, - typeInspectionResult = typeInspectionResult, - isTopLevel = isTopLevel - ) - - case OntologyConstants.KnoraApiV2Complex.MatchInStandoffFunction => - // deprecated - handleMatchInStandoffFunction( - functionCallExpression = functionCallExpression, - typeInspectionResult = typeInspectionResult, - isTopLevel = isTopLevel - ) - - case OntologyConstants.KnoraApiV2Simple.MatchTextFunction => - handleMatchTextFunctionInSimpleSchema( - functionCallExpression = functionCallExpression, - typeInspectionResult = typeInspectionResult, - isTopLevel = isTopLevel - ) - - case OntologyConstants.KnoraApiV2Complex.MatchTextFunction => - handleMatchTextFunctionInComplexSchema( - functionCallExpression = functionCallExpression, - typeInspectionResult = typeInspectionResult, - isTopLevel = isTopLevel - ) - - case OntologyConstants.KnoraApiV2Complex.MatchTextInStandoffFunction => - handleMatchTextInStandoffFunction( - functionCallExpression = functionCallExpression, - typeInspectionResult = typeInspectionResult, - isTopLevel = isTopLevel - ) - - case OntologyConstants.KnoraApiV2Complex.StandoffLinkFunction => - handleStandoffLinkFunction( - functionCallExpression = functionCallExpression, - typeInspectionResult = typeInspectionResult, - isTopLevel = isTopLevel - ) - + // Get a Scala function that implements the Gravsearch function. + val functionFunction: (FunctionCallExpression, GravsearchTypeInspectionResult, Boolean) => TransformedFilterPattern = functionIri.toString match { + case OntologyConstants.KnoraApiV2Simple.MatchFunction => handleMatchFunctionInSimpleSchema // deprecated + case OntologyConstants.KnoraApiV2Complex.MatchFunction => handleMatchFunctionInComplexSchema // deprecated + case OntologyConstants.KnoraApiV2Complex.MatchInStandoffFunction => handleMatchInStandoffFunction // deprecated + case OntologyConstants.KnoraApiV2Simple.MatchTextFunction => handleMatchTextFunctionInSimpleSchema + case OntologyConstants.KnoraApiV2Complex.MatchTextFunction => handleMatchTextFunctionInComplexSchema + case OntologyConstants.KnoraApiV2Simple.MatchLabelFunction => handleMatchLabelFunctionInSimpleSchema + case OntologyConstants.KnoraApiV2Complex.MatchLabelFunction => handleMatchLabelFunctionInComplexSchema + case OntologyConstants.KnoraApiV2Complex.MatchTextInStandoffFunction => handleMatchTextInStandoffFunction + case OntologyConstants.KnoraApiV2Complex.StandoffLinkFunction => handleStandoffLinkFunction case OntologyConstants.KnoraApiV2Complex.ToSimpleDateFunction => throw GravsearchException(s"Function ${functionIri.toSparql} must be used in a comparison expression") case _ => throw NotImplementedException(s"Function ${functionCallExpression.functionIri} not found") } + // Call the Scala function. + functionFunction( + functionCallExpression, + typeInspectionResult, + isTopLevel + ) } /** diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/types/GravsearchTypeInspectionRunner.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/types/GravsearchTypeInspectionRunner.scala index 7e7781e369..2c44b5171c 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/types/GravsearchTypeInspectionRunner.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/types/GravsearchTypeInspectionRunner.scala @@ -22,16 +22,17 @@ package org.knora.webapi.responders.v2.search.gravsearch.types import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.responders.ResponderData import org.knora.webapi.responders.v2.search._ +import org.knora.webapi.util.StringFormatter import org.knora.webapi.{GravsearchException, KnoraDispatchers, OntologyConstants} import scala.concurrent.{ExecutionContext, Future} /** - * Runs Gravsearch type inspection using one or more type inspector implementations. - * - * @param system the Akka actor system. - * @param inferTypes if true, use type inference. - */ + * Runs Gravsearch type inspection using one or more type inspector implementations. + * + * @param responderData the Knora [[ResponderData]]. + * @param inferTypes if true, use type inference. + */ class GravsearchTypeInspectionRunner(responderData: ResponderData, inferTypes: Boolean = true) { private implicit val executionContext: ExecutionContext = responderData.system.dispatchers.lookup(KnoraDispatchers.KnoraActorDispatcher) @@ -55,15 +56,17 @@ class GravsearchTypeInspectionRunner(responderData: ResponderData, ) /** - * Given the WHERE clause from a parsed Gravsearch query, returns information about the types found - * in the query. - * - * @param whereClause the Gravsearch WHERE clause. - * @param requestingUser the requesting user. - * @return the result of the type inspection. - */ + * Given the WHERE clause from a parsed Gravsearch query, returns information about the types found + * in the query. + * + * @param whereClause the Gravsearch WHERE clause. + * @param requestingUser the requesting user. + * @return the result of the type inspection. + */ def inspectTypes(whereClause: WhereClause, requestingUser: UserADM): Future[GravsearchTypeInspectionResult] = { + implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance + for { // Get the set of typeable entities in the Gravsearch query. typeableEntities: Set[TypeableEntity] <- Future { @@ -110,16 +113,16 @@ class GravsearchTypeInspectionRunner(responderData: ResponderData, /** - * A [[WhereVisitor]] that collects typeable entities from a Gravsearch WHERE clause. - */ + * A [[WhereVisitor]] that collects typeable entities from a Gravsearch WHERE clause. + */ private class TypeableEntityCollectingWhereVisitor extends WhereVisitor[Set[TypeableEntity]] { /** - * Collects typeable entities from a statement. - * - * @param statementPattern the pattern to be visited. - * @param acc the accumulator. - * @return the accumulator. - */ + * Collects typeable entities from a statement. + * + * @param statementPattern the pattern to be visited. + * @param acc the accumulator. + * @return the accumulator. + */ override def visitStatementInWhere(statementPattern: StatementPattern, acc: Set[TypeableEntity]): Set[TypeableEntity] = { statementPattern.pred match { case iriRef: IriRef if iriRef.iri.toString == OntologyConstants.Rdf.Type => @@ -133,23 +136,23 @@ class GravsearchTypeInspectionRunner(responderData: ResponderData, } /** - * Collects typeable entities from a `FILTER`. - * - * @param filterPattern the pattern to be visited. - * @param acc the accumulator. - * @return the accumulator. - */ + * Collects typeable entities from a `FILTER`. + * + * @param filterPattern the pattern to be visited. + * @param acc the accumulator. + * @return the accumulator. + */ override def visitFilter(filterPattern: FilterPattern, acc: Set[TypeableEntity]): Set[TypeableEntity] = { visitFilterExpression(filterPattern.expression, acc) } /** - * Collects typeable entities from a filter expression. - * - * @param filterExpression the filter expression to be visited. - * @param acc the accumulator. - * @return the accumulator. - */ + * Collects typeable entities from a filter expression. + * + * @param filterExpression the filter expression to be visited. + * @param acc the accumulator. + * @return the accumulator. + */ private def visitFilterExpression(filterExpression: Expression, acc: Set[TypeableEntity]): Set[TypeableEntity] = { filterExpression match { case compareExpr: CompareExpression => diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/types/InferringGravsearchTypeInspector.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/types/InferringGravsearchTypeInspector.scala index 3ae63a388b..6317397dc0 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/types/InferringGravsearchTypeInspector.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/types/InferringGravsearchTypeInspector.scala @@ -265,7 +265,7 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe None } - case other => + case _ => // We don't have the predicate's type. Set.empty[GravsearchEntityTypeInfo] } @@ -1004,6 +1004,26 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe (textVar -> (currentTextVarTypesFromFilters + OntologyConstants.KnoraApiV2Complex.TextValue.toSmartIri)) ) + case OntologyConstants.KnoraApiV2Simple.MatchLabelFunction => + // The first argument is a variable representing a resource. + val resourceVar = TypeableVariable(functionCallExpression.getArgAsQueryVar(0).variableName) + val currentResourceVarTypesFromFilters: Set[SmartIri] = acc.typedEntitiesInFilters.getOrElse(resourceVar, Set.empty[SmartIri]) + + acc.copy( + typedEntitiesInFilters = acc.typedEntitiesInFilters + + (resourceVar -> (currentResourceVarTypesFromFilters + OntologyConstants.KnoraApiV2Simple.Resource.toSmartIri)) + ) + + case OntologyConstants.KnoraApiV2Complex.MatchLabelFunction => + // The first argument is a variable representing a resource. + val resourceVar = TypeableVariable(functionCallExpression.getArgAsQueryVar(0).variableName) + val currentResourceVarTypesFromFilters: Set[SmartIri] = acc.typedEntitiesInFilters.getOrElse(resourceVar, Set.empty[SmartIri]) + + acc.copy( + typedEntitiesInFilters = acc.typedEntitiesInFilters + + (resourceVar -> (currentResourceVarTypesFromFilters + OntologyConstants.KnoraApiV2Complex.Resource.toSmartIri)) + ) + case OntologyConstants.KnoraApiV2Complex.MatchInStandoffFunction => // The first argument is a variable representing a string. val textVar = TypeableVariable(functionCallExpression.getArgAsQueryVar(0).variableName) diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/types/IntermediateTypeInspectionResult.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/types/IntermediateTypeInspectionResult.scala index 4848d9c3b7..2848960fca 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/types/IntermediateTypeInspectionResult.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/gravsearch/types/IntermediateTypeInspectionResult.scala @@ -19,7 +19,9 @@ package org.knora.webapi.responders.v2.search.gravsearch.types -import org.knora.webapi.AssertionException +import org.knora.webapi.util.IriConversions._ +import org.knora.webapi.util.StringFormatter +import org.knora.webapi.{AssertionException, IRI, OntologyConstants} /** * Represents an intermediate result during type inspection. This is different from [[GravsearchTypeInspectionResult]] @@ -79,12 +81,29 @@ case class IntermediateTypeInspectionResult(entities: Map[TypeableEntity, Set[Gr object IntermediateTypeInspectionResult { /** - * Constructs an [[IntermediateTypeInspectionResult]] for the given set of typeable entities, with no - * types specified. + * Constructs an [[IntermediateTypeInspectionResult]] for the given set of typeable entities, with built-in + * types specified (e.g. for `rdfs:label`). * * @param entities the set of typeable entities found in the WHERE clause of a Gravsearch query. */ - def apply(entities: Set[TypeableEntity]): IntermediateTypeInspectionResult = { - new IntermediateTypeInspectionResult(entities = entities.map(entity => entity -> Set.empty[GravsearchEntityTypeInfo]).toMap) + def apply(entities: Set[TypeableEntity])(implicit stringFormatter: StringFormatter): IntermediateTypeInspectionResult = { + // Make an IntermediateTypeInspectionResult in which each typeable entity has no types. + val emptyResult = new IntermediateTypeInspectionResult(entities = entities.map(entity => entity -> Set.empty[GravsearchEntityTypeInfo]).toMap) + + // Collect the typeable IRIs used. + val irisUsed: Set[IRI] = entities.collect { + case typeableIri: TypeableIri => typeableIri.iri.toString + } + + // Find the IRIs that represent resource metadata properties, and get their object types. + val resourceMetadataPropertyTypesUsed: Map[TypeableIri, PropertyTypeInfo] = OntologyConstants.ResourceMetadataPropertyAxioms.filterKeys(irisUsed).map { + case (propertyIri, objectTypeIri) => TypeableIri(propertyIri.toSmartIri) -> PropertyTypeInfo(objectTypeIri = objectTypeIri.toSmartIri) + } + + // Add those types to the IntermediateTypeInspectionResult. + resourceMetadataPropertyTypesUsed.foldLeft(emptyResult) { + case (acc: IntermediateTypeInspectionResult, (entity: TypeableIri, entityType: PropertyTypeInfo)) => + acc.addTypes(entity, Set(entityType)) + } } } \ No newline at end of file diff --git "a/webapi/src/test/resources/test-data/searchR2RV2/Zeitgl\303\266ckleinViaLabel.jsonld" "b/webapi/src/test/resources/test-data/searchR2RV2/Zeitgl\303\266ckleinViaLabel.jsonld" new file mode 100644 index 0000000000..547f0ebb20 --- /dev/null +++ "b/webapi/src/test/resources/test-data/searchR2RV2/Zeitgl\303\266ckleinViaLabel.jsonld" @@ -0,0 +1,58 @@ +{ + "@graph" : [ { + "@id" : "http://rdfh.ch/0803/c5058f3a", + "@type" : "incunabula:book", + "knora-api:arkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0803/c5058f3a5" + }, + "knora-api:attachedToProject" : { + "@id" : "http://rdfh.ch/projects/0803" + }, + "knora-api:attachedToUser" : { + "@id" : "http://rdfh.ch/users/91e19f1e01" + }, + "knora-api:creationDate" : { + "@type" : "xsd:dateTimeStamp", + "@value" : "2016-03-02T15:05:10Z" + }, + "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/0803/c5058f3a5.20160302T150510Z" + }, + "rdfs:label" : "Zeitglöcklein des Lebens und Leidens Christi" + }, { + "@id" : "http://rdfh.ch/0803/ff17e5ef9601", + "@type" : "incunabula:book", + "knora-api:arkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0803/ff17e5ef9601j" + }, + "knora-api:attachedToProject" : { + "@id" : "http://rdfh.ch/projects/0803" + }, + "knora-api:attachedToUser" : { + "@id" : "http://rdfh.ch/users/91e19f1e01" + }, + "knora-api:creationDate" : { + "@type" : "xsd:dateTimeStamp", + "@value" : "2016-03-02T15:05:23Z" + }, + "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/0803/ff17e5ef9601j.20160302T150523Z" + }, + "rdfs:label" : "Zeitglöcklein des Lebens und Leidens Christi" + } ], + "@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#", + "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 6743c5ac19..c0ae84f975 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 @@ -8181,5 +8181,183 @@ class SearchRouteV2R2RSpec extends R2RSpec { xmlDiff.hasDifferences should be(false) } } + + "search for an rdfs:label using a literal in the simple schema" in { + val gravsearchQuery: String = + """ + |PREFIX incunabula: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?book knora-api:isMainResource true . + | + |} WHERE { + | ?book rdf:type incunabula:book . + | ?book rdfs:label "Zeitglöcklein des Lebens und Leidens Christi" . + |}""".stripMargin + + Post("/v2/searchextended", HttpEntity(SparqlQueryConstants.`application/sparql-query`, gravsearchQuery)) ~> searchPath ~> check { + val searchResponseStr = responseAs[String] + assert(status == StatusCodes.OK, searchResponseStr) + val expectedAnswerJSONLD = readOrWriteTextFile(searchResponseStr, new File("src/test/resources/test-data/searchR2RV2/ZeitglöckleinViaLabel.jsonld"), writeTestDataFiles) + compareJSONLDForResourcesResponse(expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = searchResponseStr) + } + } + + "search for an rdfs:label using a literal in the complex schema" in { + val gravsearchQuery: String = + """ + |PREFIX incunabula: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?book knora-api:isMainResource true . + | + |} WHERE { + | ?book rdf:type incunabula:book . + | ?book rdfs:label "Zeitglöcklein des Lebens und Leidens Christi" . + |}""".stripMargin + + Post("/v2/searchextended", HttpEntity(SparqlQueryConstants.`application/sparql-query`, gravsearchQuery)) ~> searchPath ~> check { + val searchResponseStr = responseAs[String] + assert(status == StatusCodes.OK, searchResponseStr) + val expectedAnswerJSONLD = readOrWriteTextFile(searchResponseStr, new File("src/test/resources/test-data/searchR2RV2/ZeitglöckleinViaLabel.jsonld"), false) + compareJSONLDForResourcesResponse(expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = searchResponseStr) + } + } + + "search for an rdfs:label using a variable in the simple schema" in { + val gravsearchQuery: String = + """ + |PREFIX incunabula: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?book knora-api:isMainResource true . + | + |} WHERE { + | ?book rdf:type incunabula:book . + | ?book rdfs:label ?label . + | FILTER(?label = "Zeitglöcklein des Lebens und Leidens Christi") + |}""".stripMargin + + Post("/v2/searchextended", HttpEntity(SparqlQueryConstants.`application/sparql-query`, gravsearchQuery)) ~> searchPath ~> check { + val searchResponseStr = responseAs[String] + assert(status == StatusCodes.OK, searchResponseStr) + val expectedAnswerJSONLD = readOrWriteTextFile(searchResponseStr, new File("src/test/resources/test-data/searchR2RV2/ZeitglöckleinViaLabel.jsonld"), false) + compareJSONLDForResourcesResponse(expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = searchResponseStr) + } + } + + "search for an rdfs:label using a variable in the complex schema" in { + val gravsearchQuery: String = + """ + |PREFIX incunabula: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?book knora-api:isMainResource true . + | + |} WHERE { + | ?book rdf:type incunabula:book . + | ?book rdfs:label ?label . + | FILTER(?label = "Zeitglöcklein des Lebens und Leidens Christi") + |}""".stripMargin + + Post("/v2/searchextended", HttpEntity(SparqlQueryConstants.`application/sparql-query`, gravsearchQuery)) ~> searchPath ~> check { + val searchResponseStr = responseAs[String] + assert(status == StatusCodes.OK, searchResponseStr) + val expectedAnswerJSONLD = readOrWriteTextFile(searchResponseStr, new File("src/test/resources/test-data/searchR2RV2/ZeitglöckleinViaLabel.jsonld"), false) + compareJSONLDForResourcesResponse(expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = searchResponseStr) + } + } + + "search for an rdfs:label using knora-api:matchLabel in the simple schema" in { + val gravsearchQuery: String = + """ + |PREFIX incunabula: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?book knora-api:isMainResource true . + | + |} WHERE { + | ?book rdf:type incunabula:book . + | FILTER knora-api:matchLabel(?book, "Zeitglöcklein") + |}""".stripMargin + + Post("/v2/searchextended", HttpEntity(SparqlQueryConstants.`application/sparql-query`, gravsearchQuery)) ~> searchPath ~> check { + val searchResponseStr = responseAs[String] + assert(status == StatusCodes.OK, searchResponseStr) + val expectedAnswerJSONLD = readOrWriteTextFile(searchResponseStr, new File("src/test/resources/test-data/searchR2RV2/ZeitglöckleinViaLabel.jsonld"), false) + compareJSONLDForResourcesResponse(expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = searchResponseStr) + } + } + + "search for an rdfs:label using knora-api:matchLabel in the complex schema" in { + val gravsearchQuery: String = + """ + |PREFIX incunabula: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?book knora-api:isMainResource true . + | + |} WHERE { + | ?book rdf:type incunabula:book . + | FILTER knora-api:matchLabel(?book, "Zeitglöcklein") + |}""".stripMargin + + Post("/v2/searchextended", HttpEntity(SparqlQueryConstants.`application/sparql-query`, gravsearchQuery)) ~> searchPath ~> check { + val searchResponseStr = responseAs[String] + assert(status == StatusCodes.OK, searchResponseStr) + val expectedAnswerJSONLD = readOrWriteTextFile(searchResponseStr, new File("src/test/resources/test-data/searchR2RV2/ZeitglöckleinViaLabel.jsonld"), false) + compareJSONLDForResourcesResponse(expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = searchResponseStr) + } + } + + "search for an rdfs:label using the regex function in the simple schema" in { + val gravsearchQuery: String = + """ + |PREFIX incunabula: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?book knora-api:isMainResource true . + |} WHERE { + | ?book rdf:type incunabula:book . + | ?book rdfs:label ?bookLabel . + | FILTER regex(?bookLabel, "Zeit", "i") + |}""".stripMargin + + Post("/v2/searchextended", HttpEntity(SparqlQueryConstants.`application/sparql-query`, gravsearchQuery)) ~> searchPath ~> check { + val searchResponseStr = responseAs[String] + assert(status == StatusCodes.OK, searchResponseStr) + val expectedAnswerJSONLD = readOrWriteTextFile(searchResponseStr, new File("src/test/resources/test-data/searchR2RV2/ZeitglöckleinViaLabel.jsonld"), false) + compareJSONLDForResourcesResponse(expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = searchResponseStr) + } + } + + "search for an rdfs:label using the regex function in the complex schema" in { + val gravsearchQuery: String = + """ + |PREFIX incunabula: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?book knora-api:isMainResource true . + |} WHERE { + | ?book rdf:type incunabula:book . + | ?book rdfs:label ?bookLabel . + | FILTER regex(?bookLabel, "Zeit", "i") + |}""".stripMargin + + Post("/v2/searchextended", HttpEntity(SparqlQueryConstants.`application/sparql-query`, gravsearchQuery)) ~> searchPath ~> check { + val searchResponseStr = responseAs[String] + assert(status == StatusCodes.OK, searchResponseStr) + val expectedAnswerJSONLD = readOrWriteTextFile(searchResponseStr, new File("src/test/resources/test-data/searchR2RV2/ZeitglöckleinViaLabel.jsonld"), false) + compareJSONLDForResourcesResponse(expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = searchResponseStr) + } + } } } diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/search/gravsearch/GravsearchParserSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/search/gravsearch/GravsearchParserSpec.scala index d850d5691d..31118ac348 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/search/gravsearch/GravsearchParserSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/search/gravsearch/GravsearchParserSpec.scala @@ -22,155 +22,14 @@ package org.knora.webapi.responders.v2.search.gravsearch import org.knora.webapi.responders.v2.search._ import org.knora.webapi.util.IriConversions._ import org.knora.webapi.util.StringFormatter -import org.knora.webapi.{ApiV2Simple, ApiV2Complex, CoreSpec, GravsearchException} +import org.knora.webapi.{ApiV2Complex, ApiV2Simple, CoreSpec, GravsearchException} /** - * Tests [[GravsearchParser]]. - */ + * Tests [[GravsearchParser]]. + */ class GravsearchParserSpec extends CoreSpec() { private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - "The GravsearchParser object" should { - "parse a Gravsearch query" in { - val parsed: ConstructQuery = GravsearchParser.parseQuery(Query) - parsed should ===(ParsedQuery) - val reparsed = GravsearchParser.parseQuery(parsed.toSparql) - reparsed should ===(parsed) - } - - "parse a Gravsearch query with a BIND" in { - val parsed: ConstructQuery = GravsearchParser.parseQuery(QueryWithBind) - parsed should ===(ParsedQueryWithBind) - val reparsed = GravsearchParser.parseQuery(parsed.toSparql) - reparsed should ===(parsed) - } - - "parse a Gravsearch query with a FILTER containing a Boolean operator" in { - val parsed: ConstructQuery = GravsearchParser.parseQuery(QueryForAThingRelatingToAnotherThing) - parsed should ===(ParsedQueryForAThingRelatingToAnotherThing) - val reparsed = GravsearchParser.parseQuery(parsed.toSparql) - reparsed should ===(parsed) - } - - "parse a Gravsearch query with FILTER NOT EXISTS" in { - val parsed: ConstructQuery = GravsearchParser.parseQuery(QueryWithFilterNotExists) - parsed should ===(ParsedQueryWithFilterNotExists) - val reparsed = GravsearchParser.parseQuery(parsed.toSparql) - reparsed should ===(parsed) - } - - "parse a Gravsearch query with MINUS" in { - val parsed: ConstructQuery = GravsearchParser.parseQuery(QueryWithMinus) - parsed should ===(ParsedQueryWithMinus) - val reparsed = GravsearchParser.parseQuery(parsed.toSparql) - reparsed should ===(parsed) - } - - "parse a Gravsearch query with OFFSET" in { - val parsed: ConstructQuery = GravsearchParser.parseQuery(QueryWithOffset) - parsed should ===(ParsedQueryWithOffset) - val reparsed = GravsearchParser.parseQuery(parsed.toSparql) - reparsed should ===(parsed) - } - - "parse a Gravsearch query with a FILTER containing a regex function" in { - val parsed = GravsearchParser.parseQuery(queryWithFilterContainingRegex) - parsed should ===(ParsedQueryWithFilterContainingRegex) - val reparsed = GravsearchParser.parseQuery(parsed.toSparql) - reparsed should ===(parsed) - } - - "accept a custom 'match' function in a FILTER" in { - val parsed: ConstructQuery = GravsearchParser.parseQuery(QueryWithMatchFunction) - parsed should ===(ParsedQueryWithMatchFunction) - val reparsed = GravsearchParser.parseQuery(parsed.toSparql) - reparsed should ===(parsed) - } - - "parse a Gravsearch query with a FILTER containing a lang function" in { - val parsed = GravsearchParser.parseQuery(QueryWithFilterContainingLang) - parsed should ===(ParsedQueryWithLangFunction) - val reparsed = GravsearchParser.parseQuery(parsed.toSparql) - reparsed should ===(parsed) - } - - "parse a Gravsearch query containing a FILTER in an OPTIONAL" in { - val parsed = GravsearchParser.parseQuery(QueryWithFilterInOptional) - parsed should ===(ParsedQueryWithFilterInOptional) - val reparsed = GravsearchParser.parseQuery(parsed.toSparql) - reparsed should ===(parsed) - } - - "parse a Gravsearch query containing a nested OPTIONAL" in { - val parsed = GravsearchParser.parseQuery(QueryStrWithNestedOptional) - parsed should ===(ParsedQueryWithNestedOptional) - val reparsed = GravsearchParser.parseQuery(parsed.toSparql) - reparsed should ===(parsed) - } - - "parse a Gravsearch query containing a UNION in an OPTIONAL" in { - val parsed = GravsearchParser.parseQuery(QueryStrWithUnionInOptional) - parsed should ===(ParsedQueryWithUnionInOptional) - val reparsed = GravsearchParser.parseQuery(parsed.toSparql) - reparsed should ===(parsed) - } - - "parse a Gravsearch query containing an IRI as a function argument" in { - val parsed = GravsearchParser.parseQuery(QueryWithIriArgInFunction) - parsed should ===(ParsedQueryWithIriArgInFunction) - val reparsed = GravsearchParser.parseQuery(parsed.toSparql) - reparsed should ===(parsed) - } - - "reject a SELECT query" in { - assertThrows[GravsearchException] { - GravsearchParser.parseQuery(SparqlSelect) - } - } - - "reject a DESCRIBE query" in { - assertThrows[GravsearchException] { - GravsearchParser.parseQuery(SparqlDescribe) - } - } - - "reject an INSERT" in { - assertThrows[GravsearchException] { - GravsearchParser.parseQuery(SparqlInsert) - } - } - - "reject a DELETE" in { - assertThrows[GravsearchException] { - GravsearchParser.parseQuery(SparqlDelete) - } - } - - "reject an internal ontology IRI" in { - assertThrows[GravsearchException] { - GravsearchParser.parseQuery(QueryWithInternalEntityIri) - } - } - - "reject left-nested UNIONs" in { - assertThrows[GravsearchException] { - GravsearchParser.parseQuery(QueryWithLeftNestedUnion) - } - } - - "reject right-nested UNIONs" in { - assertThrows[GravsearchException] { - GravsearchParser.parseQuery(QueryStrWithRightNestedUnion) - } - } - - "reject an unsupported FILTER" in { - assertThrows[GravsearchException] { - GravsearchParser.parseQuery(QueryWithWrongFilter) - } - } - } - val Query: String = """ |PREFIX rdf: @@ -648,6 +507,19 @@ class GravsearchParserSpec extends CoreSpec() { |} """.stripMargin + val QueryWithMatchTextFunction: String = + """ + |PREFIX knora-api: + |PREFIX anything: + | + |CONSTRUCT { + | ?thing knora-api:isMainResource true . + |} WHERE { + | ?thing a anything:Thing . + | ?thing anything:hasText ?text . + | FILTER(knora-api:matchText(?text, "foo")) + |} + """.stripMargin val QueryWithFilterContainingLang: String = """ @@ -712,7 +584,7 @@ class GravsearchParserSpec extends CoreSpec() { | } ORDER BY ?date """.stripMargin - private val ParsedQueryWithFilterInOptional = ConstructQuery( + private val ParsedQueryWithFilterInOptional: ConstructQuery = ConstructQuery( constructClause = ConstructClause( statements = Vector( StatementPattern( @@ -1002,7 +874,7 @@ class GravsearchParserSpec extends CoreSpec() { ) ) - val ParsedQuery = ConstructQuery( + val ParsedQuery: ConstructQuery = ConstructQuery( constructClause = ConstructClause( statements = Vector( StatementPattern( @@ -1260,7 +1132,7 @@ class GravsearchParserSpec extends CoreSpec() { ) ) - val ParsedQueryWithBind = ConstructQuery( + val ParsedQueryWithBind: ConstructQuery = ConstructQuery( constructClause = ConstructClause( statements = Vector(StatementPattern( subj = QueryVariable(variableName = "thing"), @@ -1340,7 +1212,7 @@ class GravsearchParserSpec extends CoreSpec() { ) ) - val ParsedQueryForAThingRelatingToAnotherThing = ConstructQuery( + val ParsedQueryForAThingRelatingToAnotherThing: ConstructQuery = ConstructQuery( constructClause = ConstructClause( statements = Vector( StatementPattern( @@ -1451,7 +1323,7 @@ class GravsearchParserSpec extends CoreSpec() { ) ) - val ParsedQueryWithFilterNotExists = ConstructQuery( + val ParsedQueryWithFilterNotExists: ConstructQuery = ConstructQuery( constructClause = ConstructClause( statements = Vector(StatementPattern( subj = QueryVariable(variableName = "thing"), @@ -1513,7 +1385,7 @@ class GravsearchParserSpec extends CoreSpec() { ) ) - val ParsedQueryWithMinus = ConstructQuery( + val ParsedQueryWithMinus: ConstructQuery = ConstructQuery( constructClause = ConstructClause( statements = Vector(StatementPattern( subj = QueryVariable(variableName = "thing"), @@ -1575,7 +1447,7 @@ class GravsearchParserSpec extends CoreSpec() { ) ) - val ParsedQueryWithOffset = ConstructQuery( + val ParsedQueryWithOffset: ConstructQuery = ConstructQuery( constructClause = ConstructClause( statements = Vector(StatementPattern( subj = QueryVariable(variableName = "thing"), @@ -1626,7 +1498,7 @@ class GravsearchParserSpec extends CoreSpec() { ) ) - val ParsedQueryWithFilterContainingRegex = ConstructQuery( + val ParsedQueryWithFilterContainingRegex: ConstructQuery = ConstructQuery( constructClause = ConstructClause( statements = Vector( StatementPattern( @@ -1760,7 +1632,7 @@ class GravsearchParserSpec extends CoreSpec() { ) ) - val ParsedQueryWithMatchFunction = ConstructQuery( + val ParsedQueryWithMatchFunction: ConstructQuery = ConstructQuery( constructClause = ConstructClause( statements = Vector(StatementPattern( subj = QueryVariable(variableName = "thing"), @@ -1840,7 +1712,87 @@ class GravsearchParserSpec extends CoreSpec() { ) ) - val ParsedQueryWithLangFunction = ConstructQuery( + val ParsedQueryWithMatchTextFunction: ConstructQuery = ConstructQuery( + constructClause = ConstructClause( + statements = Vector(StatementPattern( + subj = QueryVariable(variableName = "thing"), + pred = IriRef( + iri = "http://api.knora.org/ontology/knora-api/simple/v2#isMainResource".toSmartIri, + propertyPathOperator = None + ), + obj = XsdLiteral( + value = "true", + datatype = "http://www.w3.org/2001/XMLSchema#boolean".toSmartIri + ), + namedGraph = None + )), + querySchema = Some(ApiV2Simple) + ), + querySchema = Some(ApiV2Simple), + offset = 0, + orderBy = Nil, + whereClause = WhereClause( + patterns = Vector( + StatementPattern( + subj = QueryVariable(variableName = "thing"), + pred = IriRef( + iri = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".toSmartIri, + propertyPathOperator = None + ), + obj = IriRef( + iri = "http://0.0.0.0:3333/ontology/0001/anything/simple/v2#Thing".toSmartIri, + propertyPathOperator = None + ), + namedGraph = None + ), + StatementPattern( + subj = QueryVariable(variableName = "thing"), + pred = IriRef( + iri = "http://0.0.0.0:3333/ontology/0001/anything/simple/v2#hasText".toSmartIri, + propertyPathOperator = None + ), + obj = QueryVariable(variableName = "text"), + namedGraph = None + ), + FilterPattern(expression = FunctionCallExpression( + functionIri = IriRef( + iri = "http://api.knora.org/ontology/knora-api/simple/v2#matchText".toSmartIri, + propertyPathOperator = None + ), + args = Vector( + QueryVariable(variableName = "text"), + XsdLiteral( + value = "foo", + datatype = "http://www.w3.org/2001/XMLSchema#string".toSmartIri + ) + ) + )) + ), + positiveEntities = Set( + QueryVariable(variableName = "thing"), + IriRef( + iri = "http://0.0.0.0:3333/ontology/0001/anything/simple/v2#hasText".toSmartIri, + propertyPathOperator = None + ), + IriRef( + iri = "http://api.knora.org/ontology/knora-api/simple/v2#isMainResource".toSmartIri, + propertyPathOperator = None + ), + IriRef( + iri = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".toSmartIri, + propertyPathOperator = None + ), + QueryVariable(variableName = "text"), + IriRef( + iri = "http://0.0.0.0:3333/ontology/0001/anything/simple/v2#Thing".toSmartIri, + propertyPathOperator = None + ) + ), + querySchema = Some(ApiV2Simple) + ) + ) + + val ParsedQueryWithLangFunction: ConstructQuery = ConstructQuery( constructClause = ConstructClause( statements = Vector(StatementPattern( subj = QueryVariable(variableName = "thing"), @@ -1915,7 +1867,7 @@ class GravsearchParserSpec extends CoreSpec() { ) ) - val ParsedQueryWithNestedOptional = ConstructQuery( + val ParsedQueryWithNestedOptional: ConstructQuery = ConstructQuery( constructClause = ConstructClause( statements = Vector( StatementPattern( @@ -2072,7 +2024,7 @@ class GravsearchParserSpec extends CoreSpec() { |} """.stripMargin - val ParsedQueryWithIriArgInFunction = ConstructQuery( + val ParsedQueryWithIriArgInFunction: ConstructQuery = ConstructQuery( constructClause = ConstructClause( statements = Vector( StatementPattern( @@ -2197,4 +2149,152 @@ class GravsearchParserSpec extends CoreSpec() { querySchema = Some(ApiV2Complex) ) ) + + "The GravsearchParser object" should { + "parse a Gravsearch query" in { + val parsed: ConstructQuery = GravsearchParser.parseQuery(Query) + parsed should ===(ParsedQuery) + val reparsed = GravsearchParser.parseQuery(parsed.toSparql) + reparsed should ===(parsed) + } + + "parse a Gravsearch query with a BIND" in { + val parsed: ConstructQuery = GravsearchParser.parseQuery(QueryWithBind) + parsed should ===(ParsedQueryWithBind) + val reparsed = GravsearchParser.parseQuery(parsed.toSparql) + reparsed should ===(parsed) + } + + "parse a Gravsearch query with a FILTER containing a Boolean operator" in { + val parsed: ConstructQuery = GravsearchParser.parseQuery(QueryForAThingRelatingToAnotherThing) + parsed should ===(ParsedQueryForAThingRelatingToAnotherThing) + val reparsed = GravsearchParser.parseQuery(parsed.toSparql) + reparsed should ===(parsed) + } + + "parse a Gravsearch query with FILTER NOT EXISTS" in { + val parsed: ConstructQuery = GravsearchParser.parseQuery(QueryWithFilterNotExists) + parsed should ===(ParsedQueryWithFilterNotExists) + val reparsed = GravsearchParser.parseQuery(parsed.toSparql) + reparsed should ===(parsed) + } + + "parse a Gravsearch query with MINUS" in { + val parsed: ConstructQuery = GravsearchParser.parseQuery(QueryWithMinus) + parsed should ===(ParsedQueryWithMinus) + val reparsed = GravsearchParser.parseQuery(parsed.toSparql) + reparsed should ===(parsed) + } + + "parse a Gravsearch query with OFFSET" in { + val parsed: ConstructQuery = GravsearchParser.parseQuery(QueryWithOffset) + parsed should ===(ParsedQueryWithOffset) + val reparsed = GravsearchParser.parseQuery(parsed.toSparql) + reparsed should ===(parsed) + } + + "parse a Gravsearch query with a FILTER containing a regex function" in { + val parsed = GravsearchParser.parseQuery(queryWithFilterContainingRegex) + parsed should ===(ParsedQueryWithFilterContainingRegex) + val reparsed = GravsearchParser.parseQuery(parsed.toSparql) + reparsed should ===(parsed) + } + + "accept a custom 'match' function in a FILTER" in { + val parsed: ConstructQuery = GravsearchParser.parseQuery(QueryWithMatchFunction) + parsed should ===(ParsedQueryWithMatchFunction) + val reparsed = GravsearchParser.parseQuery(parsed.toSparql) + reparsed should ===(parsed) + } + + "accept a custom 'matchText' function in a FILTER" in { + val parsed: ConstructQuery = GravsearchParser.parseQuery(QueryWithMatchTextFunction) + parsed should ===(ParsedQueryWithMatchTextFunction) + val reparsed = GravsearchParser.parseQuery(parsed.toSparql) + reparsed should ===(parsed) + } + + "parse a Gravsearch query with a FILTER containing a lang function" in { + val parsed = GravsearchParser.parseQuery(QueryWithFilterContainingLang) + parsed should ===(ParsedQueryWithLangFunction) + val reparsed = GravsearchParser.parseQuery(parsed.toSparql) + reparsed should ===(parsed) + } + + "parse a Gravsearch query containing a FILTER in an OPTIONAL" in { + val parsed = GravsearchParser.parseQuery(QueryWithFilterInOptional) + parsed should ===(ParsedQueryWithFilterInOptional) + val reparsed = GravsearchParser.parseQuery(parsed.toSparql) + reparsed should ===(parsed) + } + + "parse a Gravsearch query containing a nested OPTIONAL" in { + val parsed = GravsearchParser.parseQuery(QueryStrWithNestedOptional) + parsed should ===(ParsedQueryWithNestedOptional) + val reparsed = GravsearchParser.parseQuery(parsed.toSparql) + reparsed should ===(parsed) + } + + "parse a Gravsearch query containing a UNION in an OPTIONAL" in { + val parsed = GravsearchParser.parseQuery(QueryStrWithUnionInOptional) + parsed should ===(ParsedQueryWithUnionInOptional) + val reparsed = GravsearchParser.parseQuery(parsed.toSparql) + reparsed should ===(parsed) + } + + "parse a Gravsearch query containing an IRI as a function argument" in { + val parsed = GravsearchParser.parseQuery(QueryWithIriArgInFunction) + parsed should ===(ParsedQueryWithIriArgInFunction) + val reparsed = GravsearchParser.parseQuery(parsed.toSparql) + reparsed should ===(parsed) + } + + "reject a SELECT query" in { + assertThrows[GravsearchException] { + GravsearchParser.parseQuery(SparqlSelect) + } + } + + "reject a DESCRIBE query" in { + assertThrows[GravsearchException] { + GravsearchParser.parseQuery(SparqlDescribe) + } + } + + "reject an INSERT" in { + assertThrows[GravsearchException] { + GravsearchParser.parseQuery(SparqlInsert) + } + } + + "reject a DELETE" in { + assertThrows[GravsearchException] { + GravsearchParser.parseQuery(SparqlDelete) + } + } + + "reject an internal ontology IRI" in { + assertThrows[GravsearchException] { + GravsearchParser.parseQuery(QueryWithInternalEntityIri) + } + } + + "reject left-nested UNIONs" in { + assertThrows[GravsearchException] { + GravsearchParser.parseQuery(QueryWithLeftNestedUnion) + } + } + + "reject right-nested UNIONs" in { + assertThrows[GravsearchException] { + GravsearchParser.parseQuery(QueryStrWithRightNestedUnion) + } + } + + "reject an unsupported FILTER" in { + assertThrows[GravsearchException] { + GravsearchParser.parseQuery(QueryWithWrongFilter) + } + } + } } diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala index 6dc64191aa..9c77e17c06 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala @@ -585,83 +585,46 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec """.stripMargin val transformedQueryWithDateOptionalSortCriterionAndFilter: SelectQuery = - SelectQuery( - variables = Vector( - QueryVariable(variableName = "thing"), - GroupConcat( - inputVariable = QueryVariable(variableName = "date"), - separator = StringFormatter.INFORMATION_SEPARATOR_ONE, - outputVariableName = "date__Concat", - ) - ), - offset = 0, - groupBy = Vector( - QueryVariable(variableName = "thing"), - QueryVariable(variableName = "date__valueHasStartJDN") - ), - orderBy = Vector( - OrderCriterion( - queryVariable = QueryVariable(variableName = "date__valueHasStartJDN"), - isAscending = false + SelectQuery( + variables = Vector( + QueryVariable(variableName = "thing"), + GroupConcat( + inputVariable = QueryVariable(variableName = "date"), + separator = StringFormatter.INFORMATION_SEPARATOR_ONE, + outputVariableName = "date__Concat", + ) ), - OrderCriterion( - queryVariable = QueryVariable(variableName = "thing"), - isAscending = true - ) - ), - whereClause = WhereClause( - patterns = Vector( - StatementPattern( - subj = QueryVariable(variableName = "thing"), - pred = IriRef( - iri = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".toSmartIri, - propertyPathOperator = None - ), - obj = IriRef( - iri = "http://www.knora.org/ontology/knora-base#Resource".toSmartIri, - propertyPathOperator = None - ), - namedGraph = None - ), - StatementPattern( - subj = QueryVariable(variableName = "thing"), - pred = IriRef( - iri = "http://www.knora.org/ontology/knora-base#isDeleted".toSmartIri, - propertyPathOperator = None - ), - obj = XsdLiteral( - value = "false", - datatype = "http://www.w3.org/2001/XMLSchema#boolean".toSmartIri - ), - namedGraph = Some(IriRef( - iri = "http://www.knora.org/explicit".toSmartIri, - propertyPathOperator = None - )) - ), - StatementPattern( - subj = QueryVariable(variableName = "thing"), - pred = IriRef( - iri = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".toSmartIri, - propertyPathOperator = None - ), - obj = IriRef( - iri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri, - propertyPathOperator = None - ), - namedGraph = None + offset = 0, + groupBy = Vector( + QueryVariable(variableName = "thing"), + QueryVariable(variableName = "date__valueHasStartJDN") + ), + orderBy = Vector( + OrderCriterion( + queryVariable = QueryVariable(variableName = "date__valueHasStartJDN"), + isAscending = false ), - OptionalPattern(patterns = Vector( + OrderCriterion( + queryVariable = QueryVariable(variableName = "thing"), + isAscending = true + ) + ), + whereClause = WhereClause( + patterns = Vector( StatementPattern( subj = QueryVariable(variableName = "thing"), pred = IriRef( - iri = "http://www.knora.org/ontology/0001/anything#hasDate".toSmartIri, + iri = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".toSmartIri, + propertyPathOperator = None + ), + obj = IriRef( + iri = "http://www.knora.org/ontology/knora-base#Resource".toSmartIri, propertyPathOperator = None ), - obj = QueryVariable(variableName = "date"), namedGraph = None ), StatementPattern( - subj = QueryVariable(variableName = "date"), + subj = QueryVariable(variableName = "thing"), pred = IriRef( iri = "http://www.knora.org/ontology/knora-base#isDeleted".toSmartIri, propertyPathOperator = None @@ -676,33 +639,70 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec )) ), StatementPattern( - subj = QueryVariable(variableName = "date"), + subj = QueryVariable(variableName = "thing"), pred = IriRef( - iri = "http://www.knora.org/ontology/knora-base#valueHasStartJDN".toSmartIri, + iri = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".toSmartIri, propertyPathOperator = None ), - obj = QueryVariable(variableName = "date__valueHasStartJDN"), - namedGraph = Some(IriRef( - iri = "http://www.knora.org/explicit".toSmartIri, + obj = IriRef( + iri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri, propertyPathOperator = None - )) + ), + namedGraph = None ), - FilterPattern(expression = CompareExpression( - leftArg = QueryVariable(variableName = "date__valueHasStartJDN"), - operator = CompareExpressionOperator.GREATER_THAN, - rightArg = XsdLiteral( - value = "2455928", - datatype = "http://www.w3.org/2001/XMLSchema#integer".toSmartIri - ) + OptionalPattern(patterns = Vector( + StatementPattern( + subj = QueryVariable(variableName = "thing"), + pred = IriRef( + iri = "http://www.knora.org/ontology/0001/anything#hasDate".toSmartIri, + propertyPathOperator = None + ), + obj = QueryVariable(variableName = "date"), + namedGraph = None + ), + StatementPattern( + subj = QueryVariable(variableName = "date"), + pred = IriRef( + iri = "http://www.knora.org/ontology/knora-base#isDeleted".toSmartIri, + propertyPathOperator = None + ), + obj = XsdLiteral( + value = "false", + datatype = "http://www.w3.org/2001/XMLSchema#boolean".toSmartIri + ), + namedGraph = Some(IriRef( + iri = "http://www.knora.org/explicit".toSmartIri, + propertyPathOperator = None + )) + ), + StatementPattern( + subj = QueryVariable(variableName = "date"), + pred = IriRef( + iri = "http://www.knora.org/ontology/knora-base#valueHasStartJDN".toSmartIri, + propertyPathOperator = None + ), + obj = QueryVariable(variableName = "date__valueHasStartJDN"), + namedGraph = Some(IriRef( + iri = "http://www.knora.org/explicit".toSmartIri, + propertyPathOperator = None + )) + ), + FilterPattern(expression = CompareExpression( + leftArg = QueryVariable(variableName = "date__valueHasStartJDN"), + operator = CompareExpressionOperator.GREATER_THAN, + rightArg = XsdLiteral( + value = "2455928", + datatype = "http://www.w3.org/2001/XMLSchema#integer".toSmartIri + ) + )) )) - )) + ), + positiveEntities = Set(), + querySchema = None ), - positiveEntities = Set(), - querySchema = None - ), - limit = Some(25), - useDistinct = true - ) + limit = Some(25), + useDistinct = true + ) val inputQueryWithDecimalOptionalSortCriterion: String = """ @@ -1028,47 +1028,84 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec useDistinct = true ) - val transformedQueryWithDecimalOptionalSortCriterionAndFilterComplex: SelectQuery = - SelectQuery( - variables = Vector( - QueryVariable(variableName = "thing"), - GroupConcat( - inputVariable = QueryVariable(variableName = "decimal"), - separator = StringFormatter.INFORMATION_SEPARATOR_ONE, - outputVariableName = "decimal__Concat", - ) - ), - offset = 0, - groupBy = Vector( - QueryVariable(variableName = "thing"), - QueryVariable(variableName = "decimal__valueHasDecimal") + val transformedQueryWithDecimalOptionalSortCriterionAndFilterComplex: SelectQuery = + SelectQuery( + variables = Vector( + QueryVariable(variableName = "thing"), + GroupConcat( + inputVariable = QueryVariable(variableName = "decimal"), + separator = StringFormatter.INFORMATION_SEPARATOR_ONE, + outputVariableName = "decimal__Concat", + ) + ), + offset = 0, + groupBy = Vector( + QueryVariable(variableName = "thing"), + QueryVariable(variableName = "decimal__valueHasDecimal") + ), + orderBy = Vector( + OrderCriterion( + queryVariable = QueryVariable(variableName = "decimal__valueHasDecimal"), + isAscending = true ), - orderBy = Vector( - OrderCriterion( - queryVariable = QueryVariable(variableName = "decimal__valueHasDecimal"), - isAscending = true + OrderCriterion( + queryVariable = QueryVariable(variableName = "thing"), + isAscending = true + ) + ), + whereClause = WhereClause( + patterns = Vector( + StatementPattern( + subj = QueryVariable(variableName = "thing"), + pred = IriRef( + iri = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".toSmartIri, + propertyPathOperator = None + ), + obj = IriRef( + iri = "http://www.knora.org/ontology/knora-base#Resource".toSmartIri, + propertyPathOperator = None + ), + namedGraph = None ), - OrderCriterion( - queryVariable = QueryVariable(variableName = "thing"), - isAscending = true - ) - ), - whereClause = WhereClause( - patterns = Vector( + StatementPattern( + subj = QueryVariable(variableName = "thing"), + pred = IriRef( + iri = "http://www.knora.org/ontology/knora-base#isDeleted".toSmartIri, + propertyPathOperator = None + ), + obj = XsdLiteral( + value = "false", + datatype = "http://www.w3.org/2001/XMLSchema#boolean".toSmartIri + ), + namedGraph = Some(IriRef( + iri = "http://www.knora.org/explicit".toSmartIri, + propertyPathOperator = None + )) + ), + StatementPattern( + subj = QueryVariable(variableName = "thing"), + pred = IriRef( + iri = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".toSmartIri, + propertyPathOperator = None + ), + obj = IriRef( + iri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri, + propertyPathOperator = None + ), + namedGraph = None + ), + OptionalPattern(patterns = Vector( StatementPattern( subj = QueryVariable(variableName = "thing"), pred = IriRef( - iri = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".toSmartIri, - propertyPathOperator = None - ), - obj = IriRef( - iri = "http://www.knora.org/ontology/knora-base#Resource".toSmartIri, + iri = "http://www.knora.org/ontology/0001/anything#hasDecimal".toSmartIri, propertyPathOperator = None ), + obj = QueryVariable(variableName = "decimal"), namedGraph = None ), StatementPattern( - subj = QueryVariable(variableName = "thing"), + subj = QueryVariable(variableName = "decimal"), pred = IriRef( iri = "http://www.knora.org/ontology/knora-base#isDeleted".toSmartIri, propertyPathOperator = None @@ -1083,79 +1120,343 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec )) ), StatementPattern( - subj = QueryVariable(variableName = "thing"), + subj = QueryVariable(variableName = "decimal"), pred = IriRef( - iri = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".toSmartIri, + iri = "http://www.knora.org/ontology/knora-base#valueHasDecimal".toSmartIri, propertyPathOperator = None ), - obj = IriRef( - iri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri, + obj = QueryVariable(variableName = "decimal__valueHasDecimal"), + namedGraph = Some(IriRef( + iri = "http://www.knora.org/explicit".toSmartIri, + propertyPathOperator = None + )) + ), + StatementPattern( + subj = QueryVariable(variableName = "decimal"), + pred = IriRef( + iri = "http://www.knora.org/ontology/knora-base#valueHasDecimal".toSmartIri, propertyPathOperator = None ), + obj = QueryVariable(variableName = "decimalVal"), namedGraph = None ), - OptionalPattern(patterns = Vector( - StatementPattern( - subj = QueryVariable(variableName = "thing"), - pred = IriRef( - iri = "http://www.knora.org/ontology/0001/anything#hasDecimal".toSmartIri, - propertyPathOperator = None - ), - obj = QueryVariable(variableName = "decimal"), - namedGraph = None - ), - StatementPattern( - subj = QueryVariable(variableName = "decimal"), - pred = IriRef( - iri = "http://www.knora.org/ontology/knora-base#isDeleted".toSmartIri, - propertyPathOperator = None - ), - obj = XsdLiteral( - value = "false", - datatype = "http://www.w3.org/2001/XMLSchema#boolean".toSmartIri - ), - namedGraph = Some(IriRef( - iri = "http://www.knora.org/explicit".toSmartIri, - propertyPathOperator = None - )) - ), - StatementPattern( - subj = QueryVariable(variableName = "decimal"), - pred = IriRef( - iri = "http://www.knora.org/ontology/knora-base#valueHasDecimal".toSmartIri, - propertyPathOperator = None - ), - obj = QueryVariable(variableName = "decimal__valueHasDecimal"), - namedGraph = Some(IriRef( - iri = "http://www.knora.org/explicit".toSmartIri, - propertyPathOperator = None - )) - ), - StatementPattern( - subj = QueryVariable(variableName = "decimal"), - pred = IriRef( - iri = "http://www.knora.org/ontology/knora-base#valueHasDecimal".toSmartIri, - propertyPathOperator = None - ), - obj = QueryVariable(variableName = "decimalVal"), - namedGraph = None - ), - FilterPattern(expression = CompareExpression( - leftArg = QueryVariable(variableName = "decimalVal"), - operator = CompareExpressionOperator.GREATER_THAN, - rightArg = XsdLiteral( - value = "2", - datatype = "http://www.w3.org/2001/XMLSchema#decimal".toSmartIri - ) - )) + FilterPattern(expression = CompareExpression( + leftArg = QueryVariable(variableName = "decimalVal"), + operator = CompareExpressionOperator.GREATER_THAN, + rightArg = XsdLiteral( + value = "2", + datatype = "http://www.w3.org/2001/XMLSchema#decimal".toSmartIri + ) )) + )) + ), + positiveEntities = Set(), + querySchema = None + ), + limit = Some(25), + useDistinct = true + ) + + val InputQueryWithRdfsLabelAndLiteralInSimpleSchema: String = + """ + |PREFIX incunabula: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?book knora-api:isMainResource true . + | + |} WHERE { + | ?book rdf:type incunabula:book . + | ?book rdfs:label "Zeitglöcklein des Lebens und Leidens Christi" . + |} + """.stripMargin + + val InputQueryWithRdfsLabelAndLiteralInComplexSchema: String = + """ + |PREFIX incunabula: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?book knora-api:isMainResource true . + | + |} WHERE { + | ?book rdf:type incunabula:book . + | ?book rdfs:label "Zeitglöcklein des Lebens und Leidens Christi" . + |} + """.stripMargin + + val TransformedQueryWithRdfsLabelAndLiteral: SelectQuery = SelectQuery( + variables = Vector(QueryVariable(variableName = "book")), + offset = 0, + groupBy = Vector(QueryVariable(variableName = "book")), + orderBy = Vector(OrderCriterion( + queryVariable = QueryVariable(variableName = "book"), + isAscending = true + )), + whereClause = WhereClause( + patterns = Vector( + StatementPattern( + subj = QueryVariable(variableName = "book"), + pred = IriRef( + iri = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".toSmartIri, + propertyPathOperator = None + ), + obj = IriRef( + iri = "http://www.knora.org/ontology/knora-base#Resource".toSmartIri, + propertyPathOperator = None + ), + namedGraph = None + ), + StatementPattern( + subj = QueryVariable(variableName = "book"), + pred = IriRef( + iri = "http://www.knora.org/ontology/knora-base#isDeleted".toSmartIri, + propertyPathOperator = None + ), + obj = XsdLiteral( + value = "false", + datatype = "http://www.w3.org/2001/XMLSchema#boolean".toSmartIri + ), + namedGraph = Some(IriRef( + iri = "http://www.knora.org/explicit".toSmartIri, + propertyPathOperator = None + )) + ), + StatementPattern( + subj = QueryVariable(variableName = "book"), + pred = IriRef( + iri = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".toSmartIri, + propertyPathOperator = None + ), + obj = IriRef( + iri = "http://www.knora.org/ontology/0803/incunabula#book".toSmartIri, + propertyPathOperator = None + ), + namedGraph = None + ), + StatementPattern( + subj = QueryVariable(variableName = "book"), + pred = IriRef( + iri = "http://www.w3.org/2000/01/rdf-schema#label".toSmartIri, + propertyPathOperator = None + ), + obj = XsdLiteral( + value = "Zeitgl\u00F6cklein des Lebens und Leidens Christi", + datatype = "http://www.w3.org/2001/XMLSchema#string".toSmartIri + ), + namedGraph = None + ) + ), + positiveEntities = Set(), + querySchema = None + ), + limit = Some(25), + useDistinct = true + ) + + val InputQueryWithRdfsLabelAndVariableInSimpleSchema: String = + """ + |PREFIX incunabula: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?book knora-api:isMainResource true . + | + |} WHERE { + | ?book rdf:type incunabula:book . + | ?book rdfs:label ?label . + | FILTER(?label = "Zeitglöcklein des Lebens und Leidens Christi") + |} + """.stripMargin + + val InputQueryWithRdfsLabelAndVariableInComplexSchema: String = + """ + |PREFIX incunabula: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?book knora-api:isMainResource true . + | + |} WHERE { + | ?book rdf:type incunabula:book . + | ?book rdfs:label ?label . + | FILTER(?label = "Zeitglöcklein des Lebens und Leidens Christi") + |} + """.stripMargin + + + val InputQueryWithRdfsLabelAndRegexInSimpleSchema: String = + """ + |PREFIX incunabula: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?book knora-api:isMainResource true . + | + |} WHERE { + | ?book rdf:type incunabula:book . + | ?book rdfs:label ?bookLabel . + | FILTER regex(?bookLabel, "Zeit", "i") + |}""".stripMargin + + val InputQueryWithRdfsLabelAndRegexInComplexSchema: String = + """ + |PREFIX incunabula: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?book knora-api:isMainResource true . + | + |} WHERE { + | ?book rdf:type incunabula:book . + | ?book rdfs:label ?bookLabel . + | FILTER regex(?bookLabel, "Zeit", "i") + |}""".stripMargin + + val TransformedQueryWithRdfsLabelAndVariable: SelectQuery = SelectQuery( + variables = Vector(QueryVariable(variableName = "book")), + offset = 0, + groupBy = Vector(QueryVariable(variableName = "book")), + orderBy = Vector(OrderCriterion( + queryVariable = QueryVariable(variableName = "book"), + isAscending = true + )), + whereClause = WhereClause( + patterns = Vector( + StatementPattern( + subj = QueryVariable(variableName = "book"), + pred = IriRef( + iri = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".toSmartIri, + propertyPathOperator = None + ), + obj = IriRef( + iri = "http://www.knora.org/ontology/knora-base#Resource".toSmartIri, + propertyPathOperator = None + ), + namedGraph = None + ), + StatementPattern( + subj = QueryVariable(variableName = "book"), + pred = IriRef( + iri = "http://www.knora.org/ontology/knora-base#isDeleted".toSmartIri, + propertyPathOperator = None + ), + obj = XsdLiteral( + value = "false", + datatype = "http://www.w3.org/2001/XMLSchema#boolean".toSmartIri + ), + namedGraph = Some(IriRef( + iri = "http://www.knora.org/explicit".toSmartIri, + propertyPathOperator = None + )) + ), + StatementPattern( + subj = QueryVariable(variableName = "book"), + pred = IriRef( + iri = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".toSmartIri, + propertyPathOperator = None + ), + obj = IriRef( + iri = "http://www.knora.org/ontology/0803/incunabula#book".toSmartIri, + propertyPathOperator = None + ), + namedGraph = None + ), + StatementPattern( + subj = QueryVariable(variableName = "book"), + pred = IriRef( + iri = "http://www.w3.org/2000/01/rdf-schema#label".toSmartIri, + propertyPathOperator = None + ), + obj = QueryVariable(variableName = "label"), + namedGraph = None + ), + FilterPattern(expression = CompareExpression( + leftArg = QueryVariable(variableName = "label"), + operator = CompareExpressionOperator.EQUALS, + rightArg = XsdLiteral( + value = "Zeitgl\u00F6cklein des Lebens und Leidens Christi", + datatype = "http://www.w3.org/2001/XMLSchema#string".toSmartIri + ) + )) + ), + positiveEntities = Set(), + querySchema = None + ), + limit = Some(25), + useDistinct = true + ) + + val TransformedQueryWithRdfsLabelAndRegex: SelectQuery = SelectQuery( + variables = Vector(QueryVariable(variableName = "book")), + offset = 0, + groupBy = Vector(QueryVariable(variableName = "book")), + orderBy = Vector(OrderCriterion( + queryVariable = QueryVariable(variableName = "book"), + isAscending = true + )), + whereClause = WhereClause( + patterns = Vector( + StatementPattern( + subj = QueryVariable(variableName = "book"), + pred = IriRef( + iri = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".toSmartIri, + propertyPathOperator = None + ), + obj = IriRef( + iri = "http://www.knora.org/ontology/knora-base#Resource".toSmartIri, + propertyPathOperator = None + ), + namedGraph = None + ), + StatementPattern( + subj = QueryVariable(variableName = "book"), + pred = IriRef( + iri = "http://www.knora.org/ontology/knora-base#isDeleted".toSmartIri, + propertyPathOperator = None + ), + obj = XsdLiteral( + value = "false", + datatype = "http://www.w3.org/2001/XMLSchema#boolean".toSmartIri + ), + namedGraph = Some(IriRef( + iri = "http://www.knora.org/explicit".toSmartIri, + propertyPathOperator = None + )) + ), + StatementPattern( + subj = QueryVariable(variableName = "book"), + pred = IriRef( + iri = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".toSmartIri, + propertyPathOperator = None + ), + obj = IriRef( + iri = "http://www.knora.org/ontology/0803/incunabula#book".toSmartIri, + propertyPathOperator = None + ), + namedGraph = None + ), + StatementPattern( + subj = QueryVariable(variableName = "book"), + pred = IriRef( + iri = "http://www.w3.org/2000/01/rdf-schema#label".toSmartIri, + propertyPathOperator = None ), - positiveEntities = Set(), - querySchema = None + obj = QueryVariable(variableName = "bookLabel"), + namedGraph = None ), - limit = Some(25), - useDistinct = true - ) + FilterPattern(expression = RegexFunction( + textExpr = QueryVariable(variableName = "bookLabel"), + pattern = "Zeit", + modifier = Some("i") + )) + ), + positiveEntities = Set(), + querySchema = None + ), + limit = Some(25), + useDistinct = true + ) "The NonTriplestoreSpecificGravsearchToPrequeryGenerator object" should { @@ -1253,5 +1554,41 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec assert(transformedQuery === transformedQueryWithDecimalOptionalSortCriterionAndFilterComplex) } + "transform an input query using rdfs:label and a literal in the simple schema" in { + val transformedQuery = QueryHandler.transformQuery(InputQueryWithRdfsLabelAndLiteralInSimpleSchema, responderData, settings) + + assert(transformedQuery === TransformedQueryWithRdfsLabelAndLiteral) + } + + "transform an input query using rdfs:label and a literal in the complex schema" in { + val transformedQuery = QueryHandler.transformQuery(InputQueryWithRdfsLabelAndLiteralInComplexSchema, responderData, settings) + + assert(transformedQuery === TransformedQueryWithRdfsLabelAndLiteral) + } + + "transform an input query using rdfs:label and a variable in the simple schema" in { + val transformedQuery = QueryHandler.transformQuery(InputQueryWithRdfsLabelAndVariableInSimpleSchema, responderData, settings) + + assert(transformedQuery === TransformedQueryWithRdfsLabelAndVariable) + } + + "transform an input query using rdfs:label and a variable in the complex schema" in { + val transformedQuery = QueryHandler.transformQuery(InputQueryWithRdfsLabelAndVariableInComplexSchema, responderData, settings) + + assert(transformedQuery === TransformedQueryWithRdfsLabelAndVariable) + } + + + "transform an input query using rdfs:label and a regex in the simple schema" in { + val transformedQuery = QueryHandler.transformQuery(InputQueryWithRdfsLabelAndRegexInSimpleSchema, responderData, settings) + + assert(transformedQuery === TransformedQueryWithRdfsLabelAndRegex) + } + + "transform an input query using rdfs:label and a regex in the complex schema" in { + val transformedQuery = QueryHandler.transformQuery(InputQueryWithRdfsLabelAndRegexInComplexSchema, responderData, settings) + + assert(transformedQuery === TransformedQueryWithRdfsLabelAndRegex) + } } } \ No newline at end of file diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/search/gravsearch/types/GravsearchTypeInspectorSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/search/gravsearch/types/GravsearchTypeInspectorSpec.scala index 0e48f77366..a0cd3b367d 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/search/gravsearch/types/GravsearchTypeInspectorSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/search/gravsearch/types/GravsearchTypeInspectorSpec.scala @@ -30,154 +30,15 @@ import scala.concurrent.duration._ import scala.concurrent.{Await, Future} /** - * Tests Gravsearch type inspection. - */ + * Tests Gravsearch type inspection. + */ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { - - private val searchParserV2Spec = new GravsearchParserSpec - private val anythingAdminUser = SharedTestDataADM.anythingAdminUser private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance private val timeout = 10.seconds - "The type inspection utility" should { - "remove the type annotations from a WHERE clause" in { - val parsedQuery = GravsearchParser.parseQuery(QueryWithExplicitTypeAnnotations) - val whereClauseWithoutAnnotations = GravsearchTypeInspectionUtil.removeTypeAnnotations(parsedQuery.whereClause) - whereClauseWithoutAnnotations should ===(whereClauseWithoutAnnotations) - } - } - - "The annotation-reading type inspector" should { - "get type information from a simple query" in { - val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = false) - val parsedQuery = GravsearchParser.parseQuery(QueryWithExplicitTypeAnnotations) - val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) - val result = Await.result(resultFuture, timeout) - assert(result == SimpleTypeInspectionResult) - } - } - - "The inferring type inspector" should { - "infer that an entity is a knora-api:Resource if there is an rdf:type statement about it and and the specified type is a Knora resource class" in { - val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) - val parsedQuery = GravsearchParser.parseQuery(QueryRdfTypeRule) - val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) - val result = Await.result(resultFuture, timeout) - assert(result == TypeInferenceResult1) - } - - "infer a property's knora-api:objectType if the property's IRI is used as a predicate" in { - val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) - val parsedQuery = GravsearchParser.parseQuery(QueryPropertyIriObjectTypeRule) - val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) - val result = Await.result(resultFuture, timeout) - assert(result == TypeInferenceResult1) - } - - "infer an entity's type if the entity is used as the object of a statement and the predicate's knora-api:objectType is known" in { - val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) - val parsedQuery = GravsearchParser.parseQuery(QueryTypeOfObjectFromPropertyRule) - val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) - val result = Await.result(resultFuture, timeout) - assert(result == TypeInferenceResult1) - } - - "infer the knora-api:objectType of a property variable if it's used with an object whose type is known" in { - val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) - val parsedQuery = GravsearchParser.parseQuery(QueryPropertyTypeFromObjectRule) - val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) - val result = Await.result(resultFuture, timeout) - assert(result == TypeInferenceResult1) - } - - "infer an entity's type if the entity is used as the subject of a statement, the predicate is an IRI, and the predicate's knora-api:subjectType is known" in { - val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) - val parsedQuery = GravsearchParser.parseQuery(QueryTypeOfSubjectFromPropertyRule) - val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) - val result = Await.result(resultFuture, timeout) - assert(result == TypeInferenceResult1) - } - - "infer the knora-api:objectType of a property variable if it's compared to a known property IRI in a FILTER" in { - val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) - val parsedQuery = GravsearchParser.parseQuery(QueryPropertyVarTypeFromFilterRule) - val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) - val result = Await.result(resultFuture, timeout) - assert(result == TypeInferenceResult2) - } - - "infer the type of a non-property variable if it's compared to an XSD literal in a FILTER" in { - val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) - val parsedQuery = GravsearchParser.parseQuery(QueryNonPropertyVarTypeFromFilterRule) - val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) - val result = Await.result(resultFuture, timeout) - assert(result == TypeInferenceResult4) - } - - "infer the type of a non-property variable used as the argument of a function in a FILTER" in { - val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) - val parsedQuery = GravsearchParser.parseQuery(QueryVarTypeFromFunction) - val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) - val result = Await.result(resultFuture, timeout) - assert(result == TypeInferenceResult5) - } - - "infer the type of a non-property IRI used as the argument of a function in a FILTER" in { - val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) - val parsedQuery = GravsearchParser.parseQuery(QueryIriTypeFromFunction) - val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) - val result = Await.result(resultFuture, timeout) - assert(result == TypeInferenceResult6) - } - - "infer the types in a query that requires 6 iterations" in { - val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) - val parsedQuery = GravsearchParser.parseQuery(PathologicalQuery) - val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) - val result = Await.result(resultFuture, timeout) - assert(result == PathologicalTypeInferenceResult) - } - - "reject a query with a non-Knora property whose type cannot be inferred" in { - val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) - val parsedQuery = GravsearchParser.parseQuery(QueryNonKnoraTypeWithoutAnnotation) - val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) - assertThrows[GravsearchException] { - Await.result(resultFuture, timeout) - } - } - - "accept a query with a non-Knora property whose type can be inferred" in { - val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) - val parsedQuery = GravsearchParser.parseQuery(QueryNonKnoraTypeWithAnnotation) - val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) - val result = Await.result(resultFuture, timeout) - assert(result == TypeInferenceResult3) - } - - "reject a query with inconsistent types inferred from statements" in { - val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) - val parsedQuery = GravsearchParser.parseQuery(QueryWithInconsistentTypes1) - val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) - assertThrows[GravsearchException] { - Await.result(resultFuture, timeout) - } - } - - "reject a query with inconsistent types inferred from a FILTER" in { - val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) - val parsedQuery = GravsearchParser.parseQuery(QueryWithInconsistentTypes2) - val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) - assertThrows[GravsearchException] { - Await.result(resultFuture, timeout) - } - } - } - - val QueryWithExplicitTypeAnnotations: String = """ |PREFIX beol: @@ -216,7 +77,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { |} """.stripMargin - val SimpleTypeInspectionResult = GravsearchTypeInspectionResult(entities = Map( + val SimpleTypeInspectionResult: GravsearchTypeInspectionResult = GravsearchTypeInspectionResult(entities = Map( TypeableIri(iri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#hasRecipient".toSmartIri) -> PropertyTypeInfo(objectTypeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri), TypeableVariable(variableName = "linkingProp1") -> PropertyTypeInfo(objectTypeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri), TypeableIri(iri = "http://rdfh.ch/beol/oU8fMNDJQ9SGblfBl5JamA".toSmartIri) -> NonPropertyTypeInfo(typeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri), @@ -226,7 +87,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { TypeableIri(iri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#hasAuthor".toSmartIri) -> PropertyTypeInfo(objectTypeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri) )) - val WhereClauseWithoutAnnotations = WhereClause(patterns = Vector( + val WhereClauseWithoutAnnotations: WhereClause = WhereClause(patterns = Vector( StatementPattern( obj = IriRef(iri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri), pred = IriRef(iri = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".toSmartIri), @@ -594,7 +455,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { |} """.stripMargin - val PathologicalTypeInferenceResult = GravsearchTypeInspectionResult(entities = Map( + val PathologicalTypeInferenceResult: GravsearchTypeInspectionResult = GravsearchTypeInspectionResult(entities = Map( TypeableVariable(variableName = "book4") -> NonPropertyTypeInfo(typeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri), TypeableVariable(variableName = "titleProp1") -> PropertyTypeInfo(objectTypeIri = "http://www.w3.org/2001/XMLSchema#string".toSmartIri), TypeableVariable(variableName = "page1") -> NonPropertyTypeInfo(typeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri), @@ -625,7 +486,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { )) - val TypeInferenceResult1 = GravsearchTypeInspectionResult(entities = Map( + val TypeInferenceResult1: GravsearchTypeInspectionResult = GravsearchTypeInspectionResult(entities = Map( TypeableIri(iri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#hasRecipient".toSmartIri) -> PropertyTypeInfo(objectTypeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri), TypeableVariable(variableName = "linkingProp1") -> PropertyTypeInfo(objectTypeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri), TypeableVariable(variableName = "date") -> NonPropertyTypeInfo(typeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Date".toSmartIri), @@ -637,7 +498,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { TypeableIri(iri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#hasAuthor".toSmartIri) -> PropertyTypeInfo(objectTypeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri) )) - val TypeInferenceResult2 = GravsearchTypeInspectionResult(entities = Map( + val TypeInferenceResult2: GravsearchTypeInspectionResult = GravsearchTypeInspectionResult(entities = Map( TypeableIri(iri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#hasRecipient".toSmartIri) -> PropertyTypeInfo(objectTypeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri), TypeableVariable(variableName = "linkingProp1") -> PropertyTypeInfo(objectTypeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri), TypeableVariable(variableName = "date") -> NonPropertyTypeInfo(typeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Date".toSmartIri), @@ -647,13 +508,13 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { TypeableIri(iri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#hasAuthor".toSmartIri) -> PropertyTypeInfo(objectTypeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri) )) - val TypeInferenceResult3 = GravsearchTypeInspectionResult(entities = Map( + val TypeInferenceResult3: GravsearchTypeInspectionResult = GravsearchTypeInspectionResult(entities = Map( TypeableVariable(variableName = "title") -> NonPropertyTypeInfo(typeIri = "http://www.w3.org/2001/XMLSchema#string".toSmartIri), TypeableIri(iri = "http://purl.org/dc/terms/title".toSmartIri) -> PropertyTypeInfo(objectTypeIri = "http://www.w3.org/2001/XMLSchema#string".toSmartIri), TypeableVariable(variableName = "book") -> NonPropertyTypeInfo(typeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri) )) - val TypeInferenceResult4 = GravsearchTypeInspectionResult(entities = Map( + val TypeInferenceResult4: GravsearchTypeInspectionResult = GravsearchTypeInspectionResult(entities = Map( TypeableIri(iri = "http://0.0.0.0:3333/ontology/0803/incunabula/simple/v2#title".toSmartIri) -> PropertyTypeInfo(objectTypeIri = "http://www.w3.org/2001/XMLSchema#string".toSmartIri), TypeableIri(iri = "http://0.0.0.0:3333/ontology/0803/incunabula/simple/v2#pubdate".toSmartIri) -> PropertyTypeInfo(objectTypeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Date".toSmartIri), TypeableVariable(variableName = "book") -> NonPropertyTypeInfo(typeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri), @@ -661,13 +522,13 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { TypeableVariable(variableName = "title") -> NonPropertyTypeInfo(typeIri = "http://www.w3.org/2001/XMLSchema#string".toSmartIri) )) - val TypeInferenceResult5 = GravsearchTypeInspectionResult(entities = Map( + val TypeInferenceResult5: GravsearchTypeInspectionResult = GravsearchTypeInspectionResult(entities = Map( TypeableVariable(variableName = "mainRes") -> NonPropertyTypeInfo(typeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri), TypeableVariable(variableName = "titleProp") -> PropertyTypeInfo(objectTypeIri = "http://www.w3.org/2001/XMLSchema#string".toSmartIri), TypeableVariable(variableName = "propVal0") -> NonPropertyTypeInfo(typeIri = "http://www.w3.org/2001/XMLSchema#string".toSmartIri) )) - val TypeInferenceResult6 = GravsearchTypeInspectionResult(entities = Map( + val TypeInferenceResult6: GravsearchTypeInspectionResult = GravsearchTypeInspectionResult(entities = Map( TypeableIri(iri = "http://0.0.0.0:3333/ontology/0801/beol/v2#hasText".toSmartIri) -> PropertyTypeInfo(objectTypeIri = "http://api.knora.org/ontology/knora-api/v2#TextValue".toSmartIri), TypeableVariable(variableName = "text") -> NonPropertyTypeInfo(typeIri = "http://api.knora.org/ontology/knora-api/v2#TextValue".toSmartIri), TypeableVariable(variableName = "letter") -> NonPropertyTypeInfo(typeIri = "http://api.knora.org/ontology/knora-api/v2#Resource".toSmartIri), @@ -675,4 +536,195 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { TypeableIri(iri = "http://api.knora.org/ontology/knora-api/v2#textValueHasStandoff".toSmartIri) -> PropertyTypeInfo(objectTypeIri = "http://api.knora.org/ontology/knora-api/v2#StandoffTag".toSmartIri), TypeableVariable(variableName = "standoffLinkTag") -> NonPropertyTypeInfo(typeIri = "http://api.knora.org/ontology/knora-api/v2#StandoffTag".toSmartIri) )) + + val QueryWithRdfsLabelAndLiteral: String = + """ + |PREFIX incunabula: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?book knora-api:isMainResource true . + | + |} WHERE { + | ?book rdf:type incunabula:book . + | ?book rdfs:label "Zeitglöcklein des Lebens und Leidens Christi" . + |} + """.stripMargin + + val QueryWithRdfsLabelAndVariable: String = + """ + |PREFIX incunabula: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?book knora-api:isMainResource true . + | + |} WHERE { + | ?book rdf:type incunabula:book . + | ?book rdfs:label ?label . + | FILTER(?label = "Zeitglöcklein des Lebens und Leidens Christi") + |} + """.stripMargin + + val RdfsLabelWithLiteralResult: GravsearchTypeInspectionResult = GravsearchTypeInspectionResult(entities = Map( + TypeableVariable(variableName = "book") -> NonPropertyTypeInfo(typeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri), + TypeableIri(iri = "http://www.w3.org/2000/01/rdf-schema#label".toSmartIri) -> PropertyTypeInfo(objectTypeIri = "http://www.w3.org/2001/XMLSchema#string".toSmartIri) + )) + + val RdfsLabelWithVariableResult: GravsearchTypeInspectionResult = GravsearchTypeInspectionResult(entities = Map( + TypeableVariable(variableName = "book") -> NonPropertyTypeInfo(typeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri), + TypeableIri(iri = "http://www.w3.org/2000/01/rdf-schema#label".toSmartIri) -> PropertyTypeInfo(objectTypeIri = "http://www.w3.org/2001/XMLSchema#string".toSmartIri), + TypeableVariable(variableName = "label") -> NonPropertyTypeInfo(typeIri = "http://www.w3.org/2001/XMLSchema#string".toSmartIri) + )) + + "The type inspection utility" should { + "remove the type annotations from a WHERE clause" in { + val parsedQuery = GravsearchParser.parseQuery(QueryWithExplicitTypeAnnotations) + val whereClauseWithoutAnnotations = GravsearchTypeInspectionUtil.removeTypeAnnotations(parsedQuery.whereClause) + whereClauseWithoutAnnotations should ===(whereClauseWithoutAnnotations) + } + } + + "The annotation-reading type inspector" should { + "get type information from a simple query" in { + val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = false) + val parsedQuery = GravsearchParser.parseQuery(QueryWithExplicitTypeAnnotations) + val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) + val result = Await.result(resultFuture, timeout) + assert(result == SimpleTypeInspectionResult) + } + } + + "The inferring type inspector" should { + "infer that an entity is a knora-api:Resource if there is an rdf:type statement about it and and the specified type is a Knora resource class" in { + val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) + val parsedQuery = GravsearchParser.parseQuery(QueryRdfTypeRule) + val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) + val result = Await.result(resultFuture, timeout) + assert(result == TypeInferenceResult1) + } + + "infer a property's knora-api:objectType if the property's IRI is used as a predicate" in { + val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) + val parsedQuery = GravsearchParser.parseQuery(QueryPropertyIriObjectTypeRule) + val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) + val result = Await.result(resultFuture, timeout) + assert(result == TypeInferenceResult1) + } + + "infer an entity's type if the entity is used as the object of a statement and the predicate's knora-api:objectType is known" in { + val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) + val parsedQuery = GravsearchParser.parseQuery(QueryTypeOfObjectFromPropertyRule) + val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) + val result = Await.result(resultFuture, timeout) + assert(result == TypeInferenceResult1) + } + + "infer the knora-api:objectType of a property variable if it's used with an object whose type is known" in { + val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) + val parsedQuery = GravsearchParser.parseQuery(QueryPropertyTypeFromObjectRule) + val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) + val result = Await.result(resultFuture, timeout) + assert(result == TypeInferenceResult1) + } + + "infer an entity's type if the entity is used as the subject of a statement, the predicate is an IRI, and the predicate's knora-api:subjectType is known" in { + val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) + val parsedQuery = GravsearchParser.parseQuery(QueryTypeOfSubjectFromPropertyRule) + val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) + val result = Await.result(resultFuture, timeout) + assert(result == TypeInferenceResult1) + } + + "infer the knora-api:objectType of a property variable if it's compared to a known property IRI in a FILTER" in { + val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) + val parsedQuery = GravsearchParser.parseQuery(QueryPropertyVarTypeFromFilterRule) + val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) + val result = Await.result(resultFuture, timeout) + assert(result == TypeInferenceResult2) + } + + "infer the type of a non-property variable if it's compared to an XSD literal in a FILTER" in { + val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) + val parsedQuery = GravsearchParser.parseQuery(QueryNonPropertyVarTypeFromFilterRule) + val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) + val result = Await.result(resultFuture, timeout) + assert(result == TypeInferenceResult4) + } + + "infer the type of a non-property variable used as the argument of a function in a FILTER" in { + val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) + val parsedQuery = GravsearchParser.parseQuery(QueryVarTypeFromFunction) + val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) + val result = Await.result(resultFuture, timeout) + assert(result == TypeInferenceResult5) + } + + "infer the type of a non-property IRI used as the argument of a function in a FILTER" in { + val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) + val parsedQuery = GravsearchParser.parseQuery(QueryIriTypeFromFunction) + val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) + val result = Await.result(resultFuture, timeout) + assert(result == TypeInferenceResult6) + } + + "infer the types in a query that requires 6 iterations" in { + val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) + val parsedQuery = GravsearchParser.parseQuery(PathologicalQuery) + val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) + val result = Await.result(resultFuture, timeout) + assert(result == PathologicalTypeInferenceResult) + } + + "know the object type of rdfs:label" in { + val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) + val parsedQuery = GravsearchParser.parseQuery(QueryWithRdfsLabelAndLiteral) + val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) + val result = Await.result(resultFuture, timeout) + assert(result == RdfsLabelWithLiteralResult) + } + + "infer the type of a variable used as the object of rdfs:label" in { + val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) + val parsedQuery = GravsearchParser.parseQuery(QueryWithRdfsLabelAndVariable) + val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) + val result = Await.result(resultFuture, timeout) + assert(result == RdfsLabelWithVariableResult) + } + + "reject a query with a non-Knora property whose type cannot be inferred" in { + val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) + val parsedQuery = GravsearchParser.parseQuery(QueryNonKnoraTypeWithoutAnnotation) + val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) + assertThrows[GravsearchException] { + Await.result(resultFuture, timeout) + } + } + + "accept a query with a non-Knora property whose type can be inferred" in { + val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) + val parsedQuery = GravsearchParser.parseQuery(QueryNonKnoraTypeWithAnnotation) + val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) + val result = Await.result(resultFuture, timeout) + assert(result == TypeInferenceResult3) + } + + "reject a query with inconsistent types inferred from statements" in { + val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) + val parsedQuery = GravsearchParser.parseQuery(QueryWithInconsistentTypes1) + val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) + assertThrows[GravsearchException] { + Await.result(resultFuture, timeout) + } + } + + "reject a query with inconsistent types inferred from a FILTER" in { + val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) + val parsedQuery = GravsearchParser.parseQuery(QueryWithInconsistentTypes2) + val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) + assertThrows[GravsearchException] { + Await.result(resultFuture, timeout) + } + } + } }