From 3dbb59c174ad9ad6b2ff003f2a93e8fa68fd3864 Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Wed, 19 Feb 2020 18:41:31 +0100 Subject: [PATCH 01/23] feat(api-v2): Refactor to use ForbiddenResource consistently (ongoing). --- .../resourcemessages/ResourceMessagesV2.scala | 3 +- .../knora/webapi/responders/Responder.scala | 12 ++- .../responders/v2/ResourcesResponderV2.scala | 6 ++ .../responders/v2/SearchResponderV2.scala | 80 +++---------------- .../responders/v2/StandoffResponderV2.scala | 3 + .../webapi/util/ConstructResponseUtilV2.scala | 27 +++++-- .../knora/webapi/util/StringFormatter.scala | 5 ++ 7 files changed, 59 insertions(+), 77 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index a2f99867ac..5a0318019f 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -35,7 +35,6 @@ import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.v2.responder._ import org.knora.webapi.messages.v2.responder.standoffmessages.MappingXMLtoStandoff import org.knora.webapi.messages.v2.responder.valuemessages._ -import org.knora.webapi.responders.v2.SearchResponderV2Constants import org.knora.webapi.util.IriConversions._ import org.knora.webapi.util.PermissionUtilADM.EntityPermission import org.knora.webapi.util._ @@ -907,7 +906,7 @@ case class ReadResourcesSequenceV2(numberOfResources: Int, resources: Seq[ReadRe val resourceInfo = resources.head - if (resourceInfo.resourceIri == SearchResponderV2Constants.forbiddenResourceIri) { // TODO: #953 + if (resourceInfo.resourceIri == StringFormatter.ForbiddenResourceIri) { // TODO: #1543 throw NotFoundException(s"Resource <$requestedResourceIri> does not exist, has been deleted, or you do not have permission to view it and/or the values of the specified property") } diff --git a/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala b/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala index bff24321de..048e2e3de1 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala @@ -25,8 +25,9 @@ import akka.pattern._ import akka.util.Timeout import com.typesafe.scalalogging.{LazyLogging, Logger} import org.knora.webapi.messages.store.triplestoremessages.{SparqlSelectRequest, SparqlSelectResponse} +import org.knora.webapi.messages.v2.responder.resourcemessages.{ReadResourceV2, ReadResourcesSequenceV2, ResourcesGetRequestV2} import org.knora.webapi.util.{SmartIri, StringFormatter} -import org.knora.webapi.{KnoraDispatchers, Settings, SettingsImpl, UnexpectedMessageException} +import org.knora.webapi.{ApiV2Complex, InconsistentTriplestoreDataException, KnoraDispatchers, KnoraSystemInstances, Settings, SettingsImpl, UnexpectedMessageException} import scala.concurrent.{ExecutionContext, Future} import scala.language.postfixOps @@ -109,6 +110,15 @@ abstract class Responder(responderData: ResponderData) extends LazyLogging { */ val log: Logger = logger + protected lazy val forbiddenResourceFuture: Future[ReadResourceV2] = { + for { + forbiddenResourceSeq: ReadResourcesSequenceV2 <- (responderManager ? ResourcesGetRequestV2( + resourceIris = Seq(StringFormatter.ForbiddenResourceIri), + targetSchema = ApiV2Complex, // This has no effect, because ForbiddenResource has no values. + requestingUser = KnoraSystemInstances.Users.SystemUser)).mapTo[ReadResourcesSequenceV2] + } yield forbiddenResourceSeq.resources.headOption.getOrElse(throw InconsistentTriplestoreDataException(s"${StringFormatter.ForbiddenResourceIri} was not returned")) + } + /** * Checks whether an entity is used in the triplestore. * diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index d1126d14da..a94246f683 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -1210,6 +1210,8 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt FastFuture.successful(Map.empty[IRI, MappingAndXSLTransformation]) } + forbiddenResource: ReadResourceV2 <- forbiddenResourceFuture + resourcesResponseFutures: Vector[Future[ReadResourceV2]] = resourceIrisDistinct.map { resIri: IRI => ConstructResponseUtilV2.createFullResourceResponse( @@ -1218,6 +1220,7 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt mappings = mappingsAsMap, queryStandoff = queryStandoff, versionDate = versionDate, + forbiddenResource = forbiddenResource, responderManager = responderManager, targetSchema = targetSchema, settings = settings, @@ -1263,6 +1266,8 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt requestingUser = requestingUser ) + forbiddenResource: ReadResourceV2 <- forbiddenResourceFuture + resourcesResponseFutures: Vector[Future[ReadResourceV2]] = resourceIrisDistinct.map { resIri: IRI => ConstructResponseUtilV2.createFullResourceResponse( @@ -1271,6 +1276,7 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt mappings = Map.empty[IRI, MappingAndXSLTransformation], queryStandoff = false, versionDate = None, + forbiddenResource = forbiddenResource, responderManager = responderManager, targetSchema = targetSchema, settings = settings, diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala index 67478326cb..ce34b89fdb 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala @@ -41,14 +41,6 @@ import org.knora.webapi.util.standoff.StandoffTagUtilV2 import scala.concurrent.Future -/** - * Constants used in [[SearchResponderV2]]. - */ -object SearchResponderV2Constants { - - val forbiddenResourceIri: IRI = s"http://${StringFormatter.IriDomain}/0000/forbiddenResource" -} - class SearchResponderV2(responderData: ResponderData) extends ResponderWithStandoffV2(responderData) { // A Gravsearch type inspection runner. @@ -68,24 +60,6 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand case other => handleUnexpectedMessage(other, log, this.getClass.getName) } - /** - * Gets the forbidden resource. - * - * @param requestingUser the user making the request. - * @return the forbidden resource. - */ - private def getForbiddenResource(requestingUser: UserADM): Future[Some[ReadResourceV2]] = { - import SearchResponderV2Constants.forbiddenResourceIri - - for { - forbiddenResSeq: ReadResourcesSequenceV2 <- (responderManager ? ResourcesGetRequestV2( - resourceIris = Seq(forbiddenResourceIri), - targetSchema = ApiV2Complex, // This has no effect, because ForbiddenResource has no values. - requestingUser = requestingUser)).mapTo[ReadResourcesSequenceV2] - forbiddenRes = forbiddenResSeq.resources.headOption.getOrElse(throw InconsistentTriplestoreDataException(s"$forbiddenResourceIri was not returned")) - } yield Some(forbiddenRes) - } - /** * Performs a fulltext search and returns the resources count (how many resources match the search criteria), * without taking into consideration permission checking. @@ -278,16 +252,6 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand Future(Map.empty[IRI, ConstructResponseUtilV2.ResourceWithValueRdfData]) } - // check if there are resources the user does not have sufficient permissions to see - forbiddenResourceOption: Option[ReadResourceV2] <- if (resourceIris.size > queryResultsSeparatedWithFullGraphPattern.size) { - // some of the main resources have been suppressed, represent them using the forbidden resource - - getForbiddenResource(requestingUser) - } else { - // all resources visible, no need for the forbidden resource - Future(None) - } - // Find out whether to query standoff along with text values. This boolean value will be passed to // ConstructResponseUtilV2.makeTextValueContentV2. queryStandoff = SchemaOptions.queryStandoffWithTextValues(targetSchema = targetSchema, schemaOptions = schemaOptions) @@ -301,12 +265,14 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand // _ = println(mappingsAsMap) + forbiddenResource: ReadResourceV2 <- forbiddenResourceFuture + resources: Vector[ReadResourceV2] <- ConstructResponseUtilV2.createSearchResponse( searchResults = queryResultsSeparatedWithFullGraphPattern, orderByResourceIri = resourceIris, mappings = mappingsAsMap, queryStandoff = queryStandoff, - forbiddenResource = forbiddenResourceOption, + forbiddenResource = forbiddenResource, responderManager = responderManager, settings = settings, targetSchema = targetSchema, @@ -558,16 +524,6 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand Future(Map.empty[IRI, ConstructResponseUtilV2.ResourceWithValueRdfData]) } - // check if there are resources the user does not have sufficient permissions to see - forbiddenResourceOption: Option[ReadResourceV2] <- if (mainResourceIris.size > queryResultsSeparatedWithFullGraphPattern.size) { - // some of the main resources have been suppressed, represent them using the forbidden resource - - getForbiddenResource(requestingUser) - } else { - // all resources visible, no need for the forbidden resource - Future(None) - } - // Find out whether to query standoff along with text values. This boolean value will be passed to // ConstructResponseUtilV2.makeTextValueContentV2. queryStandoff = SchemaOptions.queryStandoffWithTextValues(targetSchema = targetSchema, schemaOptions = schemaOptions) @@ -579,12 +535,14 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand FastFuture.successful(Map.empty[IRI, MappingAndXSLTransformation]) } + forbiddenResource: ReadResourceV2 <- forbiddenResourceFuture + resources <- ConstructResponseUtilV2.createSearchResponse( searchResults = queryResultsSeparatedWithFullGraphPattern, orderByResourceIri = mainResourceIris, mappings = mappingsAsMap, queryStandoff = queryStandoff, - forbiddenResource = forbiddenResourceOption, + forbiddenResource = forbiddenResource, responderManager = responderManager, settings = settings, targetSchema = targetSchema, @@ -706,15 +664,6 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand // separate resources and values queryResultsSeparated: RdfResources = ConstructResponseUtilV2.splitMainResourcesAndValueRdfData(constructQueryResults = resourceRequestResponse, requestingUser = resourcesInProjectGetRequestV2.requestingUser) - // check if there are resources the user does not have sufficient permissions to see - forbiddenResourceOption: Option[ReadResourceV2] <- if (mainResourceIris.size > queryResultsSeparated.size) { - // some of the main resources have been suppressed, represent them using the forbidden resource - getForbiddenResource(resourcesInProjectGetRequestV2.requestingUser) - } else { - // all resources visible, no need for the forbidden resource - Future(None) - } - // If we're querying standoff, get XML-to standoff mappings. mappings: Map[IRI, MappingAndXSLTransformation] <- if (queryStandoff) { getMappingsFromQueryResultsSeparated(queryResultsSeparated, resourcesInProjectGetRequestV2.requestingUser) @@ -722,13 +671,15 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand FastFuture.successful(Map.empty[IRI, MappingAndXSLTransformation]) } + forbiddenResource: ReadResourceV2 <- forbiddenResourceFuture + // Construct a ReadResourceV2 for each resource that the user has permission to see. searchResponse <- ConstructResponseUtilV2.createSearchResponse( searchResults = queryResultsSeparated, orderByResourceIri = mainResourceIris, mappings = mappings, queryStandoff = maybeStandoffMinStartIndex.nonEmpty, - forbiddenResource = forbiddenResourceOption, + forbiddenResource = forbiddenResource, responderManager = responderManager, targetSchema = resourcesInProjectGetRequestV2.targetSchema, settings = settings, @@ -850,22 +801,15 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand // separate resources and value objects queryResultsSeparated = ConstructResponseUtilV2.splitMainResourcesAndValueRdfData(constructQueryResults = searchResourceByLabelResponse, requestingUser = requestingUser) - // check if there are resources the user does not have sufficient permissions to see - forbiddenResourceOption: Option[ReadResourceV2] <- if (mainResourceIris.size > queryResultsSeparated.size) { - // some of the main resources have been suppressed, represent them using the forbidden resource - getForbiddenResource(requestingUser) - } else { - // all resources visible, no need for the forbidden resource - Future(None) - } - //_ = println(queryResultsSeparated) + forbiddenResource: ReadResourceV2 <- forbiddenResourceFuture + resources <- ConstructResponseUtilV2.createSearchResponse( searchResults = queryResultsSeparated, orderByResourceIri = mainResourceIris.toSeq.sorted, queryStandoff = false, - forbiddenResource = forbiddenResourceOption, + forbiddenResource = forbiddenResource, responderManager = responderManager, targetSchema = targetSchema, settings = settings, diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala index 93946813c9..a78e82ae62 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala @@ -108,12 +108,15 @@ class StandoffResponderV2(responderData: ResponderData) extends Responder(respon throw NotFoundException(s"Resource <${getStandoffRequestV2.resourceIri}> was not found (maybe you do not have permission to see it, or it is marked as deleted)") } + forbiddenResource: ReadResourceV2 <- forbiddenResourceFuture + readResourceV2: ReadResourceV2 <- ConstructResponseUtilV2.createFullResourceResponse( resourceIri = getStandoffRequestV2.resourceIri, resourceRdfData = queryResultsSeparated(getStandoffRequestV2.resourceIri), mappings = Map.empty, queryStandoff = false, versionDate = None, + forbiddenResource = forbiddenResource, responderManager = responderManager, targetSchema = getStandoffRequestV2.targetSchema, settings = settings, diff --git a/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala b/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala index 0ff5703bef..74253eabcb 100644 --- a/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala @@ -327,6 +327,10 @@ object ConstructResponseUtilV2 { case (_: IRI, statements: RdfWithUserPermission) => statements.maybeUserPermission.nonEmpty } + // Collect the IRIs of the resources that were filtered out because the user does not have permission + // to see them. + val resourceIrisNotVisible: Set[IRI] = resourceStatements.keySet -- resourceStatementsVisible.keySet + val flatResourcesWithValues: RdfResources = resourceStatementsVisible.map { case (resourceIri: IRI, resourceRdfWithUserPermission: RdfWithUserPermission) => val assertions: ConstructPredicateObjects = resourceRdfWithUserPermission.assertions @@ -554,7 +558,7 @@ object ConstructResponseUtilV2 { val transformedValuePropertyAssertions: RdfPropertyValues = resource.valuePropertyAssertions.map { case (propIri, values) => val transformedValues = values.map { - value => + value: ValueRdfData => if (value.valueObjectClass.toString == OntologyConstants.KnoraBase.LinkValue) { val dependentResourceIri: IRI = value.requireIriObject(OntologyConstants.Rdf.Object.toSmartIri) @@ -831,6 +835,7 @@ object ConstructResponseUtilV2 { * @param mappings the mappings needed for standoff conversions and XSL transformations. * @param queryStandoff if `true`, make separate queries to get the standoff for text values. * @param versionDate if defined, represents the requested time in the the resources' version history. + * @param forbiddenResource the ForbiddenResource. * @param responderManager the Knora responder manager. * @param targetSchema the schema of the response. * @param settings the application's settings. @@ -843,6 +848,7 @@ object ConstructResponseUtilV2 { mappings: Map[IRI, MappingAndXSLTransformation], queryStandoff: Boolean, versionDate: Option[Instant], + forbiddenResource: ReadResourceV2, responderManager: ActorRef, targetSchema: ApiV2Schema, settings: SettingsImpl, @@ -874,6 +880,7 @@ object ConstructResponseUtilV2 { mappings = mappings, queryStandoff = queryStandoff, versionDate = versionDate, + forbiddenResource = forbiddenResource, responderManager = responderManager, requestingUser = requestingUser, targetSchema = targetSchema, @@ -895,6 +902,7 @@ object ConstructResponseUtilV2 { * @param mappings the mappings needed for standoff conversions and XSL transformations. * @param queryStandoff if `true`, make separate queries to get the standoff for text values. * @param versionDate if defined, represents the requested time in the the resources' version history. + * @param forbiddenResource the ForbiddenResource. * @param responderManager the Knora responder manager. * @param targetSchema the schema of the response. * @param settings the application's settings. @@ -906,6 +914,7 @@ object ConstructResponseUtilV2 { mappings: Map[IRI, MappingAndXSLTransformation], queryStandoff: Boolean, versionDate: Option[Instant] = None, + forbiddenResource: ReadResourceV2, responderManager: ActorRef, targetSchema: ApiV2Schema, settings: SettingsImpl, @@ -1036,6 +1045,7 @@ object ConstructResponseUtilV2 { mappings = mappings, queryStandoff = queryStandoff, versionDate = versionDate, + forbiddenResource = forbiddenResource, responderManager = responderManager, requestingUser = requestingUser, targetSchema = targetSchema, @@ -1066,6 +1076,7 @@ object ConstructResponseUtilV2 { * @param mappings the mappings needed for standoff conversions and XSL transformations. * @param queryStandoff if `true`, make separate queries to get the standoff for text values. * @param versionDate if defined, represents the requested time in the the resources' version history. + * @param forbiddenResource the ForbiddenResource. * @param responderManager the Knora responder manager. * @param targetSchema the schema of the response. * @param settings the application's settings. @@ -1077,6 +1088,7 @@ object ConstructResponseUtilV2 { mappings: Map[IRI, MappingAndXSLTransformation], queryStandoff: Boolean, versionDate: Option[Instant], + forbiddenResource: ReadResourceV2, responderManager: ActorRef, targetSchema: ApiV2Schema, settings: SettingsImpl, @@ -1124,6 +1136,7 @@ object ConstructResponseUtilV2 { valueObject = valObj, mappings = mappings, queryStandoff = queryStandoff, + forbiddenResource = forbiddenResource, responderManager = responderManager, requestingUser = requestingUser, targetSchema = targetSchema, @@ -1215,6 +1228,7 @@ object ConstructResponseUtilV2 { * @param mappings the mappings needed for standoff conversions and XSL transformations. * @param queryStandoff if `true`, make separate queries to get the standoff for text values. * @param versionDate if defined, represents the requested time in the the resources' version history. + * @param forbiddenResource the ForbiddenResource. * @param responderManager the Knora responder manager. * @param targetSchema the schema of response. * @param settings the application's settings. @@ -1226,6 +1240,7 @@ object ConstructResponseUtilV2 { mappings: Map[IRI, MappingAndXSLTransformation], queryStandoff: Boolean, versionDate: Option[Instant], + forbiddenResource: ReadResourceV2, responderManager: ActorRef, targetSchema: ApiV2Schema, settings: SettingsImpl, @@ -1237,6 +1252,7 @@ object ConstructResponseUtilV2 { mappings = mappings, queryStandoff = queryStandoff, versionDate = versionDate, + forbiddenResource = forbiddenResource, responderManager = responderManager, requestingUser = requestingUser, targetSchema = targetSchema, @@ -1251,7 +1267,7 @@ object ConstructResponseUtilV2 { * @param orderByResourceIri the order in which the resources should be returned. * @param mappings the mappings to convert standoff to XML, if any. * @param queryStandoff if `true`, make separate queries to get the standoff for text values. - * @param forbiddenResource the ForbiddenResource, if any. + * @param forbiddenResource the ForbiddenResource. * @param responderManager the Knora responder manager. * @param targetSchema the schema of response. * @param settings the application's settings. @@ -1262,14 +1278,12 @@ object ConstructResponseUtilV2 { orderByResourceIri: Seq[IRI], mappings: Map[IRI, MappingAndXSLTransformation] = Map.empty[IRI, MappingAndXSLTransformation], queryStandoff: Boolean, - forbiddenResource: Option[ReadResourceV2], + forbiddenResource: ReadResourceV2, responderManager: ActorRef, targetSchema: ApiV2Schema, settings: SettingsImpl, requestingUser: UserADM)(implicit stringFormatter: StringFormatter, timeout: Timeout, executionContext: ExecutionContext): Future[Vector[ReadResourceV2]] = { - if (orderByResourceIri.toSet != searchResults.keySet && forbiddenResource.isEmpty) throw AssertionException(s"Not all resources are visible, but forbiddenResource is None") - // iterate over orderByResourceIris and construct the response in the correct order val readResourceFutures: Vector[Future[ReadResourceV2]] = orderByResourceIri.map { resourceIri: IRI => @@ -1286,6 +1300,7 @@ object ConstructResponseUtilV2 { mappings = mappings, queryStandoff = queryStandoff, versionDate = None, + forbiddenResource = forbiddenResource, responderManager = responderManager, targetSchema = targetSchema, settings = settings, @@ -1294,7 +1309,7 @@ object ConstructResponseUtilV2 { case None => // include the forbidden resource instead of skipping (the amount of results should be constant -> limit) - Future(forbiddenResource.getOrElse(throw AssertionException(s"Not all resources are visible, but forbiddenResource is None"))) + Future(forbiddenResource) } }.toVector diff --git a/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala b/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala index 835c81931a..bfcdc5cd95 100644 --- a/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala +++ b/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala @@ -176,6 +176,11 @@ object StringFormatter { */ val ClientCollectionEntityNameStart: String = "#" + ClientCollectionTypeKeyword + /** + * The IRI of the singleton instance of `ForbiddenResource`. + */ + val ForbiddenResourceIri: IRI = s"http://$IriDomain/0000/forbiddenResource" + /** * A container for an XML import namespace and its prefix label. * From 49f42149ce389b05332fa4b6bad21c9ba117217a Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Fri, 28 Feb 2020 09:59:49 +0100 Subject: [PATCH 02/23] test: Fix tests. --- .../knora/webapi/responders/Responder.scala | 2 +- .../knora/webapi/util/StringFormatter.scala | 29 -------------- .../scala/org/knora/webapi/CoreSpec.scala | 5 ++- .../v2/ResourcesResponderV2Spec.scala | 2 +- .../responders/v2/ValuesResponderV2Spec.scala | 2 +- .../util/ConstructResponseUtilV2Spec.scala | 39 ++++++++++++------- 6 files changed, 31 insertions(+), 48 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala b/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala index 048e2e3de1..73d3aabdbd 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala @@ -116,7 +116,7 @@ abstract class Responder(responderData: ResponderData) extends LazyLogging { resourceIris = Seq(StringFormatter.ForbiddenResourceIri), targetSchema = ApiV2Complex, // This has no effect, because ForbiddenResource has no values. requestingUser = KnoraSystemInstances.Users.SystemUser)).mapTo[ReadResourcesSequenceV2] - } yield forbiddenResourceSeq.resources.headOption.getOrElse(throw InconsistentTriplestoreDataException(s"${StringFormatter.ForbiddenResourceIri} was not returned")) + } yield forbiddenResourceSeq.toResource(StringFormatter.ForbiddenResourceIri) } /** diff --git a/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala b/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala index f095160b27..77c84b9a1c 100644 --- a/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala +++ b/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala @@ -3057,35 +3057,6 @@ class StringFormatter private(val maybeSettings: Option[SettingsImpl] = None, ma s"http://$IriDomain/permissions/$shortcode/$knoraPermissionUuid" } - /** - * Creates an IRI for a `knora-base:Map`. - * - * @param mapPath the map's path, which must be a sequence of names separated by slashes (`/`). Each name must - * be a valid XML [[https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-NCName NCName]]. - * @return the IRI of the map. - */ - def makeMapIri(mapPath: String): IRI = { - s"http://$IriDomain/maps/$mapPath" - } - - /** - * Extracts the path of a persistent map from the IRI of a `knora-base:Map`. - * - * @param mapIri the IRI of the `knora-base:Map`. - * @return the map's path. - */ - def mapIriToMapPath(mapIri: IRI): String = { - mapIri.stripPrefix(s"http://$IriDomain/maps/") - } - - /** - * Creates a random IRI for a `knora-base:MapEntry`. - */ - def makeRandomMapEntryIri: IRI = { - val mapEntryUuid = makeRandomBase64EncodedUuid - s"http://$IriDomain/map-entries/$mapEntryUuid" - } - /** * Converts a camel-case string like `FooBar` into a string like `foo-bar`. * diff --git a/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala b/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala index f33afcfab4..2d51bacdbd 100644 --- a/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala @@ -20,6 +20,7 @@ package org.knora.webapi import akka.actor.{ActorRef, ActorSystem, Props} +import akka.event.LoggingAdapter import akka.pattern.ask import akka.stream.ActorMaterializer import akka.testkit.{ImplicitSender, TestKit} @@ -35,7 +36,7 @@ import org.knora.webapi.util.{StartupUtils, StringFormatter} import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike} import scala.concurrent.duration._ -import scala.concurrent.{Await, ExecutionContext} +import scala.concurrent.{Await, ExecutionContext, Future} import scala.language.postfixOps object CoreSpec { @@ -79,7 +80,7 @@ abstract class CoreSpec(_system: ActorSystem) extends TestKit(_system) with Core // needs to be initialized early on StringFormatter.initForTest() - val log = akka.event.Logging(system, this.getClass) + val log: LoggingAdapter = akka.event.Logging(system, this.getClass) lazy val appActor: ActorRef = system.actorOf(Props(new ApplicationActor with LiveManagers), name = APPLICATION_MANAGER_ACTOR_NAME) diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala index 73023aef4c..8eb56df69d 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala @@ -488,7 +488,7 @@ class ResourcesResponderV2Spec extends CoreSpec() with ImplicitSender { val resourceInfo = readResourcesSequence.resources.head - if (resourceInfo.resourceIri == SearchResponderV2Constants.forbiddenResourceIri) { + if (resourceInfo.resourceIri == StringFormatter.ForbiddenResourceIri) { throw ForbiddenException(s"User ${requestingUser.email} does not have permission to view resource <${resourceInfo.resourceIri}>") } diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala index 84e087c759..e4ee785520 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala @@ -277,7 +277,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { val resourceInfo = readResourcesSequence.resources.head - if (resourceInfo.resourceIri == SearchResponderV2Constants.forbiddenResourceIri) { + if (resourceInfo.resourceIri == StringFormatter.ForbiddenResourceIri) { throw ForbiddenException(s"User ${requestingUser.email} does not have permission to view resource <${resourceInfo.resourceIri}>") } diff --git a/webapi/src/test/scala/org/knora/webapi/util/ConstructResponseUtilV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/util/ConstructResponseUtilV2Spec.scala index 62632ed5f3..d8fb0029f5 100644 --- a/webapi/src/test/scala/org/knora/webapi/util/ConstructResponseUtilV2Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/util/ConstructResponseUtilV2Spec.scala @@ -21,11 +21,12 @@ package org.knora.webapi.util import java.io.File +import akka.pattern.ask import akka.testkit.ImplicitSender import akka.util.Timeout import org.knora.webapi._ import org.knora.webapi.messages.store.triplestoremessages.SparqlExtendedConstructResponse -import org.knora.webapi.messages.v2.responder.resourcemessages.{ReadResourceV2, ReadResourcesSequenceV2} +import org.knora.webapi.messages.v2.responder.resourcemessages.{ReadResourceV2, ReadResourcesSequenceV2, ResourcesGetRequestV2} import org.knora.webapi.responders.v2.{ResourcesResponderV2SpecFullData, ResourcesResponseCheckerV2} import org.knora.webapi.util.ConstructResponseUtilV2.RdfResources @@ -33,8 +34,8 @@ import scala.concurrent.duration._ import scala.concurrent.{Await, Future} /** - * Tests [[ConstructResponseUtilV2]]. - */ + * Tests [[ConstructResponseUtilV2]]. + */ class ConstructResponseUtilV2Spec extends CoreSpec() with ImplicitSender { private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance private implicit val timeout: Timeout = 10.seconds @@ -49,17 +50,27 @@ class ConstructResponseUtilV2Spec extends CoreSpec() with ImplicitSender { val resourceRequestResponse: SparqlExtendedConstructResponse = SparqlExtendedConstructResponse.parseTurtleResponse(turtleStr, log).get val queryResultsSeparated: RdfResources = ConstructResponseUtilV2.splitMainResourcesAndValueRdfData(constructQueryResults = resourceRequestResponse, requestingUser = incunabulaUser) - val resourceFuture: Future[ReadResourceV2] = ConstructResponseUtilV2.createFullResourceResponse( - resourceIri = resourceIri, - resourceRdfData = queryResultsSeparated(resourceIri), - mappings = Map.empty, - queryStandoff = false, - versionDate = None, - responderManager = responderManager, - targetSchema = ApiV2Complex, - settings = settings, - requestingUser = incunabulaUser - ) + val resourceFuture: Future[ReadResourceV2] = for { + forbiddenResourceSeq: ReadResourcesSequenceV2 <- (responderManager ? ResourcesGetRequestV2( + resourceIris = Seq(StringFormatter.ForbiddenResourceIri), + targetSchema = ApiV2Complex, // This has no effect, because ForbiddenResource has no values. + requestingUser = KnoraSystemInstances.Users.SystemUser)).mapTo[ReadResourcesSequenceV2] + + forbiddenResource = forbiddenResourceSeq.toResource(StringFormatter.ForbiddenResourceIri) + + resourceResponse <- ConstructResponseUtilV2.createFullResourceResponse( + resourceIri = resourceIri, + resourceRdfData = queryResultsSeparated(resourceIri), + mappings = Map.empty, + queryStandoff = false, + versionDate = None, + forbiddenResource = forbiddenResource, + responderManager = responderManager, + targetSchema = ApiV2Complex, + settings = settings, + requestingUser = incunabulaUser + ) + } yield resourceResponse val resource: ReadResourceV2 = Await.result(resourceFuture, 10.seconds) val resourceSequence = ReadResourcesSequenceV2(numberOfResources = 1, resources = Seq(resource)) From 91717461edb6bee3a9d3cdec5ed9f993c605e00a Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Fri, 28 Feb 2020 10:17:26 +0100 Subject: [PATCH 03/23] feat(api-v2): Use ForbiddenResource consistently (ongoing). --- .../knora/webapi/responders/Responder.scala | 2 +- .../webapi/util/ConstructResponseUtilV2.scala | 532 +++++++++--------- 2 files changed, 281 insertions(+), 253 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala b/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala index 73d3aabdbd..9647d450dd 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala @@ -27,7 +27,7 @@ import com.typesafe.scalalogging.{LazyLogging, Logger} import org.knora.webapi.messages.store.triplestoremessages.{SparqlSelectRequest, SparqlSelectResponse} import org.knora.webapi.messages.v2.responder.resourcemessages.{ReadResourceV2, ReadResourcesSequenceV2, ResourcesGetRequestV2} import org.knora.webapi.util.{SmartIri, StringFormatter} -import org.knora.webapi.{ApiV2Complex, InconsistentTriplestoreDataException, KnoraDispatchers, KnoraSystemInstances, Settings, SettingsImpl, UnexpectedMessageException} +import org.knora.webapi._ import scala.concurrent.{ExecutionContext, Future} import scala.language.postfixOps diff --git a/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala b/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala index da3b627cf3..8841894673 100644 --- a/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala @@ -37,7 +37,7 @@ import org.knora.webapi.messages.v2.responder.resourcemessages._ import org.knora.webapi.messages.v2.responder.standoffmessages.{GetRemainingStandoffFromTextValueRequestV2, GetStandoffResponseV2, MappingXMLtoStandoff, StandoffTagV2} import org.knora.webapi.messages.v2.responder.valuemessages._ import org.knora.webapi.util.IriConversions._ -import org.knora.webapi.util.PermissionUtilADM.EntityPermission +import org.knora.webapi.util.PermissionUtilADM.{EntityPermission, ViewPermission} import org.knora.webapi.util.date.{CalendarNameV2, DatePrecisionV2} import org.knora.webapi.util.standoff.StandoffTagUtilV2 @@ -52,66 +52,66 @@ object ConstructResponseUtilV2 { ) /** - * A map of resource IRIs to resource RDF data. - */ + * A map of resource IRIs to resource RDF data. + */ type RdfResources = Map[IRI, ResourceWithValueRdfData] /** - * Makes an empty instance of [[RdfResources]]. - */ + * Makes an empty instance of [[RdfResources]]. + */ def emptyRdfResources: RdfResources = Map.empty /** - * A map of property IRIs to value RDF data. - */ + * A map of property IRIs to value RDF data. + */ type RdfPropertyValues = Map[SmartIri, Seq[ValueRdfData]] /** - * Makes an empty instance of [[RdfPropertyValues]]. - */ + * Makes an empty instance of [[RdfPropertyValues]]. + */ def emptyRdfPropertyValues: RdfPropertyValues = Map.empty /** - * A map of subject IRIs to [[ConstructPredicateObjects]] instances. - */ + * A map of subject IRIs to [[ConstructPredicateObjects]] instances. + */ type Statements = Map[IRI, ConstructPredicateObjects] /** - * A flattened map of predicates to objects. This assumes that each predicate has - * * only one object. - */ + * A flattened map of predicates to objects. This assumes that each predicate has + * * only one object. + */ type FlatPredicateObjects = Map[SmartIri, LiteralV2] /** - * A map of subject IRIs to flattened maps of predicates to objects. - */ + * A map of subject IRIs to flattened maps of predicates to objects. + */ type FlatStatements = Map[IRI, Map[SmartIri, LiteralV2]] /** - * Makes an empty instance of [[FlatStatements]]. - */ + * Makes an empty instance of [[FlatStatements]]. + */ def emptyFlatStatements: FlatStatements = Map.empty /** - * Represents assertions about an RDF subject. - */ + * Represents assertions about an RDF subject. + */ sealed trait RdfData { /** - * The IRI of the subject. - */ + * The IRI of the subject. + */ val subjectIri: IRI /** - * Assertions about the subject. - */ + * Assertions about the subject. + */ val assertions: FlatPredicateObjects /** - * Returns the optional string object of the specified predicate. Throws an exception if the object is not a string. - * - * @param predicateIri the predicate. - * @return the string object of the predicate. - */ + * Returns the optional string object of the specified predicate. Throws an exception if the object is not a string. + * + * @param predicateIri the predicate. + * @return the string object of the predicate. + */ def maybeStringObject(predicateIri: SmartIri): Option[String] = { assertions.get(predicateIri).map { literal => literal.asStringLiteral(throw InconsistentTriplestoreDataException(s"Unexpected object of $subjectIri $predicateIri: $literal")).value @@ -119,22 +119,22 @@ object ConstructResponseUtilV2 { } /** - * Returns the required string object of the specified predicate. Throws an exception if the object is not found or - * is not a string. - * - * @param predicateIri the predicate. - * @return the string object of the predicate. - */ + * Returns the required string object of the specified predicate. Throws an exception if the object is not found or + * is not a string. + * + * @param predicateIri the predicate. + * @return the string object of the predicate. + */ def requireStringObject(predicateIri: SmartIri): String = { maybeStringObject(predicateIri).getOrElse(throw InconsistentTriplestoreDataException(s"Subject $subjectIri does not have predicate $predicateIri")) } /** - * Returns the optional IRI object of the specified predicate. Throws an exception if the object is not an IRI. - * - * @param predicateIri the predicate. - * @return the IRI object of the predicate. - */ + * Returns the optional IRI object of the specified predicate. Throws an exception if the object is not an IRI. + * + * @param predicateIri the predicate. + * @return the IRI object of the predicate. + */ def maybeIriObject(predicateIri: SmartIri): Option[IRI] = { assertions.get(predicateIri).map { literal => literal.asIriLiteral(throw InconsistentTriplestoreDataException(s"Unexpected object of $subjectIri $predicateIri: $literal")).value @@ -142,22 +142,22 @@ object ConstructResponseUtilV2 { } /** - * Returns the required IRI object of the specified predicate. Throws an exception if the object is not found or - * is not an IRI. - * - * @param predicateIri the predicate. - * @return the IRI object of the predicate. - */ + * Returns the required IRI object of the specified predicate. Throws an exception if the object is not found or + * is not an IRI. + * + * @param predicateIri the predicate. + * @return the IRI object of the predicate. + */ def requireIriObject(predicateIri: SmartIri): IRI = { maybeIriObject(predicateIri).getOrElse(throw InconsistentTriplestoreDataException(s"Subject $subjectIri does not have predicate $predicateIri")) } /** - * Returns the optional integer object of the specified predicate. Throws an exception if the object is not an integer. - * - * @param predicateIri the predicate. - * @return the integer object of the predicate. - */ + * Returns the optional integer object of the specified predicate. Throws an exception if the object is not an integer. + * + * @param predicateIri the predicate. + * @return the integer object of the predicate. + */ def maybeIntObject(predicateIri: SmartIri): Option[Int] = { assertions.get(predicateIri).map { literal => literal.asIntLiteral(throw InconsistentTriplestoreDataException(s"Unexpected object of $subjectIri $predicateIri: $literal")).value @@ -165,22 +165,22 @@ object ConstructResponseUtilV2 { } /** - * Returns the required integer object of the specified predicate. Throws an exception if the object is not found or - * is not an integer. - * - * @param predicateIri the predicate. - * @return the integer object of the predicate. - */ + * Returns the required integer object of the specified predicate. Throws an exception if the object is not found or + * is not an integer. + * + * @param predicateIri the predicate. + * @return the integer object of the predicate. + */ def requireIntObject(predicateIri: SmartIri): Int = { maybeIntObject(predicateIri).getOrElse(throw InconsistentTriplestoreDataException(s"Subject $subjectIri does not have predicate $predicateIri")) } /** - * Returns the optional boolean object of the specified predicate. Throws an exception if the object is not a boolean. - * - * @param predicateIri the predicate. - * @return the boolean object of the predicate. - */ + * Returns the optional boolean object of the specified predicate. Throws an exception if the object is not a boolean. + * + * @param predicateIri the predicate. + * @return the boolean object of the predicate. + */ def maybeBooleanObject(predicateIri: SmartIri): Option[Boolean] = { assertions.get(predicateIri).map { literal => literal.asBooleanLiteral(throw InconsistentTriplestoreDataException(s"Unexpected object of $subjectIri $predicateIri: $literal")).value @@ -188,22 +188,22 @@ object ConstructResponseUtilV2 { } /** - * Returns the required boolean object of the specified predicate. Throws an exception if the object is not found or - * is not an boolean value. - * - * @param predicateIri the predicate. - * @return the boolean object of the predicate. - */ + * Returns the required boolean object of the specified predicate. Throws an exception if the object is not found or + * is not an boolean value. + * + * @param predicateIri the predicate. + * @return the boolean object of the predicate. + */ def requireBooleanObject(predicateIri: SmartIri): Boolean = { maybeBooleanObject(predicateIri).getOrElse(throw InconsistentTriplestoreDataException(s"Subject $subjectIri does not have predicate $predicateIri")) } /** - * Returns the optional decimal object of the specified predicate. Throws an exception if the object is not a decimal. - * - * @param predicateIri the predicate. - * @return the decimal object of the predicate. - */ + * Returns the optional decimal object of the specified predicate. Throws an exception if the object is not a decimal. + * + * @param predicateIri the predicate. + * @return the decimal object of the predicate. + */ def maybeDecimalObject(predicateIri: SmartIri): Option[BigDecimal] = { assertions.get(predicateIri).map { literal => literal.asDecimalLiteral(throw InconsistentTriplestoreDataException(s"Unexpected object of $subjectIri $predicateIri: $literal")).value @@ -211,23 +211,23 @@ object ConstructResponseUtilV2 { } /** - * Returns the required decimal object of the specified predicate. Throws an exception if the object is not found or - * is not an decimal value. - * - * @param predicateIri the predicate. - * @return the decimal object of the predicate. - */ + * Returns the required decimal object of the specified predicate. Throws an exception if the object is not found or + * is not an decimal value. + * + * @param predicateIri the predicate. + * @return the decimal object of the predicate. + */ def requireDecimalObject(predicateIri: SmartIri): BigDecimal = { maybeDecimalObject(predicateIri).getOrElse(throw InconsistentTriplestoreDataException(s"Subject $subjectIri does not have predicate $predicateIri")) } /** - * Returns the optional timestamp object of the specified predicate. Throws an exception if the object is not a timestamp. - * - * @param predicateIri the predicate. - * @return the timestamp object of the predicate. - */ + * Returns the optional timestamp object of the specified predicate. Throws an exception if the object is not a timestamp. + * + * @param predicateIri the predicate. + * @return the timestamp object of the predicate. + */ def maybeDateTimeObject(predicateIri: SmartIri): Option[Instant] = { assertions.get(predicateIri).map { literal => literal.asDateTimeLiteral(throw InconsistentTriplestoreDataException(s"Unexpected object of $subjectIri $predicateIri: $literal")).value @@ -235,28 +235,28 @@ object ConstructResponseUtilV2 { } /** - * Returns the required timestamp object of the specified predicate. Throws an exception if the object is not found or - * is not an timestamp value. - * - * @param predicateIri the predicate. - * @return the timestamp object of the predicate. - */ + * Returns the required timestamp object of the specified predicate. Throws an exception if the object is not found or + * is not an timestamp value. + * + * @param predicateIri the predicate. + * @return the timestamp object of the predicate. + */ def requireDateTimeObject(predicateIri: SmartIri): Instant = { maybeDateTimeObject(predicateIri).getOrElse(throw InconsistentTriplestoreDataException(s"Subject $subjectIri does not have predicate $predicateIri")) } } /** - * Represents the RDF data about a value, possibly including standoff. - * - * @param subjectIri the value object's IRI. - * @param valueObjectClass the type (class) of the value object. - * @param nestedResource the nested resource in case of a link value (either the source or the target of a link value, depending on [[isIncomingLink]]). - * @param isIncomingLink indicates if it is an incoming or outgoing link in case of a link value. - * @param userPermission the permission that the requesting user has on the value. - * @param assertions the value objects assertions. - * @param standoff standoff assertions, if any. - */ + * Represents the RDF data about a value, possibly including standoff. + * + * @param subjectIri the value object's IRI. + * @param valueObjectClass the type (class) of the value object. + * @param nestedResource the nested resource in case of a link value (either the source or the target of a link value, depending on [[isIncomingLink]]). + * @param isIncomingLink indicates if it is an incoming or outgoing link in case of a link value. + * @param userPermission the permission that the requesting user has on the value. + * @param assertions the value objects assertions. + * @param standoff standoff assertions, if any. + */ case class ValueRdfData(subjectIri: IRI, valueObjectClass: SmartIri, nestedResource: Option[ResourceWithValueRdfData] = None, @@ -266,14 +266,14 @@ object ConstructResponseUtilV2 { standoff: FlatStatements) extends RdfData /** - * Represents a resource and its values. - * - * @param subjectIri the resource IRI. - * @param assertions assertions about the resource (direct statements). - * @param isMainResource indicates if this represents a top level resource or a referred resource (depending on the query). - * @param userPermission the permission that the requesting user has on the resource. - * @param valuePropertyAssertions assertions about value properties. - */ + * Represents a resource and its values. + * + * @param subjectIri the resource IRI. + * @param assertions assertions about the resource (direct statements). + * @param isMainResource indicates if this represents a top level resource or a referred resource (depending on the query). + * @param userPermission the permission that the requesting user has on the resource. + * @param valuePropertyAssertions assertions about value properties. + */ case class ResourceWithValueRdfData(subjectIri: IRI, assertions: FlatPredicateObjects, isMainResource: Boolean, @@ -281,23 +281,35 @@ object ConstructResponseUtilV2 { valuePropertyAssertions: RdfPropertyValues) extends RdfData /** - * Represents a mapping including information about the standoff entities. - * May include a default XSL transformation. - * - * @param mapping the mapping from XML to standoff and vice versa. - * @param standoffEntities information about the standoff entities referred to in the mapping. - * @param XSLTransformation the default XSL transformation to convert the resulting XML (e.g., to HTML), if any. - */ + * A [[ResourceWithValueRdfData]] representing a placeholder for a dependent resource that the user doesn't + * have permission to see. It is replaced by `ForbiddenResource` during processing. + */ + val ForbiddenDependentResourcePlaceholder: ResourceWithValueRdfData = ResourceWithValueRdfData( + subjectIri = StringFormatter.ForbiddenResourceIri, + assertions = Map.empty, + isMainResource = false, + userPermission = ViewPermission, + valuePropertyAssertions = Map.empty + ) + + /** + * Represents a mapping including information about the standoff entities. + * May include a default XSL transformation. + * + * @param mapping the mapping from XML to standoff and vice versa. + * @param standoffEntities information about the standoff entities referred to in the mapping. + * @param XSLTransformation the default XSL transformation to convert the resulting XML (e.g., to HTML), if any. + */ case class MappingAndXSLTransformation(mapping: MappingXMLtoStandoff, standoffEntities: StandoffEntityInfoGetResponseV2, XSLTransformation: Option[String]) /** - * A [[SparqlConstructResponse]] may contain both resources and value RDF data objects as well as standoff. - * This method turns a graph (i.e. triples) into a structure organized by the principle of resources and their values, i.e. a map of resource Iris to [[ResourceWithValueRdfData]]. - * The resource Iris represent main resources, dependent resources are contained in the link values as nested structures. - * - * @param constructQueryResults the results of a SPARQL construct query representing resources and their values. - * @return a Map[resource IRI -> [[ResourceWithValueRdfData]]]. - */ + * A [[SparqlConstructResponse]] may contain both resources and value RDF data objects as well as standoff. + * This method turns a graph (i.e. triples) into a structure organized by the principle of resources and their values, i.e. a map of resource Iris to [[ResourceWithValueRdfData]]. + * The resource Iris represent main resources, dependent resources are contained in the link values as nested structures. + * + * @param constructQueryResults the results of a SPARQL construct query representing resources and their values. + * @return a Map[resource IRI -> [[ResourceWithValueRdfData]]]. + */ def splitMainResourcesAndValueRdfData(constructQueryResults: SparqlExtendedConstructResponse, requestingUser: UserADM)(implicit stringFormatter: StringFormatter): RdfResources = { // An intermediate data structure containing RDF assertions about an entity and the user's permission on the entity. @@ -545,13 +557,13 @@ object ConstructResponseUtilV2 { } /** - * Given a resource IRI, finds any link values in the resource, and recursively embeds the target resource in each link value. - * - * @param resourceIri the IRI of the resource to start with. - * @param alreadyTraversed a set (initially empty) of the IRIs of resources that this function has already - * traversed, to prevent an infinite loop if a cycle is encountered. - * @return the same resource, with any nested resources attached to it. - */ + * Given a resource IRI, finds any link values in the resource, and recursively embeds the target resource in each link value. + * + * @param resourceIri the IRI of the resource to start with. + * @param alreadyTraversed a set (initially empty) of the IRIs of resources that this function has already + * traversed, to prevent an infinite loop if a cycle is encountered. + * @return the same resource, with any nested resources attached to it. + */ def nestResources(resourceIri: IRI, alreadyTraversed: Set[IRI] = Set.empty[IRI]): ResourceWithValueRdfData = { val resource = flatResourcesWithValues(resourceIri) @@ -565,16 +577,23 @@ object ConstructResponseUtilV2 { if (alreadyTraversed(dependentResourceIri)) { value } else { - // If we don't have the dependent resource, that means that the user doesn't have - // permission to see it, or it's been marked as deleted. Just return the link - // value without a nested resource. + // Do we have the dependent resource? if (flatResourcesWithValues.contains(dependentResourceIri)) { + // Yes. Nest it in the link value. val dependentResource: ResourceWithValueRdfData = nestResources(dependentResourceIri, alreadyTraversed + resourceIri) value.copy( nestedResource = Some(dependentResource) ) + } else if (resourceIrisNotVisible.contains(dependentResourceIri)) { + // No, because the user doesn't have permission to see it. Nest a placeholder for + // ForbiddenResource in the link value. + value.copy( + nestedResource = Some(ForbiddenDependentResourcePlaceholder) + ) } else { + // We don't have the dependent resource because it is marked as deleted. Just + // return the link value without a nested resource. value } } @@ -661,11 +680,11 @@ object ConstructResponseUtilV2 { } /** - * Collect all mapping Iris referred to in the given value assertions. - * - * @param valuePropertyAssertions the given assertions (property -> value object). - * @return a set of mapping Iris. - */ + * Collect all mapping Iris referred to in the given value assertions. + * + * @param valuePropertyAssertions the given assertions (property -> value object). + * @return a set of mapping Iris. + */ def getMappingIrisFromValuePropertyAssertions(valuePropertyAssertions: RdfPropertyValues)(implicit stringFormatter: StringFormatter): Set[IRI] = { valuePropertyAssertions.foldLeft(Set.empty[IRI]) { case (acc: Set[IRI], (_: SmartIri, valObjs: Seq[ValueRdfData])) => @@ -693,19 +712,19 @@ object ConstructResponseUtilV2 { } /** - * Given a [[ValueRdfData]], constructs a [[TextValueContentV2]]. This method is used to process a text value - * as returned in an API response, as well as to process a page of standoff markup that is being queried - * separately from its text value. - * - * @param valueObject the given [[ValueRdfData]]. - * @param valueObjectValueHasString the value's `knora-base:valueHasString`. - * @param valueCommentOption the value's comment, if any. - * @param mappings the mappings needed for standoff conversions and XSL transformations. - * @param queryStandoff if `true`, make separate queries to get the standoff for the text value. - * @param responderManager the Knora responder manager. - * @param requestingUser the user making the request. - * @return a [[TextValueContentV2]]. - */ + * Given a [[ValueRdfData]], constructs a [[TextValueContentV2]]. This method is used to process a text value + * as returned in an API response, as well as to process a page of standoff markup that is being queried + * separately from its text value. + * + * @param valueObject the given [[ValueRdfData]]. + * @param valueObjectValueHasString the value's `knora-base:valueHasString`. + * @param valueCommentOption the value's comment, if any. + * @param mappings the mappings needed for standoff conversions and XSL transformations. + * @param queryStandoff if `true`, make separate queries to get the standoff for the text value. + * @param responderManager the Knora responder manager. + * @param requestingUser the user making the request. + * @return a [[TextValueContentV2]]. + */ private def makeTextValueContentV2(resourceIri: IRI, valueObject: ValueRdfData, valueObjectValueHasString: Option[String], @@ -772,17 +791,17 @@ object ConstructResponseUtilV2 { } /** - * Given a [[ValueRdfData]], constructs a [[FileValueContentV2]]. - * - * @param valueType the IRI of the file value type - * @param valueObject the given [[ValueRdfData]]. - * @param valueObjectValueHasString the value's `knora-base:valueHasString`. - * @param valueCommentOption the value's comment, if any. - * @param mappings the mappings needed for standoff conversions and XSL transformations. - * @param responderManager the Knora responder manager. - * @param requestingUser the user making the request. - * @return a [[FileValueContentV2]]. - */ + * Given a [[ValueRdfData]], constructs a [[FileValueContentV2]]. + * + * @param valueType the IRI of the file value type + * @param valueObject the given [[ValueRdfData]]. + * @param valueObjectValueHasString the value's `knora-base:valueHasString`. + * @param valueCommentOption the value's comment, if any. + * @param mappings the mappings needed for standoff conversions and XSL transformations. + * @param responderManager the Knora responder manager. + * @param requestingUser the user making the request. + * @return a [[FileValueContentV2]]. + */ private def makeFileValueContentV2(valueType: IRI, valueObject: ValueRdfData, valueObjectValueHasString: String, @@ -827,21 +846,21 @@ object ConstructResponseUtilV2 { } /** - * Given a [[ValueRdfData]], constructs a [[LinkValueContentV2]]. - * - * @param valueObject the given [[ValueRdfData]]. - * @param valueObjectValueHasString the value's `knora-base:valueHasString`. - * @param valueCommentOption the value's comment, if any. - * @param mappings the mappings needed for standoff conversions and XSL transformations. - * @param queryStandoff if `true`, make separate queries to get the standoff for text values. - * @param versionDate if defined, represents the requested time in the the resources' version history. - * @param forbiddenResource the ForbiddenResource. - * @param responderManager the Knora responder manager. - * @param targetSchema the schema of the response. - * @param settings the application's settings. - * @param requestingUser the user making the request. - * @return a [[LinkValueContentV2]]. - */ + * Given a [[ValueRdfData]], constructs a [[LinkValueContentV2]]. + * + * @param valueObject the given [[ValueRdfData]]. + * @param valueObjectValueHasString the value's `knora-base:valueHasString`. + * @param valueCommentOption the value's comment, if any. + * @param mappings the mappings needed for standoff conversions and XSL transformations. + * @param queryStandoff if `true`, make separate queries to get the standoff for text values. + * @param versionDate if defined, represents the requested time in the the resources' version history. + * @param forbiddenResource the `ForbiddenResource`. + * @param responderManager the Knora responder manager. + * @param targetSchema the schema of the response. + * @param settings the application's settings. + * @param requestingUser the user making the request. + * @return a [[LinkValueContentV2]]. + */ private def makeLinkValueContentV2(valueObject: ValueRdfData, valueObjectValueHasString: String, valueCommentOption: Option[String], @@ -867,48 +886,57 @@ object ConstructResponseUtilV2 { comment = valueCommentOption ) + // Is there a nested resource in the link value? valueObject.nestedResource match { - case Some(nestedResourceAssertions: ResourceWithValueRdfData) => - - // add information about the referred resource - - for { - nestedResource <- constructReadResourceV2( - resourceIri = referredResourceIri, - resourceWithValueRdfData = nestedResourceAssertions, - mappings = mappings, - queryStandoff = queryStandoff, - versionDate = versionDate, - forbiddenResource = forbiddenResource, - responderManager = responderManager, - requestingUser = requestingUser, - targetSchema = targetSchema, - settings = settings + // Yes. Is the nested resource a placeholder for the forbidden resource? + if (nestedResourceAssertions.subjectIri == StringFormatter.ForbiddenResourceIri) { + // Yes. Replace it with the forbidden resource. + Future { + linkValue.copy( + nestedResource = Some(forbiddenResource) + ) + } + } else { + // No. Construct a ReadResourceV2 representing the nested resource. + for { + nestedResource <- constructReadResourceV2( + resourceIri = referredResourceIri, + resourceWithValueRdfData = nestedResourceAssertions, + mappings = mappings, + queryStandoff = queryStandoff, + versionDate = versionDate, + forbiddenResource = forbiddenResource, + responderManager = responderManager, + requestingUser = requestingUser, + targetSchema = targetSchema, + settings = settings + ) + } yield linkValue.copy( + nestedResource = Some(nestedResource) ) - } yield linkValue.copy( - nestedResource = Some(nestedResource) // construct a `ReadResourceV2` - ) - + } - case None => FastFuture.successful(linkValue) // do not include information about the referred resource + case None => + // There is no nested resource. + FastFuture.successful(linkValue) } } /** - * Given a [[ValueRdfData]], constructs a [[ValueContentV2]], considering the specific type of the given [[ValueRdfData]]. - * - * @param valueObject the given [[ValueRdfData]]. - * @param mappings the mappings needed for standoff conversions and XSL transformations. - * @param queryStandoff if `true`, make separate queries to get the standoff for text values. - * @param versionDate if defined, represents the requested time in the the resources' version history. - * @param forbiddenResource the ForbiddenResource. - * @param responderManager the Knora responder manager. - * @param targetSchema the schema of the response. - * @param settings the application's settings. - * @param requestingUser the user making the request. - * @return a [[ValueContentV2]] representing a value. - */ + * Given a [[ValueRdfData]], constructs a [[ValueContentV2]], considering the specific type of the given [[ValueRdfData]]. + * + * @param valueObject the given [[ValueRdfData]]. + * @param mappings the mappings needed for standoff conversions and XSL transformations. + * @param queryStandoff if `true`, make separate queries to get the standoff for text values. + * @param versionDate if defined, represents the requested time in the the resources' version history. + * @param forbiddenResource the ForbiddenResource. + * @param responderManager the Knora responder manager. + * @param targetSchema the schema of the response. + * @param settings the application's settings. + * @param requestingUser the user making the request. + * @return a [[ValueContentV2]] representing a value. + */ private def createValueContentV2FromValueRdfData(resourceIri: IRI, valueObject: ValueRdfData, mappings: Map[IRI, MappingAndXSLTransformation], @@ -1075,21 +1103,21 @@ object ConstructResponseUtilV2 { } /** - * - * Creates a [[ReadResourceV2]] from a [[ResourceWithValueRdfData]]. - * - * @param resourceIri the IRI of the resource. - * @param resourceWithValueRdfData the Rdf data belonging to the resource. - * @param mappings the mappings needed for standoff conversions and XSL transformations. - * @param queryStandoff if `true`, make separate queries to get the standoff for text values. - * @param versionDate if defined, represents the requested time in the the resources' version history. - * @param forbiddenResource the ForbiddenResource. - * @param responderManager the Knora responder manager. - * @param targetSchema the schema of the response. - * @param settings the application's settings. - * @param requestingUser the user making the request. - * @return a [[ReadResourceV2]]. - */ + * + * Creates a [[ReadResourceV2]] from a [[ResourceWithValueRdfData]]. + * + * @param resourceIri the IRI of the resource. + * @param resourceWithValueRdfData the Rdf data belonging to the resource. + * @param mappings the mappings needed for standoff conversions and XSL transformations. + * @param queryStandoff if `true`, make separate queries to get the standoff for text values. + * @param versionDate if defined, represents the requested time in the the resources' version history. + * @param forbiddenResource the ForbiddenResource. + * @param responderManager the Knora responder manager. + * @param targetSchema the schema of the response. + * @param settings the application's settings. + * @param requestingUser the user making the request. + * @return a [[ReadResourceV2]]. + */ private def constructReadResourceV2(resourceIri: IRI, resourceWithValueRdfData: ResourceWithValueRdfData, mappings: Map[IRI, MappingAndXSLTransformation], @@ -1228,20 +1256,20 @@ object ConstructResponseUtilV2 { } /** - * Creates a response to a full resource request. - * - * @param resourceIri the IRI of the requested resource. - * @param resourceRdfData the results returned by the triplestore. - * @param mappings the mappings needed for standoff conversions and XSL transformations. - * @param queryStandoff if `true`, make separate queries to get the standoff for text values. - * @param versionDate if defined, represents the requested time in the the resources' version history. - * @param forbiddenResource the ForbiddenResource. - * @param responderManager the Knora responder manager. - * @param targetSchema the schema of response. - * @param settings the application's settings. - * @param requestingUser the user making the request. - * @return a [[ReadResourceV2]]. - */ + * Creates a response to a full resource request. + * + * @param resourceIri the IRI of the requested resource. + * @param resourceRdfData the results returned by the triplestore. + * @param mappings the mappings needed for standoff conversions and XSL transformations. + * @param queryStandoff if `true`, make separate queries to get the standoff for text values. + * @param versionDate if defined, represents the requested time in the the resources' version history. + * @param forbiddenResource the ForbiddenResource. + * @param responderManager the Knora responder manager. + * @param targetSchema the schema of response. + * @param settings the application's settings. + * @param requestingUser the user making the request. + * @return a [[ReadResourceV2]]. + */ def createFullResourceResponse(resourceIri: IRI, resourceRdfData: ResourceWithValueRdfData, mappings: Map[IRI, MappingAndXSLTransformation], @@ -1268,19 +1296,19 @@ object ConstructResponseUtilV2 { } /** - * Creates a response to a fulltext or extended search. - * - * @param searchResults the resources that matched the query and the client has permissions to see. - * @param orderByResourceIri the order in which the resources should be returned. - * @param mappings the mappings to convert standoff to XML, if any. - * @param queryStandoff if `true`, make separate queries to get the standoff for text values. - * @param forbiddenResource the ForbiddenResource. - * @param responderManager the Knora responder manager. - * @param targetSchema the schema of response. - * @param settings the application's settings. - * @param requestingUser the user making the request. - * @return a collection of [[ReadResourceV2]] representing the search results. - */ + * Creates a response to a fulltext or extended search. + * + * @param searchResults the resources that matched the query and the client has permissions to see. + * @param orderByResourceIri the order in which the resources should be returned. + * @param mappings the mappings to convert standoff to XML, if any. + * @param queryStandoff if `true`, make separate queries to get the standoff for text values. + * @param forbiddenResource the ForbiddenResource. + * @param responderManager the Knora responder manager. + * @param targetSchema the schema of response. + * @param settings the application's settings. + * @param requestingUser the user making the request. + * @return a collection of [[ReadResourceV2]] representing the search results. + */ def createSearchResponse(searchResults: RdfResources, orderByResourceIri: Seq[IRI], mappings: Map[IRI, MappingAndXSLTransformation] = Map.empty[IRI, MappingAndXSLTransformation], From cae954dcc0a660285e6918f54393bddd95147c29 Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Fri, 28 Feb 2020 14:19:42 +0100 Subject: [PATCH 04/23] refactor(api-v2): Move ForbiddenResource from triplestore to app. --- webapi/_test_data/all_data/system-data.ttl | 23 -------- .../org/knora/webapi/OntologyConstants.scala | 54 ++++++++++--------- .../knora/webapi/responders/Responder.scala | 14 +---- .../responders/v2/ResourcesResponderV2.scala | 14 +++-- .../responders/v2/SearchResponderV2.scala | 12 ----- .../responders/v2/StandoffResponderV2.scala | 3 -- .../webapi/util/ConstructResponseUtilV2.scala | 19 +------ .../knora/webapi/util/StringFormatter.scala | 19 +++++++ .../searchR2RV2/ThingWithHiddenThing.jsonld | 26 ++++++++- ...ingsWithOptionalDecimalGreaterThan1.jsonld | 16 +++--- ...searchResponseWithforbiddenResource.jsonld | 4 +- .../thingWithOptionalDateSortedDesc.jsonld | 12 ++--- .../util/ConstructResponseUtilV2Spec.scala | 8 --- 13 files changed, 97 insertions(+), 127 deletions(-) diff --git a/webapi/_test_data/all_data/system-data.ttl b/webapi/_test_data/all_data/system-data.ttl index a187f0fe21..61f260bbda 100644 --- a/webapi/_test_data/all_data/system-data.ttl +++ b/webapi/_test_data/all_data/system-data.ttl @@ -39,26 +39,3 @@ knora-base:listNodePosition 2 ; rdfs:label "Maybe"@en ; rdfs:label "Vielleicht"@de . - - -########################################################## -# -# Sweet and tasty but forbidden resource -# -# but of the tree of the knowledge of good and evil, thou shalt not eat of it -# -########################################################## - - a knora-base:ForbiddenResource ; - - rdfs:label "This resource is a proxy for a resource you are not allowed to see (may depend on the context: query path)" ; - - knora-base:isDeleted false ; - - knora-base:attachedToUser ; - - knora-base:attachedToProject knora-admin:SystemProject ; - - knora-base:creationDate "2017-10-06T11:05:37Z"^^xsd:dateTime ; - - knora-base:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|V knora-admin:UnknownUser" . diff --git a/webapi/src/main/scala/org/knora/webapi/OntologyConstants.scala b/webapi/src/main/scala/org/knora/webapi/OntologyConstants.scala index 4092cbc966..ca8e1c7f42 100644 --- a/webapi/src/main/scala/org/knora/webapi/OntologyConstants.scala +++ b/webapi/src/main/scala/org/knora/webapi/OntologyConstants.scala @@ -22,8 +22,8 @@ package org.knora.webapi import org.knora.webapi.util.SmartIri /** - * Contains string constants for IRIs from ontologies used by the application. - */ + * Contains string constants for IRIs from ontologies used by the application. + */ object OntologyConstants { object Rdf { @@ -71,9 +71,9 @@ object OntologyConstants { val OnDatatype: IRI = OwlPrefixExpansion + "onDatatype" /** - * Cardinality IRIs expressed as OWL restrictions, which specify the properties that resources of - * a particular type can have. - */ + * Cardinality IRIs expressed as OWL restrictions, which specify the properties that resources of + * a particular type can have. + */ val cardinalityOWLRestrictions: Set[IRI] = Set( Cardinality, MinCardinality, @@ -83,8 +83,8 @@ object OntologyConstants { val NamedIndividual: IRI = OwlPrefixExpansion + "NamedIndividual" /** - * Classes defined by OWL that can be used as knora-base:subjectClassConstraint or knora-base:objectClassConstraint. - */ + * Classes defined by OWL that can be used as knora-base:subjectClassConstraint or knora-base:objectClassConstraint. + */ val ClassesThatCanBeKnoraClassConstraints: Set[IRI] = Set( Ontology, Class, @@ -120,8 +120,8 @@ object OntologyConstants { } /** - * http://schema.org - */ + * http://schema.org + */ object SchemaOrg { val SchemaOrgPrefixExpansion: IRI = "http://schema.org/" val Name: IRI = SchemaOrgPrefixExpansion + "name" @@ -134,8 +134,8 @@ object OntologyConstants { } /** - * Ontology labels that are reserved for built-in ontologies. - */ + * Ontology labels that are reserved for built-in ontologies. + */ val BuiltInOntologyLabels: Set[String] = Set( KnoraBase.KnoraBaseOntologyLabel, KnoraAdmin.KnoraAdminOntologyLabel, @@ -166,6 +166,8 @@ object OntologyConstants { val StillImageRepresentation: IRI = KnoraBasePrefixExpansion + "StillImageRepresentation" val TextRepresentation: IRI = KnoraBasePrefixExpansion + "TextRepresentation" + val ForbiddenResource: IRI = KnoraBasePrefixExpansion + "ForbiddenResource" + val XMLToStandoffMapping: IRI = KnoraBasePrefixExpansion + "XMLToStandoffMapping" val HasMappingElement: IRI = KnoraBasePrefixExpansion + "hasMappingElement" val MappingElement: IRI = KnoraBasePrefixExpansion + "MappingElement" @@ -528,14 +530,14 @@ object OntologyConstants { val DefaultSharedOntologiesProject: IRI = KnoraAdminPrefixExpansion + "DefaultSharedOntologiesProject" /** - * The system user is the owner of objects that are created by the system, rather than directly by the user, - * such as link values for standoff resource references. - */ + * The system user is the owner of objects that are created by the system, rather than directly by the user, + * such as link values for standoff resource references. + */ val SystemUser: IRI = KnoraAdminPrefixExpansion + "SystemUser" /** - * Every user not logged-in is per default an anonymous user. - */ + * Every user not logged-in is per default an anonymous user. + */ val AnonymousUser: IRI = KnoraAdminPrefixExpansion + "AnonymousUser" } @@ -752,17 +754,17 @@ object OntologyConstants { val KnoraApiPrefix: String = KnoraApiOntologyLabel + ":" /** - * Returns `true` if the specified IRI is `knora-api:Resource` in Knora API v2, in the simple - * or complex schema. - */ + * Returns `true` if the specified IRI is `knora-api:Resource` in Knora API v2, in the simple + * or complex schema. + */ def isKnoraApiV2Resource(iri: SmartIri): Boolean = { val iriStr = iri.toString iriStr == OntologyConstants.KnoraApiV2Simple.Resource || iriStr == OntologyConstants.KnoraApiV2Complex.Resource } /** - * Returns the IRI of `knora-api:subjectType` in the specified schema. - */ + * Returns the IRI of `knora-api:subjectType` in the specified schema. + */ def getSubjectTypePredicate(apiV2Schema: ApiV2Schema): IRI = { apiV2Schema match { case ApiV2Simple => KnoraApiV2Simple.SubjectType @@ -771,8 +773,8 @@ object OntologyConstants { } /** - * Returns the IRI of `knora-api:objectType` in the specified schema. - */ + * Returns the IRI of `knora-api:objectType` in the specified schema. + */ def getObjectTypePredicate(apiV2Schema: ApiV2Schema): IRI = { apiV2Schema match { case ApiV2Simple => KnoraApiV2Simple.ObjectType @@ -1125,9 +1127,9 @@ object OntologyConstants { } /** - * A map of IRIs in each possible source schema to the corresponding IRIs in each possible target schema, for the - * cases where this can't be done formally by [[org.knora.webapi.util.SmartIri]]. - */ + * A map of IRIs in each possible source schema to the corresponding IRIs in each possible target schema, for the + * cases where this can't be done formally by [[org.knora.webapi.util.SmartIri]]. + */ val CorrespondingIris: Map[(OntologySchema, OntologySchema), Map[IRI, IRI]] = Map( (InternalSchema, ApiV2Simple) -> Map( // All the values of this map must be either properties or datatypes. PropertyInfoContentV2.toOntologySchema diff --git a/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala b/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala index 9647d450dd..9f0a03adbd 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala @@ -24,10 +24,9 @@ import akka.http.scaladsl.util.FastFuture import akka.pattern._ import akka.util.Timeout import com.typesafe.scalalogging.{LazyLogging, Logger} +import org.knora.webapi._ import org.knora.webapi.messages.store.triplestoremessages.{SparqlSelectRequest, SparqlSelectResponse} -import org.knora.webapi.messages.v2.responder.resourcemessages.{ReadResourceV2, ReadResourcesSequenceV2, ResourcesGetRequestV2} import org.knora.webapi.util.{SmartIri, StringFormatter} -import org.knora.webapi._ import scala.concurrent.{ExecutionContext, Future} import scala.language.postfixOps @@ -108,16 +107,7 @@ abstract class Responder(responderData: ResponderData) extends LazyLogging { /** * Provides logging */ - val log: Logger = logger - - protected lazy val forbiddenResourceFuture: Future[ReadResourceV2] = { - for { - forbiddenResourceSeq: ReadResourcesSequenceV2 <- (responderManager ? ResourcesGetRequestV2( - resourceIris = Seq(StringFormatter.ForbiddenResourceIri), - targetSchema = ApiV2Complex, // This has no effect, because ForbiddenResource has no values. - requestingUser = KnoraSystemInstances.Users.SystemUser)).mapTo[ReadResourcesSequenceV2] - } yield forbiddenResourceSeq.toResource(StringFormatter.ForbiddenResourceIri) - } + protected val log: Logger = logger /** * Checks whether an entity is used in the triplestore. diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index 7aad4d9331..7dbb16a276 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -1122,7 +1122,6 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt versionDate: Option[Instant], queryStandoff: Boolean, requestingUser: UserADM): Future[RdfResources] = { - // eliminate duplicate Iris val resourceIrisDistinct: Seq[IRI] = resourceIris.distinct @@ -1134,6 +1133,12 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt ) for { + _ <- Future { + if (resourceIrisDistinct.toSet.contains(StringFormatter.ForbiddenResourceIri)) { + throw BadRequestException(s"<${StringFormatter.ForbiddenResourceIri}> cannot be requested") + } + } + resourceRequestSparql <- Future(queries.sparql.v2.txt.getResourcePropertiesAndValues( triplestore = settings.triplestoreType, resourceIris = resourceIrisDistinct, @@ -1183,7 +1188,6 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt targetSchema: ApiV2Schema, schemaOptions: Set[SchemaOption], requestingUser: UserADM): Future[ReadResourcesSequenceV2] = { - // eliminate duplicate Iris val resourceIrisDistinct: Seq[IRI] = resourceIris.distinct @@ -1210,8 +1214,6 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt FastFuture.successful(Map.empty[IRI, MappingAndXSLTransformation]) } - forbiddenResource: ReadResourceV2 <- forbiddenResourceFuture - resourcesResponseFutures: Vector[Future[ReadResourceV2]] = resourceIrisDistinct.map { resIri: IRI => ConstructResponseUtilV2.createFullResourceResponse( @@ -1220,7 +1222,6 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt mappings = mappingsAsMap, queryStandoff = queryStandoff, versionDate = versionDate, - forbiddenResource = forbiddenResource, responderManager = responderManager, targetSchema = targetSchema, settings = settings, @@ -1266,8 +1267,6 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt requestingUser = requestingUser ) - forbiddenResource: ReadResourceV2 <- forbiddenResourceFuture - resourcesResponseFutures: Vector[Future[ReadResourceV2]] = resourceIrisDistinct.map { resIri: IRI => ConstructResponseUtilV2.createFullResourceResponse( @@ -1276,7 +1275,6 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt mappings = Map.empty[IRI, MappingAndXSLTransformation], queryStandoff = false, versionDate = None, - forbiddenResource = forbiddenResource, responderManager = responderManager, targetSchema = targetSchema, settings = settings, diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala index ce34b89fdb..ab7a046945 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala @@ -265,14 +265,11 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand // _ = println(mappingsAsMap) - forbiddenResource: ReadResourceV2 <- forbiddenResourceFuture - resources: Vector[ReadResourceV2] <- ConstructResponseUtilV2.createSearchResponse( searchResults = queryResultsSeparatedWithFullGraphPattern, orderByResourceIri = resourceIris, mappings = mappingsAsMap, queryStandoff = queryStandoff, - forbiddenResource = forbiddenResource, responderManager = responderManager, settings = settings, targetSchema = targetSchema, @@ -535,14 +532,11 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand FastFuture.successful(Map.empty[IRI, MappingAndXSLTransformation]) } - forbiddenResource: ReadResourceV2 <- forbiddenResourceFuture - resources <- ConstructResponseUtilV2.createSearchResponse( searchResults = queryResultsSeparatedWithFullGraphPattern, orderByResourceIri = mainResourceIris, mappings = mappingsAsMap, queryStandoff = queryStandoff, - forbiddenResource = forbiddenResource, responderManager = responderManager, settings = settings, targetSchema = targetSchema, @@ -671,15 +665,12 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand FastFuture.successful(Map.empty[IRI, MappingAndXSLTransformation]) } - forbiddenResource: ReadResourceV2 <- forbiddenResourceFuture - // Construct a ReadResourceV2 for each resource that the user has permission to see. searchResponse <- ConstructResponseUtilV2.createSearchResponse( searchResults = queryResultsSeparated, orderByResourceIri = mainResourceIris, mappings = mappings, queryStandoff = maybeStandoffMinStartIndex.nonEmpty, - forbiddenResource = forbiddenResource, responderManager = responderManager, targetSchema = resourcesInProjectGetRequestV2.targetSchema, settings = settings, @@ -803,13 +794,10 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand //_ = println(queryResultsSeparated) - forbiddenResource: ReadResourceV2 <- forbiddenResourceFuture - resources <- ConstructResponseUtilV2.createSearchResponse( searchResults = queryResultsSeparated, orderByResourceIri = mainResourceIris.toSeq.sorted, queryStandoff = false, - forbiddenResource = forbiddenResource, responderManager = responderManager, targetSchema = targetSchema, settings = settings, diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala index 0ab1efdcde..6b623f8022 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala @@ -108,15 +108,12 @@ class StandoffResponderV2(responderData: ResponderData) extends Responder(respon throw NotFoundException(s"Resource <${getStandoffRequestV2.resourceIri}> was not found (maybe you do not have permission to see it, or it is marked as deleted)") } - forbiddenResource: ReadResourceV2 <- forbiddenResourceFuture - readResourceV2: ReadResourceV2 <- ConstructResponseUtilV2.createFullResourceResponse( resourceIri = getStandoffRequestV2.resourceIri, resourceRdfData = queryResultsSeparated(getStandoffRequestV2.resourceIri), mappings = Map.empty, queryStandoff = false, versionDate = None, - forbiddenResource = forbiddenResource, responderManager = responderManager, targetSchema = getStandoffRequestV2.targetSchema, settings = settings, diff --git a/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala b/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala index 8841894673..1e90dfafaf 100644 --- a/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala @@ -854,7 +854,6 @@ object ConstructResponseUtilV2 { * @param mappings the mappings needed for standoff conversions and XSL transformations. * @param queryStandoff if `true`, make separate queries to get the standoff for text values. * @param versionDate if defined, represents the requested time in the the resources' version history. - * @param forbiddenResource the `ForbiddenResource`. * @param responderManager the Knora responder manager. * @param targetSchema the schema of the response. * @param settings the application's settings. @@ -867,7 +866,6 @@ object ConstructResponseUtilV2 { mappings: Map[IRI, MappingAndXSLTransformation], queryStandoff: Boolean, versionDate: Option[Instant], - forbiddenResource: ReadResourceV2, responderManager: ActorRef, targetSchema: ApiV2Schema, settings: SettingsImpl, @@ -894,7 +892,7 @@ object ConstructResponseUtilV2 { // Yes. Replace it with the forbidden resource. Future { linkValue.copy( - nestedResource = Some(forbiddenResource) + nestedResource = Some(stringFormatter.forbiddenResource) ) } } else { @@ -906,7 +904,6 @@ object ConstructResponseUtilV2 { mappings = mappings, queryStandoff = queryStandoff, versionDate = versionDate, - forbiddenResource = forbiddenResource, responderManager = responderManager, requestingUser = requestingUser, targetSchema = targetSchema, @@ -930,7 +927,6 @@ object ConstructResponseUtilV2 { * @param mappings the mappings needed for standoff conversions and XSL transformations. * @param queryStandoff if `true`, make separate queries to get the standoff for text values. * @param versionDate if defined, represents the requested time in the the resources' version history. - * @param forbiddenResource the ForbiddenResource. * @param responderManager the Knora responder manager. * @param targetSchema the schema of the response. * @param settings the application's settings. @@ -942,7 +938,6 @@ object ConstructResponseUtilV2 { mappings: Map[IRI, MappingAndXSLTransformation], queryStandoff: Boolean, versionDate: Option[Instant] = None, - forbiddenResource: ReadResourceV2, responderManager: ActorRef, targetSchema: ApiV2Schema, settings: SettingsImpl, @@ -1080,7 +1075,6 @@ object ConstructResponseUtilV2 { mappings = mappings, queryStandoff = queryStandoff, versionDate = versionDate, - forbiddenResource = forbiddenResource, responderManager = responderManager, requestingUser = requestingUser, targetSchema = targetSchema, @@ -1111,7 +1105,6 @@ object ConstructResponseUtilV2 { * @param mappings the mappings needed for standoff conversions and XSL transformations. * @param queryStandoff if `true`, make separate queries to get the standoff for text values. * @param versionDate if defined, represents the requested time in the the resources' version history. - * @param forbiddenResource the ForbiddenResource. * @param responderManager the Knora responder manager. * @param targetSchema the schema of the response. * @param settings the application's settings. @@ -1123,7 +1116,6 @@ object ConstructResponseUtilV2 { mappings: Map[IRI, MappingAndXSLTransformation], queryStandoff: Boolean, versionDate: Option[Instant], - forbiddenResource: ReadResourceV2, responderManager: ActorRef, targetSchema: ApiV2Schema, settings: SettingsImpl, @@ -1171,7 +1163,6 @@ object ConstructResponseUtilV2 { valueObject = valObj, mappings = mappings, queryStandoff = queryStandoff, - forbiddenResource = forbiddenResource, responderManager = responderManager, requestingUser = requestingUser, targetSchema = targetSchema, @@ -1263,7 +1254,6 @@ object ConstructResponseUtilV2 { * @param mappings the mappings needed for standoff conversions and XSL transformations. * @param queryStandoff if `true`, make separate queries to get the standoff for text values. * @param versionDate if defined, represents the requested time in the the resources' version history. - * @param forbiddenResource the ForbiddenResource. * @param responderManager the Knora responder manager. * @param targetSchema the schema of response. * @param settings the application's settings. @@ -1275,7 +1265,6 @@ object ConstructResponseUtilV2 { mappings: Map[IRI, MappingAndXSLTransformation], queryStandoff: Boolean, versionDate: Option[Instant], - forbiddenResource: ReadResourceV2, responderManager: ActorRef, targetSchema: ApiV2Schema, settings: SettingsImpl, @@ -1287,7 +1276,6 @@ object ConstructResponseUtilV2 { mappings = mappings, queryStandoff = queryStandoff, versionDate = versionDate, - forbiddenResource = forbiddenResource, responderManager = responderManager, requestingUser = requestingUser, targetSchema = targetSchema, @@ -1302,7 +1290,6 @@ object ConstructResponseUtilV2 { * @param orderByResourceIri the order in which the resources should be returned. * @param mappings the mappings to convert standoff to XML, if any. * @param queryStandoff if `true`, make separate queries to get the standoff for text values. - * @param forbiddenResource the ForbiddenResource. * @param responderManager the Knora responder manager. * @param targetSchema the schema of response. * @param settings the application's settings. @@ -1313,7 +1300,6 @@ object ConstructResponseUtilV2 { orderByResourceIri: Seq[IRI], mappings: Map[IRI, MappingAndXSLTransformation] = Map.empty[IRI, MappingAndXSLTransformation], queryStandoff: Boolean, - forbiddenResource: ReadResourceV2, responderManager: ActorRef, targetSchema: ApiV2Schema, settings: SettingsImpl, @@ -1335,7 +1321,6 @@ object ConstructResponseUtilV2 { mappings = mappings, queryStandoff = queryStandoff, versionDate = None, - forbiddenResource = forbiddenResource, responderManager = responderManager, targetSchema = targetSchema, settings = settings, @@ -1344,7 +1329,7 @@ object ConstructResponseUtilV2 { case None => // include the forbidden resource instead of skipping (the amount of results should be constant -> limit) - Future(forbiddenResource) + Future(stringFormatter.forbiddenResource) } }.toVector diff --git a/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala b/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala index 77c84b9a1c..9a1099917f 100644 --- a/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala +++ b/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala @@ -41,6 +41,7 @@ import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM import org.knora.webapi.messages.store.triplestoremessages.{SparqlAskRequest, SparqlAskResponse} import org.knora.webapi.messages.v1.responder.projectmessages.ProjectInfoV1 import org.knora.webapi.messages.v2.responder.KnoraContentV2 +import org.knora.webapi.messages.v2.responder.resourcemessages.ReadResourceV2 import org.knora.webapi.messages.v2.responder.standoffmessages._ import org.knora.webapi.util.IriConversions._ import org.knora.webapi.util.JavaUtil.Optional @@ -908,6 +909,24 @@ class StringFormatter private(val maybeSettings: Option[SettingsImpl] = None, ma ontologySchema = None ) + /** + * The singleton instance of `knora-base:ForbiddenResource`. + */ + val forbiddenResource: ReadResourceV2 = ReadResourceV2( + resourceIri = StringFormatter.ForbiddenResourceIri, + label = "This resource is a proxy for a resource you are not allowed to see", + resourceClassIri = toSmartIri(OntologyConstants.KnoraBase.ForbiddenResource), + attachedToUser = SharedTestDataADM.rootUser.id, + projectADM = SharedTestDataADM.systemProject, + permissions = "V knora-admin:UnknownUser", + userPermission = PermissionUtilADM.ViewPermission, + values = Map.empty, + creationDate = Instant.parse("2017-10-06T11:05:37Z"), + lastModificationDate = None, + versionDate = None, + deletionInfo = None + ) + /** * The implementation of [[SmartIri]]. An instance of this class can only be constructed by [[StringFormatter]]. * The constructor validates and parses the IRI. diff --git a/webapi/src/test/resources/test-data/searchR2RV2/ThingWithHiddenThing.jsonld b/webapi/src/test/resources/test-data/searchR2RV2/ThingWithHiddenThing.jsonld index 394c91b0b3..a5f9f39163 100644 --- a/webapi/src/test/resources/test-data/searchR2RV2/ThingWithHiddenThing.jsonld +++ b/webapi/src/test/resources/test-data/searchR2RV2/ThingWithHiddenThing.jsonld @@ -59,8 +59,30 @@ "@id" : "http://rdfh.ch/users/9XBCrDV3SRa7kS1WwynB4Q" }, "knora-api:hasPermissions" : "V knora-admin:UnknownUser|M knora-admin:ProjectMember", - "knora-api:linkValueHasTargetIri" : { - "@id" : "http://rdfh.ch/0001/IwMDbs0KQsaxSRUTl2cAIQ" + "knora-api:linkValueHasTarget" : { + "@id" : "http://rdfh.ch/0000/forbiddenResource", + "@type" : "knora-api:ForbiddenResource", + "knora-api:arkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV" + }, + "knora-api:attachedToProject" : { + "@id" : "http://www.knora.org/ontology/knora-admin#SystemProject" + }, + "knora-api:attachedToUser" : { + "@id" : "http://rdfh.ch/users/root" + }, + "knora-api:creationDate" : { + "@type" : "xsd:dateTimeStamp", + "@value" : "2017-10-06T11:05:37Z" + }, + "knora-api:hasPermissions" : "V knora-admin:UnknownUser", + "knora-api:userHasPermission" : "V", + "knora-api:versionArkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV.20171006T110537Z" + }, + "rdfs:label" : "This resource is a proxy for a resource you are not allowed to see" }, "knora-api:userHasPermission" : "V", "knora-api:valueCreationDate" : { diff --git a/webapi/src/test/resources/test-data/searchR2RV2/ThingsWithOptionalDecimalGreaterThan1.jsonld b/webapi/src/test/resources/test-data/searchR2RV2/ThingsWithOptionalDecimalGreaterThan1.jsonld index b3200fa68f..23bacefcd3 100644 --- a/webapi/src/test/resources/test-data/searchR2RV2/ThingsWithOptionalDecimalGreaterThan1.jsonld +++ b/webapi/src/test/resources/test-data/searchR2RV2/ThingsWithOptionalDecimalGreaterThan1.jsonld @@ -500,13 +500,13 @@ "@type" : "xsd:dateTimeStamp", "@value" : "2017-10-06T11:05:37Z" }, - "knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|V knora-admin:UnknownUser", + "knora-api:hasPermissions" : "V knora-admin:UnknownUser", "knora-api:userHasPermission" : "V", "knora-api:versionArkUrl" : { "@type" : "xsd:anyURI", "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV.20171006T110537Z" }, - "rdfs:label" : "This resource is a proxy for a resource you are not allowed to see (may depend on the context: query path)" + "rdfs:label" : "This resource is a proxy for a resource you are not allowed to see" }, { "@id" : "http://rdfh.ch/0001/a-thing-with-text-valuesLanguage", "@type" : "anything:Thing", @@ -596,13 +596,13 @@ "@type" : "xsd:dateTimeStamp", "@value" : "2017-10-06T11:05:37Z" }, - "knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|V knora-admin:UnknownUser", + "knora-api:hasPermissions" : "V knora-admin:UnknownUser", "knora-api:userHasPermission" : "V", "knora-api:versionArkUrl" : { "@type" : "xsd:anyURI", "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV.20171006T110537Z" }, - "rdfs:label" : "This resource is a proxy for a resource you are not allowed to see (may depend on the context: query path)" + "rdfs:label" : "This resource is a proxy for a resource you are not allowed to see" }, { "@id" : "http://rdfh.ch/0000/forbiddenResource", "@type" : "knora-api:ForbiddenResource", @@ -620,13 +620,13 @@ "@type" : "xsd:dateTimeStamp", "@value" : "2017-10-06T11:05:37Z" }, - "knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|V knora-admin:UnknownUser", + "knora-api:hasPermissions" : "V knora-admin:UnknownUser", "knora-api:userHasPermission" : "V", "knora-api:versionArkUrl" : { "@type" : "xsd:anyURI", "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV.20171006T110537Z" }, - "rdfs:label" : "This resource is a proxy for a resource you are not allowed to see (may depend on the context: query path)" + "rdfs:label" : "This resource is a proxy for a resource you are not allowed to see" }, { "@id" : "http://rdfh.ch/0000/forbiddenResource", "@type" : "knora-api:ForbiddenResource", @@ -644,13 +644,13 @@ "@type" : "xsd:dateTimeStamp", "@value" : "2017-10-06T11:05:37Z" }, - "knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|V knora-admin:UnknownUser", + "knora-api:hasPermissions" : "V knora-admin:UnknownUser", "knora-api:userHasPermission" : "V", "knora-api:versionArkUrl" : { "@type" : "xsd:anyURI", "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV.20171006T110537Z" }, - "rdfs:label" : "This resource is a proxy for a resource you are not allowed to see (may depend on the context: query path)" + "rdfs:label" : "This resource is a proxy for a resource you are not allowed to see" } ], "@context" : { "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", diff --git a/webapi/src/test/resources/test-data/searchR2RV2/searchResponseWithforbiddenResource.jsonld b/webapi/src/test/resources/test-data/searchR2RV2/searchResponseWithforbiddenResource.jsonld index 062d311c5f..261d1a4f25 100644 --- a/webapi/src/test/resources/test-data/searchR2RV2/searchResponseWithforbiddenResource.jsonld +++ b/webapi/src/test/resources/test-data/searchR2RV2/searchResponseWithforbiddenResource.jsonld @@ -16,13 +16,13 @@ "@type" : "xsd:dateTimeStamp", "@value" : "2017-10-06T11:05:37Z" }, - "knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|V knora-admin:UnknownUser", + "knora-api:hasPermissions" : "V knora-admin:UnknownUser", "knora-api:userHasPermission" : "V", "knora-api:versionArkUrl" : { "@type" : "xsd:anyURI", "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV.20171006T110537Z" }, - "rdfs:label" : "This resource is a proxy for a resource you are not allowed to see (may depend on the context: query path)" + "rdfs:label" : "This resource is a proxy for a resource you are not allowed to see" }, { "@id" : "http://rdfh.ch/0001/a-thing-with-text-valuesLanguage", "@type" : "anything:Thing", diff --git a/webapi/src/test/resources/test-data/searchR2RV2/thingWithOptionalDateSortedDesc.jsonld b/webapi/src/test/resources/test-data/searchR2RV2/thingWithOptionalDateSortedDesc.jsonld index 95ccd1e0c3..7f09e295e5 100644 --- a/webapi/src/test/resources/test-data/searchR2RV2/thingWithOptionalDateSortedDesc.jsonld +++ b/webapi/src/test/resources/test-data/searchR2RV2/thingWithOptionalDateSortedDesc.jsonld @@ -568,13 +568,13 @@ "@type" : "xsd:dateTimeStamp", "@value" : "2017-10-06T11:05:37Z" }, - "knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|V knora-admin:UnknownUser", + "knora-api:hasPermissions" : "V knora-admin:UnknownUser", "knora-api:userHasPermission" : "V", "knora-api:versionArkUrl" : { "@type" : "xsd:anyURI", "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV.20171006T110537Z" }, - "rdfs:label" : "This resource is a proxy for a resource you are not allowed to see (may depend on the context: query path)" + "rdfs:label" : "This resource is a proxy for a resource you are not allowed to see" }, { "@id" : "http://rdfh.ch/0001/a-thing-with-text-valuesLanguage", "@type" : "anything:Thing", @@ -664,13 +664,13 @@ "@type" : "xsd:dateTimeStamp", "@value" : "2017-10-06T11:05:37Z" }, - "knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|V knora-admin:UnknownUser", + "knora-api:hasPermissions" : "V knora-admin:UnknownUser", "knora-api:userHasPermission" : "V", "knora-api:versionArkUrl" : { "@type" : "xsd:anyURI", "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV.20171006T110537Z" }, - "rdfs:label" : "This resource is a proxy for a resource you are not allowed to see (may depend on the context: query path)" + "rdfs:label" : "This resource is a proxy for a resource you are not allowed to see" }, { "@id" : "http://rdfh.ch/0000/forbiddenResource", "@type" : "knora-api:ForbiddenResource", @@ -688,13 +688,13 @@ "@type" : "xsd:dateTimeStamp", "@value" : "2017-10-06T11:05:37Z" }, - "knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|V knora-admin:UnknownUser", + "knora-api:hasPermissions" : "V knora-admin:UnknownUser", "knora-api:userHasPermission" : "V", "knora-api:versionArkUrl" : { "@type" : "xsd:anyURI", "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV.20171006T110537Z" }, - "rdfs:label" : "This resource is a proxy for a resource you are not allowed to see (may depend on the context: query path)" + "rdfs:label" : "This resource is a proxy for a resource you are not allowed to see" } ], "@context" : { "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", diff --git a/webapi/src/test/scala/org/knora/webapi/util/ConstructResponseUtilV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/util/ConstructResponseUtilV2Spec.scala index d8fb0029f5..bbdebbb354 100644 --- a/webapi/src/test/scala/org/knora/webapi/util/ConstructResponseUtilV2Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/util/ConstructResponseUtilV2Spec.scala @@ -51,20 +51,12 @@ class ConstructResponseUtilV2Spec extends CoreSpec() with ImplicitSender { val queryResultsSeparated: RdfResources = ConstructResponseUtilV2.splitMainResourcesAndValueRdfData(constructQueryResults = resourceRequestResponse, requestingUser = incunabulaUser) val resourceFuture: Future[ReadResourceV2] = for { - forbiddenResourceSeq: ReadResourcesSequenceV2 <- (responderManager ? ResourcesGetRequestV2( - resourceIris = Seq(StringFormatter.ForbiddenResourceIri), - targetSchema = ApiV2Complex, // This has no effect, because ForbiddenResource has no values. - requestingUser = KnoraSystemInstances.Users.SystemUser)).mapTo[ReadResourcesSequenceV2] - - forbiddenResource = forbiddenResourceSeq.toResource(StringFormatter.ForbiddenResourceIri) - resourceResponse <- ConstructResponseUtilV2.createFullResourceResponse( resourceIri = resourceIri, resourceRdfData = queryResultsSeparated(resourceIri), mappings = Map.empty, queryStandoff = false, versionDate = None, - forbiddenResource = forbiddenResource, responderManager = responderManager, targetSchema = ApiV2Complex, settings = settings, From d6c8fc0a446eb182250e9ec658104668cd4ae35f Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Fri, 28 Feb 2020 14:25:08 +0100 Subject: [PATCH 05/23] refactor(test): Simplify code. --- .../util/ConstructResponseUtilV2Spec.scala | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/webapi/src/test/scala/org/knora/webapi/util/ConstructResponseUtilV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/util/ConstructResponseUtilV2Spec.scala index bbdebbb354..8b892fce73 100644 --- a/webapi/src/test/scala/org/knora/webapi/util/ConstructResponseUtilV2Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/util/ConstructResponseUtilV2Spec.scala @@ -21,12 +21,11 @@ package org.knora.webapi.util import java.io.File -import akka.pattern.ask import akka.testkit.ImplicitSender import akka.util.Timeout import org.knora.webapi._ import org.knora.webapi.messages.store.triplestoremessages.SparqlExtendedConstructResponse -import org.knora.webapi.messages.v2.responder.resourcemessages.{ReadResourceV2, ReadResourcesSequenceV2, ResourcesGetRequestV2} +import org.knora.webapi.messages.v2.responder.resourcemessages.{ReadResourceV2, ReadResourcesSequenceV2} import org.knora.webapi.responders.v2.{ResourcesResponderV2SpecFullData, ResourcesResponseCheckerV2} import org.knora.webapi.util.ConstructResponseUtilV2.RdfResources @@ -50,19 +49,17 @@ class ConstructResponseUtilV2Spec extends CoreSpec() with ImplicitSender { val resourceRequestResponse: SparqlExtendedConstructResponse = SparqlExtendedConstructResponse.parseTurtleResponse(turtleStr, log).get val queryResultsSeparated: RdfResources = ConstructResponseUtilV2.splitMainResourcesAndValueRdfData(constructQueryResults = resourceRequestResponse, requestingUser = incunabulaUser) - val resourceFuture: Future[ReadResourceV2] = for { - resourceResponse <- ConstructResponseUtilV2.createFullResourceResponse( - resourceIri = resourceIri, - resourceRdfData = queryResultsSeparated(resourceIri), - mappings = Map.empty, - queryStandoff = false, - versionDate = None, - responderManager = responderManager, - targetSchema = ApiV2Complex, - settings = settings, - requestingUser = incunabulaUser - ) - } yield resourceResponse + val resourceFuture: Future[ReadResourceV2] = ConstructResponseUtilV2.createFullResourceResponse( + resourceIri = resourceIri, + resourceRdfData = queryResultsSeparated(resourceIri), + mappings = Map.empty, + queryStandoff = false, + versionDate = None, + responderManager = responderManager, + targetSchema = ApiV2Complex, + settings = settings, + requestingUser = incunabulaUser + ) val resource: ReadResourceV2 = Await.result(resourceFuture, 10.seconds) val resourceSequence = ReadResourcesSequenceV2(numberOfResources = 1, resources = Seq(resource)) From 7f1d91f1c7695c461a6e5e635f2690a89cbe0284 Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Fri, 28 Feb 2020 16:14:12 +0100 Subject: [PATCH 06/23] refactor(api-v2): Simplify response processing (ongoing). --- .../resourcemessages/ResourceMessagesV2.scala | 13 +- .../responders/v2/ResourcesResponderV2.scala | 67 ++- .../responders/v2/SearchResponderV2.scala | 20 +- .../responders/v2/StandoffResponderV2.scala | 8 +- .../webapi/util/ConstructResponseUtilV2.scala | 401 +++++++++--------- .../util/ConstructResponseUtilV2Spec.scala | 10 +- 6 files changed, 243 insertions(+), 276 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index 5a0318019f..464ad78d2d 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -889,13 +889,12 @@ case class ReadResourcesSequenceV2(numberOfResources: Int, resources: Seq[ReadRe // #toJsonLDDocument /** - * Checks that a [[ReadResourcesSequenceV2]] contains exactly one resource, and returns that resource. If the resource - * is not present, or if it's `ForbiddenResource`, throws an exception. + * Checks that a [[ReadResourcesSequenceV2]] contains exactly one resource, and returns that resource. * * @param requestedResourceIri the IRI of the expected resource. * @return the resource. */ - def toResource(requestedResourceIri: IRI): ReadResourceV2 = { + def toResource(requestedResourceIri: IRI)(implicit stringFormatter: StringFormatter): ReadResourceV2 = { if (numberOfResources == 0) { throw AssertionException(s"Expected one resource, <$requestedResourceIri>, but no resources were returned") } @@ -904,13 +903,7 @@ case class ReadResourcesSequenceV2(numberOfResources: Int, resources: Seq[ReadRe throw AssertionException(s"More than one resource returned with IRI <$requestedResourceIri>") } - val resourceInfo = resources.head - - if (resourceInfo.resourceIri == StringFormatter.ForbiddenResourceIri) { // TODO: #1543 - throw NotFoundException(s"Resource <$requestedResourceIri> does not exist, has been deleted, or you do not have permission to view it and/or the values of the specified property") - } - - resourceInfo + resources.head } /** diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index 7dbb16a276..70520cc949 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -1157,14 +1157,10 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt resourceRequestResponse: SparqlExtendedConstructResponse <- (storeManager ? SparqlExtendedConstructRequest(resourceRequestSparql)).mapTo[SparqlExtendedConstructResponse] // separate resources and values - queryResultsSeparated: RdfResources = ConstructResponseUtilV2.splitMainResourcesAndValueRdfData(constructQueryResults = resourceRequestResponse, requestingUser = requestingUser) - - // check if all the requested resources were returned - requestedButMissing: Set[IRI] = resourceIrisDistinct.toSet -- queryResultsSeparated.keySet - - _ = if (requestedButMissing.nonEmpty) { - throw NotFoundException(s"One or more requested resources were not found (maybe you do not have permission to see them, or they are marked as deleted): ${requestedButMissing.map(resourceIri => s"<$resourceIri>").mkString(", ")}") - } + queryResultsSeparated: RdfResources = ConstructResponseUtilV2.splitMainResourcesAndValueRdfData( + constructQueryResults = resourceRequestResponse, + requestingUser = requestingUser + ) } yield queryResultsSeparated } @@ -1214,22 +1210,17 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt FastFuture.successful(Map.empty[IRI, MappingAndXSLTransformation]) } - resourcesResponseFutures: Vector[Future[ReadResourceV2]] = resourceIrisDistinct.map { - resIri: IRI => - ConstructResponseUtilV2.createFullResourceResponse( - resourceIri = resIri, - resourceRdfData = queryResultsSeparated(resIri), - mappings = mappingsAsMap, - queryStandoff = queryStandoff, - versionDate = versionDate, - responderManager = responderManager, - targetSchema = targetSchema, - settings = settings, - requestingUser = requestingUser - ) - }.toVector - - resourcesResponse: Vector[ReadResourceV2] <- Future.sequence(resourcesResponseFutures) + resourcesResponse: Vector[ReadResourceV2] <- ConstructResponseUtilV2.createApiResponse( + queryResults = queryResultsSeparated, + orderByResourceIri = resourceIrisDistinct, + mappings = mappingsAsMap, + queryStandoff = queryStandoff, + versionDate = versionDate, + responderManager = responderManager, + targetSchema = targetSchema, + settings = settings, + requestingUser = requestingUser + ) _ = valueUuid match { case Some(definedValueUuid) => @@ -1267,23 +1258,17 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt requestingUser = requestingUser ) - resourcesResponseFutures: Vector[Future[ReadResourceV2]] = resourceIrisDistinct.map { - resIri: IRI => - ConstructResponseUtilV2.createFullResourceResponse( - resourceIri = resIri, - resourceRdfData = queryResultsSeparated(resIri), - mappings = Map.empty[IRI, MappingAndXSLTransformation], - queryStandoff = false, - versionDate = None, - responderManager = responderManager, - targetSchema = targetSchema, - settings = settings, - requestingUser = requestingUser - ) - }.toVector - - resourcesResponse: Vector[ReadResourceV2] <- Future.sequence(resourcesResponseFutures) - + resourcesResponse: Vector[ReadResourceV2] <- ConstructResponseUtilV2.createApiResponse( + queryResults = queryResultsSeparated, + orderByResourceIri = resourceIrisDistinct, + mappings = Map.empty[IRI, MappingAndXSLTransformation], + queryStandoff = false, + versionDate = None, + responderManager = responderManager, + targetSchema = targetSchema, + settings = settings, + requestingUser = requestingUser + ) } yield ReadResourcesSequenceV2(numberOfResources = resourceIrisDistinct.size, resources = resourcesResponse) } diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala index ab7a046945..df1e86828b 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala @@ -265,11 +265,12 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand // _ = println(mappingsAsMap) - resources: Vector[ReadResourceV2] <- ConstructResponseUtilV2.createSearchResponse( - searchResults = queryResultsSeparatedWithFullGraphPattern, + resources: Vector[ReadResourceV2] <- ConstructResponseUtilV2.createApiResponse( + queryResults = queryResultsSeparatedWithFullGraphPattern, orderByResourceIri = resourceIris, mappings = mappingsAsMap, queryStandoff = queryStandoff, + versionDate = None, responderManager = responderManager, settings = settings, targetSchema = targetSchema, @@ -532,11 +533,12 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand FastFuture.successful(Map.empty[IRI, MappingAndXSLTransformation]) } - resources <- ConstructResponseUtilV2.createSearchResponse( - searchResults = queryResultsSeparatedWithFullGraphPattern, + resources <- ConstructResponseUtilV2.createApiResponse( + queryResults = queryResultsSeparatedWithFullGraphPattern, orderByResourceIri = mainResourceIris, mappings = mappingsAsMap, queryStandoff = queryStandoff, + versionDate = None, responderManager = responderManager, settings = settings, targetSchema = targetSchema, @@ -666,11 +668,12 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand } // Construct a ReadResourceV2 for each resource that the user has permission to see. - searchResponse <- ConstructResponseUtilV2.createSearchResponse( - searchResults = queryResultsSeparated, + searchResponse <- ConstructResponseUtilV2.createApiResponse( + queryResults = queryResultsSeparated, orderByResourceIri = mainResourceIris, mappings = mappings, queryStandoff = maybeStandoffMinStartIndex.nonEmpty, + versionDate = None, responderManager = responderManager, targetSchema = resourcesInProjectGetRequestV2.targetSchema, settings = settings, @@ -794,10 +797,11 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand //_ = println(queryResultsSeparated) - resources <- ConstructResponseUtilV2.createSearchResponse( - searchResults = queryResultsSeparated, + resources <- ConstructResponseUtilV2.createApiResponse( + queryResults = queryResultsSeparated, orderByResourceIri = mainResourceIris.toSeq.sorted, queryStandoff = false, + versionDate = None, responderManager = responderManager, targetSchema = targetSchema, settings = settings, diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala index 6b623f8022..89ed684f11 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala @@ -108,9 +108,9 @@ class StandoffResponderV2(responderData: ResponderData) extends Responder(respon throw NotFoundException(s"Resource <${getStandoffRequestV2.resourceIri}> was not found (maybe you do not have permission to see it, or it is marked as deleted)") } - readResourceV2: ReadResourceV2 <- ConstructResponseUtilV2.createFullResourceResponse( - resourceIri = getStandoffRequestV2.resourceIri, - resourceRdfData = queryResultsSeparated(getStandoffRequestV2.resourceIri), + resources: Vector[ReadResourceV2] <- ConstructResponseUtilV2.createApiResponse( + queryResults = queryResultsSeparated, + orderByResourceIri = Seq(getStandoffRequestV2.resourceIri), mappings = Map.empty, queryStandoff = false, versionDate = None, @@ -120,6 +120,8 @@ class StandoffResponderV2(responderData: ResponderData) extends Responder(respon requestingUser = getStandoffRequestV2.requestingUser ) + readResourceV2 = resources.headOption.getOrElse(throw NotFoundException(s"Resource <${getStandoffRequestV2.resourceIri}> not found")) + valueObj: ReadValueV2 = readResourceV2.values.values.flatten.find(_.valueIri == getStandoffRequestV2.valueIri).getOrElse(throw NotFoundException(s"Value <${getStandoffRequestV2.valueIri}> not found in resource <${getStandoffRequestV2.resourceIri}> (maybe you do not have permission to see it, or it is marked as deleted)")) textValueObj: ReadTextValueV2 = valueObj match { diff --git a/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala b/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala index 1e90dfafaf..b2b00afb0f 100644 --- a/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala @@ -277,9 +277,22 @@ object ConstructResponseUtilV2 { case class ResourceWithValueRdfData(subjectIri: IRI, assertions: FlatPredicateObjects, isMainResource: Boolean, - userPermission: EntityPermission, + userPermission: Option[EntityPermission], valuePropertyAssertions: RdfPropertyValues) extends RdfData + + /** + * A [[ResourceWithValueRdfData]] representing a placeholder for a main resource that the user doesn't + * have permission to see. It is replaced by `ForbiddenResource` during processing. + */ + val ForbiddenMainResourcePlaceholder: ResourceWithValueRdfData = ResourceWithValueRdfData( + subjectIri = StringFormatter.ForbiddenResourceIri, + assertions = Map.empty, + isMainResource = true, + userPermission = Some(ViewPermission), + valuePropertyAssertions = Map.empty + ) + /** * A [[ResourceWithValueRdfData]] representing a placeholder for a dependent resource that the user doesn't * have permission to see. It is replaced by `ForbiddenResource` during processing. @@ -288,7 +301,7 @@ object ConstructResponseUtilV2 { subjectIri = StringFormatter.ForbiddenResourceIri, assertions = Map.empty, isMainResource = false, - userPermission = ViewPermission, + userPermission = Some(ViewPermission), valuePropertyAssertions = Map.empty ) @@ -329,24 +342,8 @@ object ConstructResponseUtilV2 { assertions.getOrElse(OntologyConstants.Rdf.Type.toSmartIri, Seq.empty).contains(IriLiteralV2(OntologyConstants.KnoraBase.Resource)) } - // filter out the resources the user does not have permissions to see, including dependent resources. - - val resourceStatementsVisible: Map[IRI, RdfWithUserPermission] = resourceStatements.map { + val flatResourcesWithValues: RdfResources = resourceStatements.map { case (resourceIri: IRI, assertions: ConstructPredicateObjects) => - val maybeUserPermission: Option[EntityPermission] = PermissionUtilADM.getUserPermissionFromConstructAssertionsADM(resourceIri, assertions, requestingUser) - resourceIri -> RdfWithUserPermission(assertions, maybeUserPermission) - }.filter { - case (_: IRI, statements: RdfWithUserPermission) => statements.maybeUserPermission.nonEmpty - } - - // Collect the IRIs of the resources that were filtered out because the user does not have permission - // to see them. - val resourceIrisNotVisible: Set[IRI] = resourceStatements.keySet -- resourceStatementsVisible.keySet - - val flatResourcesWithValues: RdfResources = resourceStatementsVisible.map { - case (resourceIri: IRI, resourceRdfWithUserPermission: RdfWithUserPermission) => - val assertions: ConstructPredicateObjects = resourceRdfWithUserPermission.assertions - // remove inferred statements (non explicit) returned in the query result // the query returns the following inferred information: // - every resource is a knora-base:Resource @@ -502,18 +499,42 @@ object ConstructResponseUtilV2 { case (pred: SmartIri, objs: Seq[LiteralV2]) => pred -> objs.head } + val userPermission: Option[EntityPermission] = PermissionUtilADM.getUserPermissionFromConstructAssertionsADM(resourceIri, assertions, requestingUser) + // create a map of resource Iris to a `ResourceWithValueRdfData` resourceIri -> ResourceWithValueRdfData( subjectIri = resourceIri, assertions = resourceAssertions, isMainResource = isMainResource, - userPermission = resourceRdfWithUserPermission.maybeUserPermission.get, + userPermission = userPermission, valuePropertyAssertions = valuePropertyToValueObject ) } + // Identify the resources that the user has permission to see. + + val (visibleResources, hiddenResources) = flatResourcesWithValues.partition { + case (_: IRI, resource: ResourceWithValueRdfData) => resource.userPermission.nonEmpty + } + + val mainResourceIrisVisible: Set[IRI] = visibleResources.collect { + case (resourceIri: IRI, resource: ResourceWithValueRdfData) if resource.isMainResource => resourceIri + }.toSet + + val mainResourceIrisNotVisible: Set[IRI] = hiddenResources.collect { + case (resourceIri: IRI, resource: ResourceWithValueRdfData) if resource.isMainResource => resourceIri + }.toSet + + val dependentResourceIrisVisible: Set[IRI] = visibleResources.collect { + case (resourceIri: IRI, resource: ResourceWithValueRdfData) if !resource.isMainResource => resourceIri + }.toSet + + val dependentResourceIrisNotVisible: Set[IRI] = hiddenResources.collect { + case (resourceIri: IRI, resource: ResourceWithValueRdfData) if !resource.isMainResource => resourceIri + }.toSet + // get incoming links for each resource: a map of resource IRIs to resources that link to it - val incomingLinksForResource: Map[IRI, RdfResources] = flatResourcesWithValues.map { + val incomingLinksForResource: Map[IRI, RdfResources] = visibleResources.map { case (resourceIri: IRI, values: ResourceWithValueRdfData) => // get all incoming links for resourceIri @@ -565,7 +586,7 @@ object ConstructResponseUtilV2 { * @return the same resource, with any nested resources attached to it. */ def nestResources(resourceIri: IRI, alreadyTraversed: Set[IRI] = Set.empty[IRI]): ResourceWithValueRdfData = { - val resource = flatResourcesWithValues(resourceIri) + val resource = visibleResources(resourceIri) val transformedValuePropertyAssertions: RdfPropertyValues = resource.valuePropertyAssertions.map { case (propIri, values) => @@ -578,14 +599,14 @@ object ConstructResponseUtilV2 { value } else { // Do we have the dependent resource? - if (flatResourcesWithValues.contains(dependentResourceIri)) { + if (dependentResourceIrisVisible.contains(dependentResourceIri)) { // Yes. Nest it in the link value. val dependentResource: ResourceWithValueRdfData = nestResources(dependentResourceIri, alreadyTraversed + resourceIri) value.copy( nestedResource = Some(dependentResource) ) - } else if (resourceIrisNotVisible.contains(dependentResourceIri)) { + } else if (dependentResourceIrisNotVisible.contains(dependentResourceIri)) { // No, because the user doesn't have permission to see it. Nest a placeholder for // ForbiddenResource in the link value. value.copy( @@ -666,17 +687,17 @@ object ConstructResponseUtilV2 { } - val mainResourceIris: Set[IRI] = flatResourcesWithValues.filter { - case (_, resource) => resource.isMainResource // only main resources are present on the top level, dependent resources are nested in the link values - }.map { - case (resourceIri, _) => resourceIri - }.toSet - - mainResourceIris.map { + val mainResourcesNested: Map[IRI, ResourceWithValueRdfData] = mainResourceIrisVisible.map { resourceIri => val transformedResource = nestResources(resourceIri) resourceIri -> transformedResource }.toMap + + val forbiddenResourcesForHiddenMainResources: Map[IRI, ResourceWithValueRdfData] = mainResourceIrisNotVisible.map { + resourceIri => resourceIri -> ForbiddenMainResourcePlaceholder + }.toMap + + mainResourcesNested ++ forbiddenResourcesForHiddenMainResources } /** @@ -923,14 +944,14 @@ object ConstructResponseUtilV2 { /** * Given a [[ValueRdfData]], constructs a [[ValueContentV2]], considering the specific type of the given [[ValueRdfData]]. * - * @param valueObject the given [[ValueRdfData]]. - * @param mappings the mappings needed for standoff conversions and XSL transformations. - * @param queryStandoff if `true`, make separate queries to get the standoff for text values. - * @param versionDate if defined, represents the requested time in the the resources' version history. - * @param responderManager the Knora responder manager. - * @param targetSchema the schema of the response. - * @param settings the application's settings. - * @param requestingUser the user making the request. + * @param valueObject the given [[ValueRdfData]]. + * @param mappings the mappings needed for standoff conversions and XSL transformations. + * @param queryStandoff if `true`, make separate queries to get the standoff for text values. + * @param versionDate if defined, represents the requested time in the the resources' version history. + * @param responderManager the Knora responder manager. + * @param targetSchema the schema of the response. + * @param settings the application's settings. + * @param requestingUser the user making the request. * @return a [[ValueContentV2]] representing a value. */ private def createValueContentV2FromValueRdfData(resourceIri: IRI, @@ -1138,200 +1159,162 @@ object ConstructResponseUtilV2 { } } - val resourceLabel: String = resourceWithValueRdfData.requireStringObject(OntologyConstants.Rdfs.Label.toSmartIri) - val resourceClassStr: IRI = resourceWithValueRdfData.requireIriObject(OntologyConstants.Rdf.Type.toSmartIri) - val resourceClass = resourceClassStr.toSmartIriWithErr(throw InconsistentTriplestoreDataException(s"Couldn't parse rdf:type of resource <$resourceIri>: <$resourceClassStr>")) - val resourceAttachedToUser: IRI = resourceWithValueRdfData.requireIriObject(OntologyConstants.KnoraBase.AttachedToUser.toSmartIri) - val resourceAttachedToProject: IRI = resourceWithValueRdfData.requireIriObject(OntologyConstants.KnoraBase.AttachedToProject.toSmartIri) - val resourcePermissions: String = resourceWithValueRdfData.requireStringObject(OntologyConstants.KnoraBase.HasPermissions.toSmartIri) - val resourceCreationDate: Instant = resourceWithValueRdfData.requireDateTimeObject(OntologyConstants.KnoraBase.CreationDate.toSmartIri) - val resourceLastModificationDate: Option[Instant] = resourceWithValueRdfData.maybeDateTimeObject(OntologyConstants.KnoraBase.LastModificationDate.toSmartIri) - val resourceDeletionInfo = getDeletionInfo(resourceWithValueRdfData) - - // get the resource's values - val valueObjectFutures: Map[SmartIri, Seq[Future[ReadValueV2]]] = resourceWithValueRdfData.valuePropertyAssertions.map { - case (property: SmartIri, valObjs: Seq[ValueRdfData]) => - val readValues: Seq[Future[ReadValueV2]] = valObjs.sortBy(_.subjectIri).sortBy { // order values by value IRI, then by knora-base:valueHasOrder - valObj: ValueRdfData => - // set order to zero if not given - valObj.maybeIntObject(OntologyConstants.KnoraBase.ValueHasOrder.toSmartIri).getOrElse(0) - }.map { - valObj: ValueRdfData => - for { - valueContent: ValueContentV2 <- createValueContentV2FromValueRdfData( - resourceIri = resourceIri, - valueObject = valObj, - mappings = mappings, - queryStandoff = queryStandoff, - responderManager = responderManager, - requestingUser = requestingUser, - targetSchema = targetSchema, - settings = settings - ) - - attachedToUser = valObj.requireIriObject(OntologyConstants.KnoraBase.AttachedToUser.toSmartIri) - permissions = valObj.requireStringObject(OntologyConstants.KnoraBase.HasPermissions.toSmartIri) - valueCreationDate: Instant = valObj.requireDateTimeObject(OntologyConstants.KnoraBase.ValueCreationDate.toSmartIri) - valueDeletionInfo = getDeletionInfo(valObj) - valueHasUUID: UUID = stringFormatter.decodeUuid(valObj.requireStringObject(OntologyConstants.KnoraBase.ValueHasUUID.toSmartIri)) - previousValueIri: Option[IRI] = valObj.maybeIriObject(OntologyConstants.KnoraBase.PreviousValue.toSmartIri) - - } yield valueContent match { - case linkValueContentV2: LinkValueContentV2 => - val valueHasRefCount: Int = valObj.requireIntObject(OntologyConstants.KnoraBase.ValueHasRefCount.toSmartIri) - - ReadLinkValueV2( - valueIri = valObj.subjectIri, - attachedToUser = attachedToUser, - permissions = permissions, - userPermission = valObj.userPermission, - valueCreationDate = valueCreationDate, - valueHasUUID = valueHasUUID, - valueContent = linkValueContentV2, - valueHasRefCount = valueHasRefCount, - previousValueIri = previousValueIri, - deletionInfo = valueDeletionInfo + if (resourceWithValueRdfData.subjectIri == StringFormatter.ForbiddenResourceIri) { + FastFuture.successful(stringFormatter.forbiddenResource) + } else { + val resourceLabel: String = resourceWithValueRdfData.requireStringObject(OntologyConstants.Rdfs.Label.toSmartIri) + val resourceClassStr: IRI = resourceWithValueRdfData.requireIriObject(OntologyConstants.Rdf.Type.toSmartIri) + val resourceClass = resourceClassStr.toSmartIriWithErr(throw InconsistentTriplestoreDataException(s"Couldn't parse rdf:type of resource <$resourceIri>: <$resourceClassStr>")) + val resourceAttachedToUser: IRI = resourceWithValueRdfData.requireIriObject(OntologyConstants.KnoraBase.AttachedToUser.toSmartIri) + val resourceAttachedToProject: IRI = resourceWithValueRdfData.requireIriObject(OntologyConstants.KnoraBase.AttachedToProject.toSmartIri) + val resourcePermissions: String = resourceWithValueRdfData.requireStringObject(OntologyConstants.KnoraBase.HasPermissions.toSmartIri) + val resourceCreationDate: Instant = resourceWithValueRdfData.requireDateTimeObject(OntologyConstants.KnoraBase.CreationDate.toSmartIri) + val resourceLastModificationDate: Option[Instant] = resourceWithValueRdfData.maybeDateTimeObject(OntologyConstants.KnoraBase.LastModificationDate.toSmartIri) + val resourceDeletionInfo = getDeletionInfo(resourceWithValueRdfData) + + // get the resource's values + val valueObjectFutures: Map[SmartIri, Seq[Future[ReadValueV2]]] = resourceWithValueRdfData.valuePropertyAssertions.map { + case (property: SmartIri, valObjs: Seq[ValueRdfData]) => + val readValues: Seq[Future[ReadValueV2]] = valObjs.sortBy(_.subjectIri).sortBy { // order values by value IRI, then by knora-base:valueHasOrder + valObj: ValueRdfData => + // set order to zero if not given + valObj.maybeIntObject(OntologyConstants.KnoraBase.ValueHasOrder.toSmartIri).getOrElse(0) + }.map { + valObj: ValueRdfData => + for { + valueContent: ValueContentV2 <- createValueContentV2FromValueRdfData( + resourceIri = resourceIri, + valueObject = valObj, + mappings = mappings, + queryStandoff = queryStandoff, + responderManager = responderManager, + requestingUser = requestingUser, + targetSchema = targetSchema, + settings = settings ) - case textValueContentV2: TextValueContentV2 => - val maybeValueHasMaxStandoffStartIndex: Option[Int] = valObj.maybeIntObject(OntologyConstants.KnoraBase.ValueHasMaxStandoffStartIndex.toSmartIri) - - ReadTextValueV2( - valueIri = valObj.subjectIri, - attachedToUser = attachedToUser, - permissions = permissions, - userPermission = valObj.userPermission, - valueCreationDate = valueCreationDate, - valueHasUUID = valueHasUUID, - valueContent = textValueContentV2, - valueHasMaxStandoffStartIndex = maybeValueHasMaxStandoffStartIndex, - previousValueIri = previousValueIri, - deletionInfo = valueDeletionInfo - ) + attachedToUser = valObj.requireIriObject(OntologyConstants.KnoraBase.AttachedToUser.toSmartIri) + permissions = valObj.requireStringObject(OntologyConstants.KnoraBase.HasPermissions.toSmartIri) + valueCreationDate: Instant = valObj.requireDateTimeObject(OntologyConstants.KnoraBase.ValueCreationDate.toSmartIri) + valueDeletionInfo = getDeletionInfo(valObj) + valueHasUUID: UUID = stringFormatter.decodeUuid(valObj.requireStringObject(OntologyConstants.KnoraBase.ValueHasUUID.toSmartIri)) + previousValueIri: Option[IRI] = valObj.maybeIriObject(OntologyConstants.KnoraBase.PreviousValue.toSmartIri) + + } yield valueContent match { + case linkValueContentV2: LinkValueContentV2 => + val valueHasRefCount: Int = valObj.requireIntObject(OntologyConstants.KnoraBase.ValueHasRefCount.toSmartIri) + + ReadLinkValueV2( + valueIri = valObj.subjectIri, + attachedToUser = attachedToUser, + permissions = permissions, + userPermission = valObj.userPermission, + valueCreationDate = valueCreationDate, + valueHasUUID = valueHasUUID, + valueContent = linkValueContentV2, + valueHasRefCount = valueHasRefCount, + previousValueIri = previousValueIri, + deletionInfo = valueDeletionInfo + ) - case otherValueContentV2: ValueContentV2 => - ReadOtherValueV2( - valueIri = valObj.subjectIri, - attachedToUser = attachedToUser, - permissions = permissions, - userPermission = valObj.userPermission, - valueCreationDate = valueCreationDate, - valueHasUUID = valueHasUUID, - valueContent = otherValueContentV2, - previousValueIri = previousValueIri, - deletionInfo = valueDeletionInfo - ) - } - } + case textValueContentV2: TextValueContentV2 => + val maybeValueHasMaxStandoffStartIndex: Option[Int] = valObj.maybeIntObject(OntologyConstants.KnoraBase.ValueHasMaxStandoffStartIndex.toSmartIri) + + ReadTextValueV2( + valueIri = valObj.subjectIri, + attachedToUser = attachedToUser, + permissions = permissions, + userPermission = valObj.userPermission, + valueCreationDate = valueCreationDate, + valueHasUUID = valueHasUUID, + valueContent = textValueContentV2, + valueHasMaxStandoffStartIndex = maybeValueHasMaxStandoffStartIndex, + previousValueIri = previousValueIri, + deletionInfo = valueDeletionInfo + ) - property -> readValues - } + case otherValueContentV2: ValueContentV2 => + ReadOtherValueV2( + valueIri = valObj.subjectIri, + attachedToUser = attachedToUser, + permissions = permissions, + userPermission = valObj.userPermission, + valueCreationDate = valueCreationDate, + valueHasUUID = valueHasUUID, + valueContent = otherValueContentV2, + previousValueIri = previousValueIri, + deletionInfo = valueDeletionInfo + ) + } + } - for { - projectResponse: ProjectGetResponseADM <- (responderManager ? ProjectGetRequestADM(ProjectIdentifierADM(maybeIri = Some(resourceAttachedToProject)), requestingUser = requestingUser)).mapTo[ProjectGetResponseADM] - valueObjects <- ActorUtil.sequenceSeqFuturesInMap(valueObjectFutures) - } yield ReadResourceV2( - resourceIri = resourceIri, - resourceClassIri = resourceClass, - label = resourceLabel, - attachedToUser = resourceAttachedToUser, - projectADM = projectResponse.project, - permissions = resourcePermissions, - userPermission = resourceWithValueRdfData.userPermission, - values = valueObjects, - creationDate = resourceCreationDate, - lastModificationDate = resourceLastModificationDate, - versionDate = versionDate, - deletionInfo = resourceDeletionInfo - ) - } + property -> readValues + } - /** - * Creates a response to a full resource request. - * - * @param resourceIri the IRI of the requested resource. - * @param resourceRdfData the results returned by the triplestore. - * @param mappings the mappings needed for standoff conversions and XSL transformations. - * @param queryStandoff if `true`, make separate queries to get the standoff for text values. - * @param versionDate if defined, represents the requested time in the the resources' version history. - * @param responderManager the Knora responder manager. - * @param targetSchema the schema of response. - * @param settings the application's settings. - * @param requestingUser the user making the request. - * @return a [[ReadResourceV2]]. - */ - def createFullResourceResponse(resourceIri: IRI, - resourceRdfData: ResourceWithValueRdfData, - mappings: Map[IRI, MappingAndXSLTransformation], - queryStandoff: Boolean, - versionDate: Option[Instant], - responderManager: ActorRef, - targetSchema: ApiV2Schema, - settings: SettingsImpl, - requestingUser: UserADM)(implicit stringFormatter: StringFormatter, timeout: Timeout, executionContext: ExecutionContext): Future[ReadResourceV2] = { - - constructReadResourceV2( - resourceIri = resourceIri, - resourceWithValueRdfData = resourceRdfData, - mappings = mappings, - queryStandoff = queryStandoff, - versionDate = versionDate, - responderManager = responderManager, - requestingUser = requestingUser, - targetSchema = targetSchema, - settings = settings - ) + for { + projectResponse: ProjectGetResponseADM <- (responderManager ? ProjectGetRequestADM(ProjectIdentifierADM(maybeIri = Some(resourceAttachedToProject)), requestingUser = requestingUser)).mapTo[ProjectGetResponseADM] + valueObjects <- ActorUtil.sequenceSeqFuturesInMap(valueObjectFutures) + } yield ReadResourceV2( + resourceIri = resourceIri, + resourceClassIri = resourceClass, + label = resourceLabel, + attachedToUser = resourceAttachedToUser, + projectADM = projectResponse.project, + permissions = resourcePermissions, + userPermission = resourceWithValueRdfData.userPermission.get, + values = valueObjects, + creationDate = resourceCreationDate, + lastModificationDate = resourceLastModificationDate, + versionDate = versionDate, + deletionInfo = resourceDeletionInfo + ) + } } /** - * Creates a response to a fulltext or extended search. + * Creates an API response. * - * @param searchResults the resources that matched the query and the client has permissions to see. + * @param queryResults the query results. * @param orderByResourceIri the order in which the resources should be returned. * @param mappings the mappings to convert standoff to XML, if any. * @param queryStandoff if `true`, make separate queries to get the standoff for text values. + * @param versionDate if defined, represents the requested time in the the resources' version history. * @param responderManager the Knora responder manager. * @param targetSchema the schema of response. * @param settings the application's settings. * @param requestingUser the user making the request. * @return a collection of [[ReadResourceV2]] representing the search results. */ - def createSearchResponse(searchResults: RdfResources, - orderByResourceIri: Seq[IRI], - mappings: Map[IRI, MappingAndXSLTransformation] = Map.empty[IRI, MappingAndXSLTransformation], - queryStandoff: Boolean, - responderManager: ActorRef, - targetSchema: ApiV2Schema, - settings: SettingsImpl, - requestingUser: UserADM)(implicit stringFormatter: StringFormatter, timeout: Timeout, executionContext: ExecutionContext): Future[Vector[ReadResourceV2]] = { + def createApiResponse(queryResults: RdfResources, + orderByResourceIri: Seq[IRI], + mappings: Map[IRI, MappingAndXSLTransformation] = Map.empty[IRI, MappingAndXSLTransformation], + queryStandoff: Boolean, + versionDate: Option[Instant], + responderManager: ActorRef, + targetSchema: ApiV2Schema, + settings: SettingsImpl, + requestingUser: UserADM)(implicit stringFormatter: StringFormatter, timeout: Timeout, executionContext: ExecutionContext): Future[Vector[ReadResourceV2]] = { // iterate over orderByResourceIris and construct the response in the correct order val readResourceFutures: Vector[Future[ReadResourceV2]] = orderByResourceIri.map { resourceIri: IRI => + val resource: ResourceWithValueRdfData = queryResults(resourceIri) + + // If the user doesn't have permission to see the resource, its IRI will be mapped + // to ForbiddenMainResourcePlaceholder. Therefore use the IRI from the ResourceWithValueRdfData, + // not the actual resource IRI. + constructReadResourceV2( + resourceIri = resource.subjectIri, + resourceWithValueRdfData = resource, + mappings = mappings, + queryStandoff = queryStandoff, + versionDate = None, + responderManager = responderManager, + targetSchema = targetSchema, + settings = settings, + requestingUser = requestingUser + ) - // the user may not have the permissions to see the resource - // i.e. it may not be contained in searchResults - searchResults.get(resourceIri) match { - case Some(assertions: ResourceWithValueRdfData) => - // sufficient permissions - // add the resource to the list of results - constructReadResourceV2( - resourceIri = resourceIri, - resourceWithValueRdfData = assertions, - mappings = mappings, - queryStandoff = queryStandoff, - versionDate = None, - responderManager = responderManager, - targetSchema = targetSchema, - settings = settings, - requestingUser = requestingUser - ) - - case None => - // include the forbidden resource instead of skipping (the amount of results should be constant -> limit) - Future(stringFormatter.forbiddenResource) - - } }.toVector Future.sequence(readResourceFutures) diff --git a/webapi/src/test/scala/org/knora/webapi/util/ConstructResponseUtilV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/util/ConstructResponseUtilV2Spec.scala index 8b892fce73..d96abcaae5 100644 --- a/webapi/src/test/scala/org/knora/webapi/util/ConstructResponseUtilV2Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/util/ConstructResponseUtilV2Spec.scala @@ -49,9 +49,9 @@ class ConstructResponseUtilV2Spec extends CoreSpec() with ImplicitSender { val resourceRequestResponse: SparqlExtendedConstructResponse = SparqlExtendedConstructResponse.parseTurtleResponse(turtleStr, log).get val queryResultsSeparated: RdfResources = ConstructResponseUtilV2.splitMainResourcesAndValueRdfData(constructQueryResults = resourceRequestResponse, requestingUser = incunabulaUser) - val resourceFuture: Future[ReadResourceV2] = ConstructResponseUtilV2.createFullResourceResponse( - resourceIri = resourceIri, - resourceRdfData = queryResultsSeparated(resourceIri), + val resourcesFuture: Future[Vector[ReadResourceV2]] = ConstructResponseUtilV2.createApiResponse( + queryResults = queryResultsSeparated, + orderByResourceIri = Seq(resourceIri), mappings = Map.empty, queryStandoff = false, versionDate = None, @@ -61,8 +61,8 @@ class ConstructResponseUtilV2Spec extends CoreSpec() with ImplicitSender { requestingUser = incunabulaUser ) - val resource: ReadResourceV2 = Await.result(resourceFuture, 10.seconds) - val resourceSequence = ReadResourcesSequenceV2(numberOfResources = 1, resources = Seq(resource)) + val resources: Vector[ReadResourceV2] = Await.result(resourcesFuture, 10.seconds) + val resourceSequence = ReadResourcesSequenceV2(numberOfResources = 1, resources = resources) ResourcesResponseCheckerV2.compareReadResourcesSequenceV2Response(expected = resourcesResponderV2SpecFullData.expectedFullResourceResponseForZeitgloecklein, received = resourceSequence) } } From 5d1b28ec7346297dbbbc66413c481a801f7f1b25 Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Tue, 3 Mar 2020 13:24:27 +0100 Subject: [PATCH 07/23] feat(api-v2): Add ForbiddenValue (ongoing). --- knora-ontologies/knora-base.ttl | 23 +++++++++++----- .../org/knora/webapi/OntologyConstants.scala | 2 ++ .../valuemessages/ValueMessagesV2.scala | 27 +++++++++++++++++++ 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/knora-ontologies/knora-base.ttl b/knora-ontologies/knora-base.ttl index d5f19ef41c..a95b67dce9 100644 --- a/knora-ontologies/knora-base.ttl +++ b/knora-ontologies/knora-base.ttl @@ -2564,15 +2564,24 @@ :ForbiddenResource rdf:type owl:Class ; - rdfs:subClassOf :Resource , - [ rdf:type owl:Restriction ; - owl:onProperty :hasComment ; - owl:minCardinality "0"^^xsd:nonNegativeInteger - ]; + rdfs:subClassOf :Resource; + + rdfs:label "Forbidden resource"@en ; + + rdfs:comment "Represents a resource that the client does not have permission to see."@en . + + +### http://www.knora.org/ontology/knora-base#ForbiddenValue + +:ForbiddenValue rdf:type owl:Class ; + + rdfs:subClassOf :Value ; + + rdfs:label "Forbidden value"@en ; + + rdfs:comment "Represents a value that the client does not have permission to see."@en . - rdfs:label """A ForbiddenResource is a proxy for a resource that the client has insufficient permissions to see."""@en ; - rdfs:comment """A ForbiddenResource is a proxy for a resource that the client has insufficient permissions to see."""@en . ### http://www.knora.org/ontology/knora-base#XSLTransformation diff --git a/webapi/src/main/scala/org/knora/webapi/OntologyConstants.scala b/webapi/src/main/scala/org/knora/webapi/OntologyConstants.scala index e200b643ac..91de737421 100644 --- a/webapi/src/main/scala/org/knora/webapi/OntologyConstants.scala +++ b/webapi/src/main/scala/org/knora/webapi/OntologyConstants.scala @@ -318,6 +318,8 @@ object OntologyConstants { TextFileValue ) + val ForbiddenValue: IRI = KnoraBasePrefixExpansion + "ForbiddenValue" + val ListNode: IRI = KnoraBasePrefixExpansion + "ListNode" val ListNodeName: IRI = KnoraBasePrefixExpansion + "listNodeName" val IsRootNode: IRI = KnoraBasePrefixExpansion + "isRootNode" diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala index de0d8c6157..69e7fcdf79 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala @@ -2393,6 +2393,33 @@ object ColorValueContentV2 extends ValueContentReaderV2[ColorValueContentV2] { } } +/** + * Represents a value that the user does not have permission to see. + */ +case class ForbiddenValueContentV2(ontologySchema: OntologySchema) extends ValueContentV2 { + override def valueType: SmartIri = { + implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance + OntologyConstants.KnoraBase.ForbiddenValue.toSmartIri + } + + override def valueHasString: String = "Forbidden value" + + override def comment: Option[String] = None + + override def toOntologySchema(targetSchema: OntologySchema): ValueContentV2 = this.copy(ontologySchema = targetSchema) + + override def toJsonLDValue(targetSchema: ApiV2Schema, + projectADM: ProjectADM, + settings: SettingsImpl, + schemaOptions: Set[SchemaOption]): JsonLDValue = JsonLDObject(Map.empty) + + override def unescape: ValueContentV2 = this + + override def wouldDuplicateOtherValue(that: ValueContentV2): Boolean = false + + override def wouldDuplicateCurrentVersion(currentVersion: ValueContentV2): Boolean = false +} + /** * Represents a Knora URI value. * From e86b20bb1140b1a1c720d7c8504549b977b0f8f4 Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Wed, 4 Mar 2020 16:55:35 +0100 Subject: [PATCH 08/23] feat(api-v2): Remove ForbiddenResource (ongoing). --- knora-ontologies/knora-base.ttl | 22 -- .../org/knora/webapi/OntologyConstants.scala | 8 +- .../resourcemessages/ResourceMessagesV2.scala | 35 +- .../valuemessages/ValueMessagesV2.scala | 27 -- .../responders/v2/ResourcesResponderV2.scala | 41 +-- .../v2/ResponderWithStandoffV2.scala | 16 +- .../responders/v2/SearchResponderV2.scala | 259 ++++++------- .../responders/v2/StandoffResponderV2.scala | 12 +- .../webapi/util/ConstructResponseUtilV2.scala | 348 ++++++++---------- .../knora/webapi/util/StringFormatter.scala | 23 -- .../v2/ResourcesResponderV2Spec.scala | 42 +-- .../v2/ResourcesResponderV2SpecFullData.scala | 47 +-- .../v2/ResourcesResponseCheckerV2.scala | 2 +- ...sourcesResponseCheckerV2SpecFullData.scala | 7 +- .../responders/v2/SearchResponderV2Spec.scala | 12 +- .../responders/v2/ValuesResponderV2Spec.scala | 33 +- .../util/ConstructResponseUtilV2Spec.scala | 19 +- 17 files changed, 369 insertions(+), 584 deletions(-) diff --git a/knora-ontologies/knora-base.ttl b/knora-ontologies/knora-base.ttl index a95b67dce9..298793df7e 100644 --- a/knora-ontologies/knora-base.ttl +++ b/knora-ontologies/knora-base.ttl @@ -2560,28 +2560,6 @@ rdfs:comment "A resource containing a text file"@en . -### http://www.knora.org/ontology/knora-base#ForbiddenResource - -:ForbiddenResource rdf:type owl:Class ; - - rdfs:subClassOf :Resource; - - rdfs:label "Forbidden resource"@en ; - - rdfs:comment "Represents a resource that the client does not have permission to see."@en . - - -### http://www.knora.org/ontology/knora-base#ForbiddenValue - -:ForbiddenValue rdf:type owl:Class ; - - rdfs:subClassOf :Value ; - - rdfs:label "Forbidden value"@en ; - - rdfs:comment "Represents a value that the client does not have permission to see."@en . - - ### http://www.knora.org/ontology/knora-base#XSLTransformation diff --git a/webapi/src/main/scala/org/knora/webapi/OntologyConstants.scala b/webapi/src/main/scala/org/knora/webapi/OntologyConstants.scala index 91de737421..5859c75bb9 100644 --- a/webapi/src/main/scala/org/knora/webapi/OntologyConstants.scala +++ b/webapi/src/main/scala/org/knora/webapi/OntologyConstants.scala @@ -166,8 +166,6 @@ object OntologyConstants { val StillImageRepresentation: IRI = KnoraBasePrefixExpansion + "StillImageRepresentation" val TextRepresentation: IRI = KnoraBasePrefixExpansion + "TextRepresentation" - val ForbiddenResource: IRI = KnoraBasePrefixExpansion + "ForbiddenResource" - val XMLToStandoffMapping: IRI = KnoraBasePrefixExpansion + "XMLToStandoffMapping" val HasMappingElement: IRI = KnoraBasePrefixExpansion + "hasMappingElement" val MappingElement: IRI = KnoraBasePrefixExpansion + "MappingElement" @@ -318,8 +316,6 @@ object OntologyConstants { TextFileValue ) - val ForbiddenValue: IRI = KnoraBasePrefixExpansion + "ForbiddenValue" - val ListNode: IRI = KnoraBasePrefixExpansion + "ListNode" val ListNodeName: IRI = KnoraBasePrefixExpansion + "listNodeName" val IsRootNode: IRI = KnoraBasePrefixExpansion + "isRootNode" @@ -691,6 +687,7 @@ object OntologyConstants { val Result: IRI = KnoraApiV2PrefixExpansion + "result" val Error: IRI = KnoraApiV2PrefixExpansion + "error" + val MayHaveMoreResults: IRI = KnoraApiV2PrefixExpansion + "mayHaveMoreResults" val IsShared: IRI = KnoraApiV2PrefixExpansion + "isShared" val IsBuiltIn: IRI = KnoraApiV2PrefixExpansion + "isBuiltIn" @@ -735,7 +732,6 @@ object OntologyConstants { val Author: IRI = KnoraApiV2PrefixExpansion + "author" val Resource: IRI = KnoraApiV2PrefixExpansion + "Resource" - val ForbiddenResource: IRI = KnoraApiV2PrefixExpansion + "ForbiddenResource" val Region: IRI = KnoraApiV2PrefixExpansion + "Region" val Representation: IRI = KnoraApiV2PrefixExpansion + "Representation" val StillImageRepresentation: IRI = KnoraApiV2PrefixExpansion + "StillImageRepresentation" @@ -950,6 +946,7 @@ object OntologyConstants { val Result: IRI = KnoraApiV2PrefixExpansion + "result" val Error: IRI = KnoraApiV2PrefixExpansion + "error" + val MayHaveMoreResults: IRI = KnoraApiV2PrefixExpansion + "mayHaveMoreResults" val SubjectType: IRI = KnoraApiV2PrefixExpansion + "subjectType" @@ -979,7 +976,6 @@ object OntologyConstants { val ListNode: IRI = KnoraApiV2PrefixExpansion + "ListNode" val Resource: IRI = KnoraApiV2PrefixExpansion + "Resource" - val ForbiddenResource: IRI = KnoraApiV2PrefixExpansion + "ForbiddenResource" val ResourceIcon: IRI = KnoraApiV2PrefixExpansion + "resourceIcon" diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index 464ad78d2d..ab10ad6379 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -193,7 +193,7 @@ case class TEIHeader(headerInfo: ReadResourceV2, headerXSLT: Option[String], set if (headerXSLT.nonEmpty) { - val headerJSONLD = ReadResourcesSequenceV2(1, Vector(headerInfo)).toJsonLDDocument(ApiV2Complex, settings) + val headerJSONLD = ReadResourcesSequenceV2(Vector(headerInfo)).toJsonLDDocument(ApiV2Complex, settings) val rdfParser: RDFParser = Rio.createParser(RDFFormat.JSONLD) val stringReader = new StringReader(headerJSONLD.toCompactString) @@ -810,10 +810,9 @@ object DeleteOrEraseResourceRequestV2 extends KnoraJsonLDRequestReaderV2[DeleteO /** * Represents a sequence of resources read back from Knora. * - * @param numberOfResources the amount of resources returned. * @param resources a sequence of resources. */ -case class ReadResourcesSequenceV2(numberOfResources: Int, resources: Seq[ReadResourceV2]) extends KnoraResponseV2 with KnoraReadV2[ReadResourcesSequenceV2] with UpdateResultInProject { +case class ReadResourcesSequenceV2(resources: Seq[ReadResourceV2], mayHaveMoreResults: Boolean = false) extends KnoraResponseV2 with KnoraReadV2[ReadResourcesSequenceV2] with UpdateResultInProject { override def toOntologySchema(targetSchema: ApiV2Schema): ReadResourcesSequenceV2 = { copy( @@ -852,14 +851,14 @@ case class ReadResourcesSequenceV2(numberOfResources: Int, resources: Seq[ReadRe // Make the knora-api prefix for the target schema. - val knoraApiPrefixExpansion = targetSchema match { + val knoraApiPrefixExpansion: IRI = targetSchema match { case ApiV2Simple => OntologyConstants.KnoraApiV2Simple.KnoraApiV2PrefixExpansion case ApiV2Complex => OntologyConstants.KnoraApiV2Complex.KnoraApiV2PrefixExpansion } // Make the JSON-LD document. - val context = JsonLDUtil.makeContext( + val context: JsonLDObject = JsonLDUtil.makeContext( fixedPrefixes = Map( "rdf" -> OntologyConstants.Rdf.RdfPrefixExpansion, "rdfs" -> OntologyConstants.Rdfs.RdfsPrefixExpansion, @@ -869,9 +868,22 @@ case class ReadResourcesSequenceV2(numberOfResources: Int, resources: Seq[ReadRe knoraOntologiesNeedingPrefixes = projectSpecificOntologiesUsed ) - val body = JsonLDObject(Map( - JsonLDConstants.GRAPH -> JsonLDArray(resourcesJsonObjects) - )) + val mayHaveMoreResultsStatement: Option[(IRI, JsonLDBoolean)] = if (mayHaveMoreResults) { + val mayHaveMoreResultsProp: IRI = targetSchema match { + case ApiV2Simple => OntologyConstants.KnoraApiV2Simple.MayHaveMoreResults + case ApiV2Complex => OntologyConstants.KnoraApiV2Complex.MayHaveMoreResults + } + + Some(mayHaveMoreResultsProp -> JsonLDBoolean(mayHaveMoreResults)) + } else { + None + } + + val body = JsonLDObject( + Map( + JsonLDConstants.GRAPH -> JsonLDArray(resourcesJsonObjects) + ) ++ mayHaveMoreResultsStatement + ) JsonLDDocument(body = body, context = context) @@ -885,7 +897,6 @@ case class ReadResourcesSequenceV2(numberOfResources: Int, resources: Seq[ReadRe schemaOptions = schemaOptions ) } - // #toJsonLDDocument /** @@ -895,12 +906,12 @@ case class ReadResourcesSequenceV2(numberOfResources: Int, resources: Seq[ReadRe * @return the resource. */ def toResource(requestedResourceIri: IRI)(implicit stringFormatter: StringFormatter): ReadResourceV2 = { - if (numberOfResources == 0) { + if (resources.isEmpty) { throw AssertionException(s"Expected one resource, <$requestedResourceIri>, but no resources were returned") } - if (numberOfResources > 1) { - throw AssertionException(s"More than one resource returned with IRI <$requestedResourceIri>") + if (resources.size > 1) { + throw AssertionException(s"More than one resource returned") } resources.head diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala index 69e7fcdf79..de0d8c6157 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala @@ -2393,33 +2393,6 @@ object ColorValueContentV2 extends ValueContentReaderV2[ColorValueContentV2] { } } -/** - * Represents a value that the user does not have permission to see. - */ -case class ForbiddenValueContentV2(ontologySchema: OntologySchema) extends ValueContentV2 { - override def valueType: SmartIri = { - implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - OntologyConstants.KnoraBase.ForbiddenValue.toSmartIri - } - - override def valueHasString: String = "Forbidden value" - - override def comment: Option[String] = None - - override def toOntologySchema(targetSchema: OntologySchema): ValueContentV2 = this.copy(ontologySchema = targetSchema) - - override def toJsonLDValue(targetSchema: ApiV2Schema, - projectADM: ProjectADM, - settings: SettingsImpl, - schemaOptions: Set[SchemaOption]): JsonLDValue = JsonLDObject(Map.empty) - - override def unescape: ValueContentV2 = this - - override def wouldDuplicateOtherValue(that: ValueContentV2): Boolean = false - - override def wouldDuplicateCurrentVersion(currentVersion: ValueContentV2): Boolean = false -} - /** * Represents a Knora URI value. * diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index 70520cc949..d01c83a284 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -42,7 +42,7 @@ import org.knora.webapi.responders.v2.search.ConstructQuery import org.knora.webapi.responders.v2.search.gravsearch.GravsearchParser import org.knora.webapi.responders.{IriLocker, ResponderData} import org.knora.webapi.twirl.SparqlTemplateResourceToCreate -import org.knora.webapi.util.ConstructResponseUtilV2.{MappingAndXSLTransformation, RdfResources} +import org.knora.webapi.util.ConstructResponseUtilV2.MappingAndXSLTransformation import org.knora.webapi.util.IriConversions._ import org.knora.webapi.util.PermissionUtilADM.{AGreaterThanB, DeletePermission, ModifyPermission, PermissionComparisonResult} import org.knora.webapi.util._ @@ -314,8 +314,8 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt requestingUser = updateResourceMetadataRequestV2.requestingUser ) - _ = if (updatedResourcesSeq.numberOfResources != 1) { - throw AssertionException(s"Expected one resource, got ${resourcesSeq.numberOfResources}") + _ = if (updatedResourcesSeq.resources.size != 1) { + throw AssertionException(s"Expected one resource, got ${resourcesSeq.resources.size}") } updatedResource: ReadResourceV2 = updatedResourcesSeq.resources.head @@ -1066,7 +1066,6 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt } } } yield ReadResourcesSequenceV2( - numberOfResources = 1, resources = Seq(resource.copy(values = Map.empty)) ) } @@ -1121,7 +1120,7 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt valueUuid: Option[UUID], versionDate: Option[Instant], queryStandoff: Boolean, - requestingUser: UserADM): Future[RdfResources] = { + requestingUser: UserADM): Future[ConstructResponseUtilV2.MainResourcesAndValueRdfData] = { // eliminate duplicate Iris val resourceIrisDistinct: Seq[IRI] = resourceIris.distinct @@ -1133,12 +1132,6 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt ) for { - _ <- Future { - if (resourceIrisDistinct.toSet.contains(StringFormatter.ForbiddenResourceIri)) { - throw BadRequestException(s"<${StringFormatter.ForbiddenResourceIri}> cannot be requested") - } - } - resourceRequestSparql <- Future(queries.sparql.v2.txt.getResourcePropertiesAndValues( triplestore = settings.triplestoreType, resourceIris = resourceIrisDistinct, @@ -1157,11 +1150,11 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt resourceRequestResponse: SparqlExtendedConstructResponse <- (storeManager ? SparqlExtendedConstructRequest(resourceRequestSparql)).mapTo[SparqlExtendedConstructResponse] // separate resources and values - queryResultsSeparated: RdfResources = ConstructResponseUtilV2.splitMainResourcesAndValueRdfData( + mainResourcesAndValueRdfData: ConstructResponseUtilV2.MainResourcesAndValueRdfData = ConstructResponseUtilV2.splitMainResourcesAndValueRdfData( constructQueryResults = resourceRequestResponse, requestingUser = requestingUser ) - } yield queryResultsSeparated + } yield mainResourcesAndValueRdfData } @@ -1193,7 +1186,7 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt for { - queryResultsSeparated: RdfResources <- getResourcesFromTriplestore( + mainResourcesAndValueRdfData: ConstructResponseUtilV2.MainResourcesAndValueRdfData <- getResourcesFromTriplestore( resourceIris = resourceIris, preview = false, propertyIri = propertyIri, @@ -1205,17 +1198,18 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt // If we're querying standoff, get XML-to standoff mappings. mappingsAsMap: Map[IRI, MappingAndXSLTransformation] <- if (queryStandoff) { - getMappingsFromQueryResultsSeparated(queryResultsSeparated, requestingUser) + getMappingsFromQueryResultsSeparated(mainResourcesAndValueRdfData.resources, requestingUser) } else { FastFuture.successful(Map.empty[IRI, MappingAndXSLTransformation]) } - resourcesResponse: Vector[ReadResourceV2] <- ConstructResponseUtilV2.createApiResponse( - queryResults = queryResultsSeparated, + apiResponse: ReadResourcesSequenceV2 <- ConstructResponseUtilV2.createApiResponse( + mainResourcesAndValueRdfData = mainResourcesAndValueRdfData, orderByResourceIri = resourceIrisDistinct, mappings = mappingsAsMap, queryStandoff = queryStandoff, versionDate = versionDate, + calculateMayHaveMoreResults = false, responderManager = responderManager, targetSchema = targetSchema, settings = settings, @@ -1224,14 +1218,14 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt _ = valueUuid match { case Some(definedValueUuid) => - if (!resourcesResponse.exists(_.values.values.exists(_.exists(_.valueHasUUID == definedValueUuid)))) { + if (!apiResponse.resources.exists(_.values.values.exists(_.exists(_.valueHasUUID == definedValueUuid)))) { throw NotFoundException(s"Value with UUID ${stringFormatter.base64EncodeUuid(definedValueUuid)} not found (maybe you do not have permission to see it, or it is marked as deleted)") } case None => () } - } yield ReadResourcesSequenceV2(numberOfResources = resourceIrisDistinct.size, resources = resourcesResponse) + } yield apiResponse } @@ -1248,7 +1242,7 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt val resourceIrisDistinct: Seq[IRI] = resourceIris.distinct for { - queryResultsSeparated: RdfResources <- getResourcesFromTriplestore( + mainResourcesAndValueRdfData: ConstructResponseUtilV2.MainResourcesAndValueRdfData <- getResourcesFromTriplestore( resourceIris = resourceIris, preview = true, propertyIri = None, @@ -1258,18 +1252,19 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt requestingUser = requestingUser ) - resourcesResponse: Vector[ReadResourceV2] <- ConstructResponseUtilV2.createApiResponse( - queryResults = queryResultsSeparated, + apiResponse: ReadResourcesSequenceV2 <- ConstructResponseUtilV2.createApiResponse( + mainResourcesAndValueRdfData = mainResourcesAndValueRdfData, orderByResourceIri = resourceIrisDistinct, mappings = Map.empty[IRI, MappingAndXSLTransformation], queryStandoff = false, versionDate = None, + calculateMayHaveMoreResults = false, responderManager = responderManager, targetSchema = targetSchema, settings = settings, requestingUser = requestingUser ) - } yield ReadResourcesSequenceV2(numberOfResources = resourceIrisDistinct.size, resources = resourcesResponse) + } yield apiResponse } diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResponderWithStandoffV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResponderWithStandoffV2.scala index 3d2576fcbf..d0f9d15648 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResponderWithStandoffV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResponderWithStandoffV2.scala @@ -31,17 +31,17 @@ import org.knora.webapi.util.ConstructResponseUtilV2.{MappingAndXSLTransformatio import scala.concurrent.Future /** - * An abstract class with standoff utility methods for v2 responders. - */ + * An abstract class with standoff utility methods for v2 responders. + */ abstract class ResponderWithStandoffV2(responderData: ResponderData) extends Responder(responderData) { /** - * Gets mappings referred to in query results [[Map[IRI, ResourceWithValueRdfData]]]. - * - * @param queryResultsSeparated query results referring to mappings. - * @param requestingUser the user making the request. - * @return the referred mappings. - */ + * Gets mappings referred to in query results [[Map[IRI, ResourceWithValueRdfData]]]. + * + * @param queryResultsSeparated query results referring to mappings. + * @param requestingUser the user making the request. + * @return the referred mappings. + */ protected def getMappingsFromQueryResultsSeparated(queryResultsSeparated: Map[IRI, ResourceWithValueRdfData], requestingUser: UserADM): Future[Map[IRI, MappingAndXSLTransformation]] = { // collect the Iris of the mappings referred to in the resources' text values diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala index df1e86828b..e68d64c45b 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala @@ -24,17 +24,18 @@ import akka.pattern._ import org.knora.webapi._ import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.store.triplestoremessages.{SubjectV2, _} +import org.knora.webapi.messages.v2.responder.KnoraResponseV2 import org.knora.webapi.messages.v2.responder.ontologymessages.{EntityInfoGetRequestV2, EntityInfoGetResponseV2, ReadClassInfoV2, ReadPropertyInfoV2} import org.knora.webapi.messages.v2.responder.resourcemessages._ import org.knora.webapi.messages.v2.responder.searchmessages._ import org.knora.webapi.responders.Responder.handleUnexpectedMessage import org.knora.webapi.responders.ResponderData -import org.knora.webapi.util.ApacheLuceneSupport._ import org.knora.webapi.responders.v2.search._ import org.knora.webapi.responders.v2.search.gravsearch._ import org.knora.webapi.responders.v2.search.gravsearch.prequery._ import org.knora.webapi.responders.v2.search.gravsearch.types._ -import org.knora.webapi.util.ConstructResponseUtilV2.{MappingAndXSLTransformation, RdfResources, ResourceWithValueRdfData} +import org.knora.webapi.util.ApacheLuceneSupport._ +import org.knora.webapi.util.ConstructResponseUtilV2.MappingAndXSLTransformation import org.knora.webapi.util.IriConversions._ import org.knora.webapi.util._ import org.knora.webapi.util.standoff.StandoffTagUtilV2 @@ -47,9 +48,9 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand private val gravsearchTypeInspectionRunner = new GravsearchTypeInspectionRunner(responderData) /** - * Receives a message of type [[SearchResponderRequestV2]], and returns an appropriate response message. - */ - def receive(msg: SearchResponderRequestV2) = msg match { + * Receives a message of type [[SearchResponderRequestV2]], and returns an appropriate response message. + */ + def receive(msg: SearchResponderRequestV2): Future[KnoraResponseV2] = msg match { case FullTextSearchCountRequestV2(searchValue, limitToProject, limitToResourceClass, limitToStandoffClass, requestingUser) => fulltextSearchCountV2(searchValue, limitToProject, limitToResourceClass, limitToStandoffClass, requestingUser) case FulltextSearchRequestV2(searchValue, offset, limitToProject, limitToResourceClass, limitToStandoffClass, targetSchema, schemaOptions, requestingUser) => fulltextSearchV2(searchValue, offset, limitToProject, limitToResourceClass, limitToStandoffClass, targetSchema, schemaOptions, requestingUser) case GravsearchCountRequestV2(query, requestingUser) => gravsearchCountV2(inputQuery = query, requestingUser = requestingUser) @@ -61,17 +62,17 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand } /** - * Performs a fulltext search and returns the resources count (how many resources match the search criteria), - * without taking into consideration permission checking. - * - * This method does not return the resources themselves. - * - * @param searchValue the values to search for. - * @param limitToProject limit search to given project. - * @param limitToResourceClass limit search to given resource class. - * @param requestingUser the the client making the request. - * @return a [[ResourceCountV2]] representing the number of resources that have been found. - */ + * Performs a fulltext search and returns the resources count (how many resources match the search criteria), + * without taking into consideration permission checking. + * + * This method does not return the resources themselves. + * + * @param searchValue the values to search for. + * @param limitToProject limit search to given project. + * @param limitToResourceClass limit search to given resource class. + * @param requestingUser the the client making the request. + * @return a [[ResourceCountV2]] representing the number of resources that have been found. + */ private def fulltextSearchCountV2(searchValue: String, limitToProject: Option[IRI], limitToResourceClass: Option[SmartIri], limitToStandoffClass: Option[SmartIri], requestingUser: UserADM): Future[ResourceCountV2] = { val searchTerms: LuceneQueryString = LuceneQueryString(searchValue) @@ -104,17 +105,17 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand } /** - * Performs a fulltext search (simple search). - * - * @param searchValue the values to search for. - * @param offset the offset to be used for paging. - * @param limitToProject limit search to given project. - * @param limitToResourceClass limit search to given resource class. - * @param targetSchema the target API schema. - * @param schemaOptions the schema options submitted with the request. - * @param requestingUser the the client making the request. - * @return a [[ReadResourcesSequenceV2]] representing the resources that have been found. - */ + * Performs a fulltext search (simple search). + * + * @param searchValue the values to search for. + * @param offset the offset to be used for paging. + * @param limitToProject limit search to given project. + * @param limitToResourceClass limit search to given resource class. + * @param targetSchema the target API schema. + * @param schemaOptions the schema options submitted with the request. + * @param requestingUser the the client making the request. + * @return a [[ReadResourcesSequenceV2]] representing the resources that have been found. + */ private def fulltextSearchV2(searchValue: String, offset: Int, limitToProject: Option[IRI], @@ -154,8 +155,8 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand resultRow: VariableResultsRow => resultRow.rowMap(FullTextSearchConstants.resourceVar.variableName) } - // make sure that the prequery returned some results - queryResultsSeparatedWithFullGraphPattern: Map[IRI, ConstructResponseUtilV2.ResourceWithValueRdfData] <- if (resourceIris.nonEmpty) { + // If the prequery returned some results, prepare a main query. + mainResourcesAndValueRdfData: ConstructResponseUtilV2.MainResourcesAndValueRdfData <- if (resourceIris.nonEmpty) { // for each resource, create a Set of value object IRIs val valueObjectIrisPerResource: Map[IRI, Set[IRI]] = prequeryResponse.results.bindings.foldLeft(Map.empty[IRI, Set[IRI]]) { @@ -178,7 +179,7 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand // collect all value object IRIs val allValueObjectIris = valueObjectIrisPerResource.values.flatten.toSet - // create CONSTRUCT queries to query resources and their values + // create a CONSTRUCT query to query resources and their values val mainQuery = FullTextMainQueryGenerator.createMainQuery( resourceIris = resourceIris.toSet, valueObjectIris = allValueObjectIris, @@ -208,48 +209,17 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand searchResponse: SparqlExtendedConstructResponse <- (storeManager ? SparqlExtendedConstructRequest(triplestoreSpecificQuery.toSparql)).mapTo[SparqlExtendedConstructResponse] // separate resources and value objects - queryResultsSep = ConstructResponseUtilV2.splitMainResourcesAndValueRdfData(constructQueryResults = searchResponse, requestingUser = requestingUser) - - // for each main resource check if all dependent resources and value objects are still present after permission checking - // this ensures that the user has sufficient permissions on the whole graph pattern - queryResWithFullGraphPattern = queryResultsSep.foldLeft(Map.empty[IRI, ConstructResponseUtilV2.ResourceWithValueRdfData]) { - case (acc: Map[IRI, ConstructResponseUtilV2.ResourceWithValueRdfData], (mainResIri: IRI, values: ConstructResponseUtilV2.ResourceWithValueRdfData)) => - - valueObjectIrisPerResource.get(mainResIri) match { - - case Some(valObjIris) => - - // check for presence of value objects: valueObjectIrisPerResource - val expectedValueObjects: Set[IRI] = valueObjectIrisPerResource(mainResIri) - - // value property assertions for the current resource - val valuePropAssertions: Map[SmartIri, Seq[ConstructResponseUtilV2.ValueRdfData]] = values.valuePropertyAssertions - - // all value objects contained in `valuePropAssertions` - val resAndValueObjIris: MainQueryResultProcessor.ResourceIrisAndValueObjectIris = MainQueryResultProcessor.collectResourceIrisAndValueObjectIrisFromMainQueryResult(valuePropAssertions) - - // check if the client has sufficient permissions on all value objects IRIs present in the graph pattern - val allValueObjects: Boolean = resAndValueObjIris.valueObjectIris.intersect(expectedValueObjects) == expectedValueObjects - - if (allValueObjects) { - // sufficient permissions, include the main resource and its values - acc + (mainResIri -> values) - } else { - // insufficient permissions, skip the resource - acc - } - - case None => - // no properties -> rfs:label matched - acc + (mainResIri -> values) - } - } - - } yield queryResWithFullGraphPattern + queryResultsSep: ConstructResponseUtilV2.MainResourcesAndValueRdfData = ConstructResponseUtilV2.splitMainResourcesAndValueRdfData(constructQueryResults = searchResponse, requestingUser = requestingUser) + } yield queryResultsSep } else { // the prequery returned no results, no further query is necessary - Future(Map.empty[IRI, ConstructResponseUtilV2.ResourceWithValueRdfData]) + Future( + ConstructResponseUtilV2.MainResourcesAndValueRdfData( + resources = Map.empty[IRI, ConstructResponseUtilV2.ResourceWithValueRdfData], + hiddenResourceCount = 0 + ) + ) } // Find out whether to query standoff along with text values. This boolean value will be passed to @@ -258,18 +228,19 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand // If we're querying standoff, get XML-to standoff mappings. mappingsAsMap: Map[IRI, MappingAndXSLTransformation] <- if (queryStandoff) { - getMappingsFromQueryResultsSeparated(queryResultsSeparatedWithFullGraphPattern, requestingUser) + getMappingsFromQueryResultsSeparated(mainResourcesAndValueRdfData.resources, requestingUser) } else { FastFuture.successful(Map.empty[IRI, MappingAndXSLTransformation]) } // _ = println(mappingsAsMap) - resources: Vector[ReadResourceV2] <- ConstructResponseUtilV2.createApiResponse( - queryResults = queryResultsSeparatedWithFullGraphPattern, + apiResponse: ReadResourcesSequenceV2 <- ConstructResponseUtilV2.createApiResponse( + mainResourcesAndValueRdfData = mainResourcesAndValueRdfData, orderByResourceIri = resourceIris, mappings = mappingsAsMap, queryStandoff = queryStandoff, + calculateMayHaveMoreResults = true, versionDate = None, responderManager = responderManager, settings = settings, @@ -277,21 +248,17 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand requestingUser = requestingUser ) - } yield ReadResourcesSequenceV2( - numberOfResources = resourceIris.size, - resources = resources - ) - + } yield apiResponse } /** - * Performs a count query for a Gravsearch query provided by the user. - * - * @param inputQuery a Gravsearch query provided by the client. - * @param requestingUser the the client making the request. - * @return a [[ResourceCountV2]] representing the number of resources that have been found. - */ + * Performs a count query for a Gravsearch query provided by the user. + * + * @param inputQuery a Gravsearch query provided by the client. + * @param requestingUser the the client making the request. + * @return a [[ResourceCountV2]] representing the number of resources that have been found. + */ private def gravsearchCountV2(inputQuery: ConstructQuery, apiSchema: ApiV2Schema = ApiV2Simple, requestingUser: UserADM): Future[ResourceCountV2] = { // make sure that OFFSET is 0 @@ -358,14 +325,14 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand } /** - * Performs a search using a Gravsearch query provided by the client. - * - * @param inputQuery a Gravsearch query provided by the client. - * @param targetSchema the target API schema. - * @param schemaOptions the schema options submitted with the request. - * @param requestingUser the the client making the request. - * @return a [[ReadResourcesSequenceV2]] representing the resources that have been found. - */ + * Performs a search using a Gravsearch query provided by the client. + * + * @param inputQuery a Gravsearch query provided by the client. + * @param targetSchema the target API schema. + * @param schemaOptions the schema options submitted with the request. + * @param requestingUser the the client making the request. + * @return a [[ReadResourcesSequenceV2]] representing the resources that have been found. + */ private def gravsearchV2(inputQuery: ConstructQuery, targetSchema: ApiV2Schema, schemaOptions: Set[SchemaOption], @@ -433,7 +400,7 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand resultRow.rowMap(mainResourceVar.variableName) } - queryResultsSeparatedWithFullGraphPattern: Map[IRI, ConstructResponseUtilV2.ResourceWithValueRdfData] <- if (mainResourceIris.nonEmpty) { + queryResultsSeparatedWithFullGraphPattern: ConstructResponseUtilV2.MainResourcesAndValueRdfData <- if (mainResourceIris.nonEmpty) { // at least one resource matched the prequery // get all the IRIs for variables representing dependent resources per main resource @@ -499,14 +466,14 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand mainQueryResponse: SparqlExtendedConstructResponse <- (storeManager ? SparqlExtendedConstructRequest(triplestoreSpecificSparql)).mapTo[SparqlExtendedConstructResponse] // Filter out values that the user doesn't have permission to see. - queryResultsFilteredForPermissions: Map[IRI, ConstructResponseUtilV2.ResourceWithValueRdfData] = ConstructResponseUtilV2.splitMainResourcesAndValueRdfData( + queryResultsFilteredForPermissions: ConstructResponseUtilV2.MainResourcesAndValueRdfData = ConstructResponseUtilV2.splitMainResourcesAndValueRdfData( constructQueryResults = mainQueryResponse, requestingUser = requestingUser ) // filter out those value objects that the user does not want to be returned by the query (not present in the input query's CONSTRUCT clause) queryResWithFullGraphPatternOnlyRequestedValues: Map[IRI, ConstructResponseUtilV2.ResourceWithValueRdfData] = MainQueryResultProcessor.getRequestedValuesFromResultsWithFullGraphPattern( - queryResultsFilteredForPermissions, + queryResultsFilteredForPermissions.resources, valueObjectVarsAndIrisPerMainResource, allResourceVariablesFromTypeInspection, dependentResourceIrisFromTypeInspection, @@ -515,11 +482,11 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand inputQuery ) - } yield queryResWithFullGraphPatternOnlyRequestedValues + } yield ConstructResponseUtilV2.MainResourcesAndValueRdfData(resources = queryResWithFullGraphPatternOnlyRequestedValues, queryResultsFilteredForPermissions.hiddenResourceCount) } else { // the prequery returned no results, no further query is necessary - Future(Map.empty[IRI, ConstructResponseUtilV2.ResourceWithValueRdfData]) + Future(ConstructResponseUtilV2.MainResourcesAndValueRdfData(Map.empty[IRI, ConstructResponseUtilV2.ResourceWithValueRdfData], 0)) } // Find out whether to query standoff along with text values. This boolean value will be passed to @@ -528,36 +495,34 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand // If we're querying standoff, get XML-to standoff mappings. mappingsAsMap: Map[IRI, MappingAndXSLTransformation] <- if (queryStandoff) { - getMappingsFromQueryResultsSeparated(queryResultsSeparatedWithFullGraphPattern, requestingUser) + getMappingsFromQueryResultsSeparated(queryResultsSeparatedWithFullGraphPattern.resources, requestingUser) } else { FastFuture.successful(Map.empty[IRI, MappingAndXSLTransformation]) } - resources <- ConstructResponseUtilV2.createApiResponse( - queryResults = queryResultsSeparatedWithFullGraphPattern, + apiResponse: ReadResourcesSequenceV2 <- ConstructResponseUtilV2.createApiResponse( + mainResourcesAndValueRdfData = queryResultsSeparatedWithFullGraphPattern, orderByResourceIri = mainResourceIris, mappings = mappingsAsMap, queryStandoff = queryStandoff, versionDate = None, + calculateMayHaveMoreResults = true, responderManager = responderManager, settings = settings, targetSchema = targetSchema, requestingUser = requestingUser ) - } yield ReadResourcesSequenceV2( - numberOfResources = mainResourceIris.size, - resources = resources - ) + } yield apiResponse } /** - * Gets resources from a project. - * - * @param resourcesInProjectGetRequestV2 the request message. - * @return a [[ReadResourcesSequenceV2]]. - */ + * Gets resources from a project. + * + * @param resourcesInProjectGetRequestV2 the request message. + * @return a [[ReadResourcesSequenceV2]]. + */ private def searchResourcesByProjectAndClassV2(resourcesInProjectGetRequestV2: SearchResourcesByProjectAndClassRequestV2): Future[ReadResourcesSequenceV2] = { val internalClassIri = resourcesInProjectGetRequestV2.resourceClass.toOntologySchema(InternalSchema) val maybeInternalOrderByPropertyIri: Option[SmartIri] = resourcesInProjectGetRequestV2.orderByProperty.map(_.toOntologySchema(InternalSchema)) @@ -637,7 +602,7 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand ) // Are there any matching resources? - resources: Vector[ReadResourceV2] <- if (mainResourceIris.nonEmpty) { + apiResponse: ReadResourcesSequenceV2 <- if (mainResourceIris.nonEmpty) { for { // Yes. Do a CONSTRUCT query to get the contents of those resources. If we're querying standoff, get // at most one page of standoff per text value. @@ -658,48 +623,45 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand resourceRequestResponse: SparqlExtendedConstructResponse <- (storeManager ? SparqlExtendedConstructRequest(resourceRequestSparql)).mapTo[SparqlExtendedConstructResponse] // separate resources and values - queryResultsSeparated: RdfResources = ConstructResponseUtilV2.splitMainResourcesAndValueRdfData(constructQueryResults = resourceRequestResponse, requestingUser = resourcesInProjectGetRequestV2.requestingUser) + mainResourcesAndValueRdfData: ConstructResponseUtilV2.MainResourcesAndValueRdfData = ConstructResponseUtilV2.splitMainResourcesAndValueRdfData(constructQueryResults = resourceRequestResponse, requestingUser = resourcesInProjectGetRequestV2.requestingUser) // If we're querying standoff, get XML-to standoff mappings. mappings: Map[IRI, MappingAndXSLTransformation] <- if (queryStandoff) { - getMappingsFromQueryResultsSeparated(queryResultsSeparated, resourcesInProjectGetRequestV2.requestingUser) + getMappingsFromQueryResultsSeparated(mainResourcesAndValueRdfData.resources, resourcesInProjectGetRequestV2.requestingUser) } else { FastFuture.successful(Map.empty[IRI, MappingAndXSLTransformation]) } // Construct a ReadResourceV2 for each resource that the user has permission to see. - searchResponse <- ConstructResponseUtilV2.createApiResponse( - queryResults = queryResultsSeparated, + readResourcesSequence: ReadResourcesSequenceV2 <- ConstructResponseUtilV2.createApiResponse( + mainResourcesAndValueRdfData = mainResourcesAndValueRdfData, orderByResourceIri = mainResourceIris, mappings = mappings, queryStandoff = maybeStandoffMinStartIndex.nonEmpty, versionDate = None, + calculateMayHaveMoreResults = true, responderManager = responderManager, targetSchema = resourcesInProjectGetRequestV2.targetSchema, settings = settings, requestingUser = resourcesInProjectGetRequestV2.requestingUser ) - } yield searchResponse + } yield readResourcesSequence } else { - FastFuture.successful(Vector.empty[ReadResourceV2]) + FastFuture.successful(ReadResourcesSequenceV2(Vector.empty[ReadResourceV2])) } - - } yield ReadResourcesSequenceV2( - numberOfResources = resources.size, - resources = resources - ) + } yield apiResponse } /** - * Performs a count query for a search for resources by their rdfs:label. - * - * @param searchValue the values to search for. - * @param limitToProject limit search to given project. - * @param limitToResourceClass limit search to given resource class. - * @param requestingUser the the client making the request. - * @return a [[ReadResourcesSequenceV2]] representing the resources that have been found. - */ - private def searchResourcesByLabelCountV2(searchValue: String, limitToProject: Option[IRI], limitToResourceClass: Option[SmartIri], requestingUser: UserADM) = { + * Performs a count query for a search for resources by their rdfs:label. + * + * @param searchValue the values to search for. + * @param limitToProject limit search to given project. + * @param limitToResourceClass limit search to given resource class. + * @param requestingUser the the client making the request. + * @return a [[ReadResourcesSequenceV2]] representing the resources that have been found. + */ + private def searchResourcesByLabelCountV2(searchValue: String, limitToProject: Option[IRI], limitToResourceClass: Option[SmartIri], requestingUser: UserADM): Future[ResourceCountV2] = { val searchPhrase: MatchStringWhileTyping = MatchStringWhileTyping(searchValue) @@ -725,26 +687,24 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand count = countResponse.results.bindings.head.rowMap("count") - } yield ReadResourcesSequenceV2( - numberOfResources = count.toInt, - resources = Seq.empty[ReadResourceV2] // no results for a count query + } yield ResourceCountV2( + numberOfResources = count.toInt ) } - /** - * Performs a search for resources by their rdfs:label. - * - * @param searchValue the values to search for. - * @param offset the offset to be used for paging. - * @param limitToProject limit search to given project. - * @param limitToResourceClass limit search to given resource class. - * @param targetSchema the schema of the response. - * @param requestingUser the the client making the request. - * @return a [[ReadResourcesSequenceV2]] representing the resources that have been found. - */ + * Performs a search for resources by their rdfs:label. + * + * @param searchValue the values to search for. + * @param offset the offset to be used for paging. + * @param limitToProject limit search to given project. + * @param limitToResourceClass limit search to given resource class. + * @param targetSchema the schema of the response. + * @param requestingUser the the client making the request. + * @return a [[ReadResourcesSequenceV2]] representing the resources that have been found. + */ private def searchResourcesByLabelV2(searchValue: String, offset: Int, limitToProject: Option[IRI], @@ -793,26 +753,23 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand // _ = println(mainResourceIris.size) // separate resources and value objects - queryResultsSeparated = ConstructResponseUtilV2.splitMainResourcesAndValueRdfData(constructQueryResults = searchResourceByLabelResponse, requestingUser = requestingUser) + mainResourcesAndValueRdfData = ConstructResponseUtilV2.splitMainResourcesAndValueRdfData(constructQueryResults = searchResourceByLabelResponse, requestingUser = requestingUser) //_ = println(queryResultsSeparated) - resources <- ConstructResponseUtilV2.createApiResponse( - queryResults = queryResultsSeparated, + apiResponse: ReadResourcesSequenceV2 <- ConstructResponseUtilV2.createApiResponse( + mainResourcesAndValueRdfData = mainResourcesAndValueRdfData, orderByResourceIri = mainResourceIris.toSeq.sorted, queryStandoff = false, versionDate = None, + calculateMayHaveMoreResults = true, responderManager = responderManager, targetSchema = targetSchema, settings = settings, requestingUser = requestingUser ) - } yield ReadResourcesSequenceV2( - numberOfResources = queryResultsSeparated.size, - resources = resources - ) - + } yield apiResponse } } \ No newline at end of file diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala index 89ed684f11..46b8ab9c6c 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala @@ -41,7 +41,6 @@ import org.knora.webapi.messages.v2.responder.valuemessages._ import org.knora.webapi.responders.Responder.handleUnexpectedMessage import org.knora.webapi.responders.{IriLocker, Responder, ResponderData} import org.knora.webapi.twirl.{MappingElement, MappingStandoffDatatypeClass, MappingXMLAttribute} -import org.knora.webapi.util.ConstructResponseUtilV2.ResourceWithValueRdfData import org.knora.webapi.util.IriConversions._ import org.knora.webapi.util._ import org.knora.webapi.util.standoff.StandoffTagUtilV2 @@ -102,17 +101,18 @@ class StandoffResponderV2(responderData: ResponderData) extends Responder(respon // _ = println(s"Got a page of standoff in ${standoffPageEndTime - standoffPageStartTime} ms") // separate resources and values - queryResultsSeparated: Map[IRI, ResourceWithValueRdfData] = ConstructResponseUtilV2.splitMainResourcesAndValueRdfData(constructQueryResults = resourceRequestResponse, requestingUser = getStandoffRequestV2.requestingUser) + mainResourcesAndValueRdfData: ConstructResponseUtilV2.MainResourcesAndValueRdfData = ConstructResponseUtilV2.splitMainResourcesAndValueRdfData(constructQueryResults = resourceRequestResponse, requestingUser = getStandoffRequestV2.requestingUser) - _ = if (queryResultsSeparated.keySet != Set(getStandoffRequestV2.resourceIri)) { + _ = if (mainResourcesAndValueRdfData.resources.keySet != Set(getStandoffRequestV2.resourceIri)) { throw NotFoundException(s"Resource <${getStandoffRequestV2.resourceIri}> was not found (maybe you do not have permission to see it, or it is marked as deleted)") } - resources: Vector[ReadResourceV2] <- ConstructResponseUtilV2.createApiResponse( - queryResults = queryResultsSeparated, + readResourcesSequenceV2: ReadResourcesSequenceV2 <- ConstructResponseUtilV2.createApiResponse( + mainResourcesAndValueRdfData = mainResourcesAndValueRdfData, orderByResourceIri = Seq(getStandoffRequestV2.resourceIri), mappings = Map.empty, queryStandoff = false, + calculateMayHaveMoreResults = false, versionDate = None, responderManager = responderManager, targetSchema = getStandoffRequestV2.targetSchema, @@ -120,7 +120,7 @@ class StandoffResponderV2(responderData: ResponderData) extends Responder(respon requestingUser = getStandoffRequestV2.requestingUser ) - readResourceV2 = resources.headOption.getOrElse(throw NotFoundException(s"Resource <${getStandoffRequestV2.resourceIri}> not found")) + readResourceV2 = readResourcesSequenceV2.resources.headOption.getOrElse(throw NotFoundException(s"Resource <${getStandoffRequestV2.resourceIri}> not found")) valueObj: ReadValueV2 = readResourceV2.values.values.flatten.find(_.valueIri == getStandoffRequestV2.valueIri).getOrElse(throw NotFoundException(s"Value <${getStandoffRequestV2.valueIri}> not found in resource <${getStandoffRequestV2.resourceIri}> (maybe you do not have permission to see it, or it is marked as deleted)")) diff --git a/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala b/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala index b2b00afb0f..59b2bd16db 100644 --- a/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala @@ -37,7 +37,7 @@ import org.knora.webapi.messages.v2.responder.resourcemessages._ import org.knora.webapi.messages.v2.responder.standoffmessages.{GetRemainingStandoffFromTextValueRequestV2, GetStandoffResponseV2, MappingXMLtoStandoff, StandoffTagV2} import org.knora.webapi.messages.v2.responder.valuemessages._ import org.knora.webapi.util.IriConversions._ -import org.knora.webapi.util.PermissionUtilADM.{EntityPermission, ViewPermission} +import org.knora.webapi.util.PermissionUtilADM.EntityPermission import org.knora.webapi.util.date.{CalendarNameV2, DatePrecisionV2} import org.knora.webapi.util.standoff.StandoffTagUtilV2 @@ -280,31 +280,6 @@ object ConstructResponseUtilV2 { userPermission: Option[EntityPermission], valuePropertyAssertions: RdfPropertyValues) extends RdfData - - /** - * A [[ResourceWithValueRdfData]] representing a placeholder for a main resource that the user doesn't - * have permission to see. It is replaced by `ForbiddenResource` during processing. - */ - val ForbiddenMainResourcePlaceholder: ResourceWithValueRdfData = ResourceWithValueRdfData( - subjectIri = StringFormatter.ForbiddenResourceIri, - assertions = Map.empty, - isMainResource = true, - userPermission = Some(ViewPermission), - valuePropertyAssertions = Map.empty - ) - - /** - * A [[ResourceWithValueRdfData]] representing a placeholder for a dependent resource that the user doesn't - * have permission to see. It is replaced by `ForbiddenResource` during processing. - */ - val ForbiddenDependentResourcePlaceholder: ResourceWithValueRdfData = ResourceWithValueRdfData( - subjectIri = StringFormatter.ForbiddenResourceIri, - assertions = Map.empty, - isMainResource = false, - userPermission = Some(ViewPermission), - valuePropertyAssertions = Map.empty - ) - /** * Represents a mapping including information about the standoff entities. * May include a default XSL transformation. @@ -315,6 +290,8 @@ object ConstructResponseUtilV2 { */ case class MappingAndXSLTransformation(mapping: MappingXMLtoStandoff, standoffEntities: StandoffEntityInfoGetResponseV2, XSLTransformation: Option[String]) + case class MainResourcesAndValueRdfData(resources: RdfResources, hiddenResourceCount: Int) + /** * A [[SparqlConstructResponse]] may contain both resources and value RDF data objects as well as standoff. * This method turns a graph (i.e. triples) into a structure organized by the principle of resources and their values, i.e. a map of resource Iris to [[ResourceWithValueRdfData]]. @@ -323,7 +300,7 @@ object ConstructResponseUtilV2 { * @param constructQueryResults the results of a SPARQL construct query representing resources and their values. * @return a Map[resource IRI -> [[ResourceWithValueRdfData]]]. */ - def splitMainResourcesAndValueRdfData(constructQueryResults: SparqlExtendedConstructResponse, requestingUser: UserADM)(implicit stringFormatter: StringFormatter): RdfResources = { + def splitMainResourcesAndValueRdfData(constructQueryResults: SparqlExtendedConstructResponse, requestingUser: UserADM)(implicit stringFormatter: StringFormatter): MainResourcesAndValueRdfData = { // An intermediate data structure containing RDF assertions about an entity and the user's permission on the entity. case class RdfWithUserPermission(assertions: ConstructPredicateObjects, maybeUserPermission: Option[EntityPermission]) @@ -521,9 +498,9 @@ object ConstructResponseUtilV2 { case (resourceIri: IRI, resource: ResourceWithValueRdfData) if resource.isMainResource => resourceIri }.toSet - val mainResourceIrisNotVisible: Set[IRI] = hiddenResources.collect { + val mainResourceIrisNotVisibleCount: Int = hiddenResources.collect { case (resourceIri: IRI, resource: ResourceWithValueRdfData) if resource.isMainResource => resourceIri - }.toSet + }.toSet.size val dependentResourceIrisVisible: Set[IRI] = visibleResources.collect { case (resourceIri: IRI, resource: ResourceWithValueRdfData) if !resource.isMainResource => resourceIri @@ -590,36 +567,33 @@ object ConstructResponseUtilV2 { val transformedValuePropertyAssertions: RdfPropertyValues = resource.valuePropertyAssertions.map { case (propIri, values) => - val transformedValues = values.map { - value: ValueRdfData => + val transformedValues: Seq[ValueRdfData] = values.foldLeft(Vector.empty[ValueRdfData]) { + case (acc: Vector[ValueRdfData], value: ValueRdfData) => if (value.valueObjectClass.toString == OntologyConstants.KnoraBase.LinkValue) { val dependentResourceIri: IRI = value.requireIriObject(OntologyConstants.Rdf.Object.toSmartIri) if (alreadyTraversed(dependentResourceIri)) { - value + acc :+ value } else { // Do we have the dependent resource? if (dependentResourceIrisVisible.contains(dependentResourceIri)) { // Yes. Nest it in the link value. val dependentResource: ResourceWithValueRdfData = nestResources(dependentResourceIri, alreadyTraversed + resourceIri) - value.copy( + acc :+ value.copy( nestedResource = Some(dependentResource) ) } else if (dependentResourceIrisNotVisible.contains(dependentResourceIri)) { - // No, because the user doesn't have permission to see it. Nest a placeholder for - // ForbiddenResource in the link value. - value.copy( - nestedResource = Some(ForbiddenDependentResourcePlaceholder) - ) + // No, because the user doesn't have permission to see it. Skip the link value. + acc } else { // We don't have the dependent resource because it is marked as deleted. Just // return the link value without a nested resource. - value + acc :+ value } } } else { - value + acc :+ value } } @@ -693,11 +667,7 @@ object ConstructResponseUtilV2 { resourceIri -> transformedResource }.toMap - val forbiddenResourcesForHiddenMainResources: Map[IRI, ResourceWithValueRdfData] = mainResourceIrisNotVisible.map { - resourceIri => resourceIri -> ForbiddenMainResourcePlaceholder - }.toMap - - mainResourcesNested ++ forbiddenResourcesForHiddenMainResources + MainResourcesAndValueRdfData(resources = mainResourcesNested, hiddenResourceCount = mainResourceIrisNotVisibleCount) } /** @@ -908,32 +878,22 @@ object ConstructResponseUtilV2 { // Is there a nested resource in the link value? valueObject.nestedResource match { case Some(nestedResourceAssertions: ResourceWithValueRdfData) => - // Yes. Is the nested resource a placeholder for the forbidden resource? - if (nestedResourceAssertions.subjectIri == StringFormatter.ForbiddenResourceIri) { - // Yes. Replace it with the forbidden resource. - Future { - linkValue.copy( - nestedResource = Some(stringFormatter.forbiddenResource) - ) - } - } else { - // No. Construct a ReadResourceV2 representing the nested resource. - for { - nestedResource <- constructReadResourceV2( - resourceIri = referredResourceIri, - resourceWithValueRdfData = nestedResourceAssertions, - mappings = mappings, - queryStandoff = queryStandoff, - versionDate = versionDate, - responderManager = responderManager, - requestingUser = requestingUser, - targetSchema = targetSchema, - settings = settings - ) - } yield linkValue.copy( - nestedResource = Some(nestedResource) + // Yes. Construct a ReadResourceV2 representing the nested resource. + for { + nestedResource <- constructReadResourceV2( + resourceIri = referredResourceIri, + resourceWithValueRdfData = nestedResourceAssertions, + mappings = mappings, + queryStandoff = queryStandoff, + versionDate = versionDate, + responderManager = responderManager, + requestingUser = requestingUser, + targetSchema = targetSchema, + settings = settings ) - } + } yield linkValue.copy( + nestedResource = Some(nestedResource) + ) case None => // There is no nested resource. @@ -1159,153 +1119,147 @@ object ConstructResponseUtilV2 { } } - if (resourceWithValueRdfData.subjectIri == StringFormatter.ForbiddenResourceIri) { - FastFuture.successful(stringFormatter.forbiddenResource) - } else { - val resourceLabel: String = resourceWithValueRdfData.requireStringObject(OntologyConstants.Rdfs.Label.toSmartIri) - val resourceClassStr: IRI = resourceWithValueRdfData.requireIriObject(OntologyConstants.Rdf.Type.toSmartIri) - val resourceClass = resourceClassStr.toSmartIriWithErr(throw InconsistentTriplestoreDataException(s"Couldn't parse rdf:type of resource <$resourceIri>: <$resourceClassStr>")) - val resourceAttachedToUser: IRI = resourceWithValueRdfData.requireIriObject(OntologyConstants.KnoraBase.AttachedToUser.toSmartIri) - val resourceAttachedToProject: IRI = resourceWithValueRdfData.requireIriObject(OntologyConstants.KnoraBase.AttachedToProject.toSmartIri) - val resourcePermissions: String = resourceWithValueRdfData.requireStringObject(OntologyConstants.KnoraBase.HasPermissions.toSmartIri) - val resourceCreationDate: Instant = resourceWithValueRdfData.requireDateTimeObject(OntologyConstants.KnoraBase.CreationDate.toSmartIri) - val resourceLastModificationDate: Option[Instant] = resourceWithValueRdfData.maybeDateTimeObject(OntologyConstants.KnoraBase.LastModificationDate.toSmartIri) - val resourceDeletionInfo = getDeletionInfo(resourceWithValueRdfData) - - // get the resource's values - val valueObjectFutures: Map[SmartIri, Seq[Future[ReadValueV2]]] = resourceWithValueRdfData.valuePropertyAssertions.map { - case (property: SmartIri, valObjs: Seq[ValueRdfData]) => - val readValues: Seq[Future[ReadValueV2]] = valObjs.sortBy(_.subjectIri).sortBy { // order values by value IRI, then by knora-base:valueHasOrder - valObj: ValueRdfData => - // set order to zero if not given - valObj.maybeIntObject(OntologyConstants.KnoraBase.ValueHasOrder.toSmartIri).getOrElse(0) - }.map { - valObj: ValueRdfData => - for { - valueContent: ValueContentV2 <- createValueContentV2FromValueRdfData( - resourceIri = resourceIri, - valueObject = valObj, - mappings = mappings, - queryStandoff = queryStandoff, - responderManager = responderManager, - requestingUser = requestingUser, - targetSchema = targetSchema, - settings = settings + val resourceLabel: String = resourceWithValueRdfData.requireStringObject(OntologyConstants.Rdfs.Label.toSmartIri) + val resourceClassStr: IRI = resourceWithValueRdfData.requireIriObject(OntologyConstants.Rdf.Type.toSmartIri) + val resourceClass = resourceClassStr.toSmartIriWithErr(throw InconsistentTriplestoreDataException(s"Couldn't parse rdf:type of resource <$resourceIri>: <$resourceClassStr>")) + val resourceAttachedToUser: IRI = resourceWithValueRdfData.requireIriObject(OntologyConstants.KnoraBase.AttachedToUser.toSmartIri) + val resourceAttachedToProject: IRI = resourceWithValueRdfData.requireIriObject(OntologyConstants.KnoraBase.AttachedToProject.toSmartIri) + val resourcePermissions: String = resourceWithValueRdfData.requireStringObject(OntologyConstants.KnoraBase.HasPermissions.toSmartIri) + val resourceCreationDate: Instant = resourceWithValueRdfData.requireDateTimeObject(OntologyConstants.KnoraBase.CreationDate.toSmartIri) + val resourceLastModificationDate: Option[Instant] = resourceWithValueRdfData.maybeDateTimeObject(OntologyConstants.KnoraBase.LastModificationDate.toSmartIri) + val resourceDeletionInfo = getDeletionInfo(resourceWithValueRdfData) + + // get the resource's values + val valueObjectFutures: Map[SmartIri, Seq[Future[ReadValueV2]]] = resourceWithValueRdfData.valuePropertyAssertions.map { + case (property: SmartIri, valObjs: Seq[ValueRdfData]) => + val readValues: Seq[Future[ReadValueV2]] = valObjs.sortBy(_.subjectIri).sortBy { // order values by value IRI, then by knora-base:valueHasOrder + valObj: ValueRdfData => + // set order to zero if not given + valObj.maybeIntObject(OntologyConstants.KnoraBase.ValueHasOrder.toSmartIri).getOrElse(0) + }.map { + valObj: ValueRdfData => + for { + valueContent: ValueContentV2 <- createValueContentV2FromValueRdfData( + resourceIri = resourceIri, + valueObject = valObj, + mappings = mappings, + queryStandoff = queryStandoff, + responderManager = responderManager, + requestingUser = requestingUser, + targetSchema = targetSchema, + settings = settings + ) + + attachedToUser = valObj.requireIriObject(OntologyConstants.KnoraBase.AttachedToUser.toSmartIri) + permissions = valObj.requireStringObject(OntologyConstants.KnoraBase.HasPermissions.toSmartIri) + valueCreationDate: Instant = valObj.requireDateTimeObject(OntologyConstants.KnoraBase.ValueCreationDate.toSmartIri) + valueDeletionInfo = getDeletionInfo(valObj) + valueHasUUID: UUID = stringFormatter.decodeUuid(valObj.requireStringObject(OntologyConstants.KnoraBase.ValueHasUUID.toSmartIri)) + previousValueIri: Option[IRI] = valObj.maybeIriObject(OntologyConstants.KnoraBase.PreviousValue.toSmartIri) + + } yield valueContent match { + case linkValueContentV2: LinkValueContentV2 => + val valueHasRefCount: Int = valObj.requireIntObject(OntologyConstants.KnoraBase.ValueHasRefCount.toSmartIri) + + ReadLinkValueV2( + valueIri = valObj.subjectIri, + attachedToUser = attachedToUser, + permissions = permissions, + userPermission = valObj.userPermission, + valueCreationDate = valueCreationDate, + valueHasUUID = valueHasUUID, + valueContent = linkValueContentV2, + valueHasRefCount = valueHasRefCount, + previousValueIri = previousValueIri, + deletionInfo = valueDeletionInfo ) - attachedToUser = valObj.requireIriObject(OntologyConstants.KnoraBase.AttachedToUser.toSmartIri) - permissions = valObj.requireStringObject(OntologyConstants.KnoraBase.HasPermissions.toSmartIri) - valueCreationDate: Instant = valObj.requireDateTimeObject(OntologyConstants.KnoraBase.ValueCreationDate.toSmartIri) - valueDeletionInfo = getDeletionInfo(valObj) - valueHasUUID: UUID = stringFormatter.decodeUuid(valObj.requireStringObject(OntologyConstants.KnoraBase.ValueHasUUID.toSmartIri)) - previousValueIri: Option[IRI] = valObj.maybeIriObject(OntologyConstants.KnoraBase.PreviousValue.toSmartIri) - - } yield valueContent match { - case linkValueContentV2: LinkValueContentV2 => - val valueHasRefCount: Int = valObj.requireIntObject(OntologyConstants.KnoraBase.ValueHasRefCount.toSmartIri) - - ReadLinkValueV2( - valueIri = valObj.subjectIri, - attachedToUser = attachedToUser, - permissions = permissions, - userPermission = valObj.userPermission, - valueCreationDate = valueCreationDate, - valueHasUUID = valueHasUUID, - valueContent = linkValueContentV2, - valueHasRefCount = valueHasRefCount, - previousValueIri = previousValueIri, - deletionInfo = valueDeletionInfo - ) - - case textValueContentV2: TextValueContentV2 => - val maybeValueHasMaxStandoffStartIndex: Option[Int] = valObj.maybeIntObject(OntologyConstants.KnoraBase.ValueHasMaxStandoffStartIndex.toSmartIri) - - ReadTextValueV2( - valueIri = valObj.subjectIri, - attachedToUser = attachedToUser, - permissions = permissions, - userPermission = valObj.userPermission, - valueCreationDate = valueCreationDate, - valueHasUUID = valueHasUUID, - valueContent = textValueContentV2, - valueHasMaxStandoffStartIndex = maybeValueHasMaxStandoffStartIndex, - previousValueIri = previousValueIri, - deletionInfo = valueDeletionInfo - ) - - case otherValueContentV2: ValueContentV2 => - ReadOtherValueV2( - valueIri = valObj.subjectIri, - attachedToUser = attachedToUser, - permissions = permissions, - userPermission = valObj.userPermission, - valueCreationDate = valueCreationDate, - valueHasUUID = valueHasUUID, - valueContent = otherValueContentV2, - previousValueIri = previousValueIri, - deletionInfo = valueDeletionInfo - ) - } - } + case textValueContentV2: TextValueContentV2 => + val maybeValueHasMaxStandoffStartIndex: Option[Int] = valObj.maybeIntObject(OntologyConstants.KnoraBase.ValueHasMaxStandoffStartIndex.toSmartIri) + + ReadTextValueV2( + valueIri = valObj.subjectIri, + attachedToUser = attachedToUser, + permissions = permissions, + userPermission = valObj.userPermission, + valueCreationDate = valueCreationDate, + valueHasUUID = valueHasUUID, + valueContent = textValueContentV2, + valueHasMaxStandoffStartIndex = maybeValueHasMaxStandoffStartIndex, + previousValueIri = previousValueIri, + deletionInfo = valueDeletionInfo + ) - property -> readValues - } + case otherValueContentV2: ValueContentV2 => + ReadOtherValueV2( + valueIri = valObj.subjectIri, + attachedToUser = attachedToUser, + permissions = permissions, + userPermission = valObj.userPermission, + valueCreationDate = valueCreationDate, + valueHasUUID = valueHasUUID, + valueContent = otherValueContentV2, + previousValueIri = previousValueIri, + deletionInfo = valueDeletionInfo + ) + } + } - for { - projectResponse: ProjectGetResponseADM <- (responderManager ? ProjectGetRequestADM(ProjectIdentifierADM(maybeIri = Some(resourceAttachedToProject)), requestingUser = requestingUser)).mapTo[ProjectGetResponseADM] - valueObjects <- ActorUtil.sequenceSeqFuturesInMap(valueObjectFutures) - } yield ReadResourceV2( - resourceIri = resourceIri, - resourceClassIri = resourceClass, - label = resourceLabel, - attachedToUser = resourceAttachedToUser, - projectADM = projectResponse.project, - permissions = resourcePermissions, - userPermission = resourceWithValueRdfData.userPermission.get, - values = valueObjects, - creationDate = resourceCreationDate, - lastModificationDate = resourceLastModificationDate, - versionDate = versionDate, - deletionInfo = resourceDeletionInfo - ) + property -> readValues } + + for { + projectResponse: ProjectGetResponseADM <- (responderManager ? ProjectGetRequestADM(ProjectIdentifierADM(maybeIri = Some(resourceAttachedToProject)), requestingUser = requestingUser)).mapTo[ProjectGetResponseADM] + valueObjects <- ActorUtil.sequenceSeqFuturesInMap(valueObjectFutures) + } yield ReadResourceV2( + resourceIri = resourceIri, + resourceClassIri = resourceClass, + label = resourceLabel, + attachedToUser = resourceAttachedToUser, + projectADM = projectResponse.project, + permissions = resourcePermissions, + userPermission = resourceWithValueRdfData.userPermission.get, + values = valueObjects, + creationDate = resourceCreationDate, + lastModificationDate = resourceLastModificationDate, + versionDate = versionDate, + deletionInfo = resourceDeletionInfo + ) } /** * Creates an API response. * - * @param queryResults the query results. - * @param orderByResourceIri the order in which the resources should be returned. - * @param mappings the mappings to convert standoff to XML, if any. - * @param queryStandoff if `true`, make separate queries to get the standoff for text values. - * @param versionDate if defined, represents the requested time in the the resources' version history. - * @param responderManager the Knora responder manager. - * @param targetSchema the schema of response. - * @param settings the application's settings. - * @param requestingUser the user making the request. + * @param mainResourcesAndValueRdfData the query results. + * @param orderByResourceIri the order in which the resources should be returned. + * @param mappings the mappings to convert standoff to XML, if any. + * @param queryStandoff if `true`, make separate queries to get the standoff for text values. + * @param versionDate if defined, represents the requested time in the the resources' version history. + * @param responderManager the Knora responder manager. + * @param targetSchema the schema of response. + * @param settings the application's settings. + * @param requestingUser the user making the request. * @return a collection of [[ReadResourceV2]] representing the search results. */ - def createApiResponse(queryResults: RdfResources, + def createApiResponse(mainResourcesAndValueRdfData: MainResourcesAndValueRdfData, orderByResourceIri: Seq[IRI], mappings: Map[IRI, MappingAndXSLTransformation] = Map.empty[IRI, MappingAndXSLTransformation], queryStandoff: Boolean, + calculateMayHaveMoreResults: Boolean, versionDate: Option[Instant], responderManager: ActorRef, targetSchema: ApiV2Schema, settings: SettingsImpl, - requestingUser: UserADM)(implicit stringFormatter: StringFormatter, timeout: Timeout, executionContext: ExecutionContext): Future[Vector[ReadResourceV2]] = { + requestingUser: UserADM)(implicit stringFormatter: StringFormatter, timeout: Timeout, executionContext: ExecutionContext): Future[ReadResourcesSequenceV2] = { + + val visibleResourceIris: Seq[IRI] = orderByResourceIri.filter(resourceIri => mainResourcesAndValueRdfData.resources.keySet.contains(resourceIri)) // iterate over orderByResourceIris and construct the response in the correct order - val readResourceFutures: Vector[Future[ReadResourceV2]] = orderByResourceIri.map { + val readResourceFutures: Vector[Future[ReadResourceV2]] = visibleResourceIris.map { resourceIri: IRI => - val resource: ResourceWithValueRdfData = queryResults(resourceIri) - - // If the user doesn't have permission to see the resource, its IRI will be mapped - // to ForbiddenMainResourcePlaceholder. Therefore use the IRI from the ResourceWithValueRdfData, - // not the actual resource IRI. constructReadResourceV2( - resourceIri = resource.subjectIri, - resourceWithValueRdfData = resource, + resourceIri = resourceIri, + resourceWithValueRdfData = mainResourcesAndValueRdfData.resources(resourceIri), mappings = mappings, queryStandoff = queryStandoff, versionDate = None, @@ -1317,6 +1271,10 @@ object ConstructResponseUtilV2 { }.toVector - Future.sequence(readResourceFutures) + for { + resources <- Future.sequence(readResourceFutures) + mayHaveMoreResults = calculateMayHaveMoreResults && + settings.maxResultsPerSearchResultPage > (visibleResourceIris.size + mainResourcesAndValueRdfData.hiddenResourceCount) + } yield ReadResourcesSequenceV2(resources = resources, mayHaveMoreResults = mayHaveMoreResults) } } \ No newline at end of file diff --git a/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala b/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala index a202c3ab9b..fe642971a8 100644 --- a/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala +++ b/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala @@ -173,11 +173,6 @@ object StringFormatter { */ val ClientCollectionEntityNameStart: String = "#" + ClientCollectionTypeKeyword - /** - * The IRI of the singleton instance of `ForbiddenResource`. - */ - val ForbiddenResourceIri: IRI = s"http://$IriDomain/0000/forbiddenResource" - /** * A container for an XML import namespace and its prefix label. * @@ -895,24 +890,6 @@ class StringFormatter private(val maybeSettings: Option[SettingsImpl] = None, ma ontologySchema = None ) - /** - * The singleton instance of `knora-base:ForbiddenResource`. - */ - val forbiddenResource: ReadResourceV2 = ReadResourceV2( - resourceIri = StringFormatter.ForbiddenResourceIri, - label = "This resource is a proxy for a resource you are not allowed to see", - resourceClassIri = toSmartIri(OntologyConstants.KnoraBase.ForbiddenResource), - attachedToUser = SharedTestDataADM.rootUser.id, - projectADM = SharedTestDataADM.systemProject, - permissions = "V knora-admin:UnknownUser", - userPermission = PermissionUtilADM.ViewPermission, - values = Map.empty, - creationDate = Instant.parse("2017-10-06T11:05:37Z"), - lastModificationDate = None, - versionDate = None, - deletionInfo = None - ) - /** * The implementation of [[SmartIri]]. An instance of this class can only be constructed by [[StringFormatter]]. * The constructor validates and parses the IRI. diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala index 8eb56df69d..d347d564a1 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala @@ -65,7 +65,7 @@ object ResourcesResponderV2Spec { class GraphTestData { private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - val graphForAnythingUser1 = GraphDataGetResponseV2( + val graphForAnythingUser1: GraphDataGetResponseV2 = GraphDataGetResponseV2( edges = Vector( GraphEdgeV2( target = "http://rdfh.ch/0001/tPfZeNMvRVujCQqbIbvO0A", @@ -223,7 +223,7 @@ class GraphTestData { ontologySchema = InternalSchema ) - val graphForIncunabulaUser = GraphDataGetResponseV2( + val graphForIncunabulaUser: GraphDataGetResponseV2 = GraphDataGetResponseV2( edges = Vector( GraphEdgeV2( target = "http://rdfh.ch/0001/tPfZeNMvRVujCQqbIbvO0A", @@ -351,7 +351,7 @@ class GraphTestData { ontologySchema = InternalSchema ) - val graphWithStandoffLink = GraphDataGetResponseV2( + val graphWithStandoffLink: GraphDataGetResponseV2 = GraphDataGetResponseV2( edges = Vector(GraphEdgeV2( target = "http://rdfh.ch/0001/a-thing", propertyIri = "http://www.knora.org/ontology/knora-base#hasStandoffLinkTo".toSmartIri, @@ -372,7 +372,7 @@ class GraphTestData { ontologySchema = InternalSchema ) - val graphWithOneNode = GraphDataGetResponseV2( + val graphWithOneNode: GraphDataGetResponseV2 = GraphDataGetResponseV2( edges = Nil, nodes = Vector(GraphNodeV2( resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri, @@ -468,31 +468,8 @@ class ResourcesResponderV2Spec extends CoreSpec() with ImplicitSender { responderManager ! ResourcesGetRequestV2(resourceIris = Seq(resourceIri), targetSchema = ApiV2Complex, requestingUser = anythingUserProfile) expectMsgPF(timeout) { - case response: ReadResourcesSequenceV2 => - resourcesSequenceToResource( - requestedresourceIri = resourceIri, - readResourcesSequence = response, - requestingUser = anythingUserProfile - ) - } - } - - private def resourcesSequenceToResource(requestedresourceIri: IRI, readResourcesSequence: ReadResourcesSequenceV2, requestingUser: UserADM): ReadResourceV2 = { - if (readResourcesSequence.numberOfResources == 0) { - throw AssertionException(s"Expected one resource, <$requestedresourceIri>, but no resources were returned") - } - - if (readResourcesSequence.numberOfResources > 1) { - throw AssertionException(s"More than one resource returned with IRI <$requestedresourceIri>") + case response: ReadResourcesSequenceV2 => response.toResource(resourceIri).toOntologySchema(ApiV2Complex) } - - val resourceInfo = readResourcesSequence.resources.head - - if (resourceInfo.resourceIri == StringFormatter.ForbiddenResourceIri) { - throw ForbiddenException(s"User ${requestingUser.email} does not have permission to view resource <${resourceInfo.resourceIri}>") - } - - resourceInfo.toOntologySchema(ApiV2Complex) } private def checkCreateResource(inputResource: CreateResourceV2, @@ -860,11 +837,7 @@ class ResourcesResponderV2Spec extends CoreSpec() with ImplicitSender { expectMsgPF(timeout) { case response: ReadResourcesSequenceV2 => - val outputResource: ReadResourceV2 = resourcesSequenceToResource( - requestedresourceIri = resourceIri, - readResourcesSequence = response, - requestingUser = anythingUserProfile - ) + val outputResource: ReadResourceV2 = response.toResource(resourceIri).toOntologySchema(ApiV2Complex) checkCreateResource( inputResource = inputResource, @@ -2172,8 +2145,7 @@ class ResourcesResponderV2Spec extends CoreSpec() with ImplicitSender { val isEntityUsedSparql: String = queries.sparql.v2.txt.isEntityUsed( triplestore = settings.triplestoreType, entityIri = resourceIriToErase.get.toSmartIri, - ignoreKnoraConstraints = true, - ignoreRdfSubjectAndObject = false + ignoreKnoraConstraints = true ).toString() storeManager ! SparqlSelectRequest(isEntityUsedSparql) diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2SpecFullData.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2SpecFullData.scala index de2af6af48..4dbbf97faa 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2SpecFullData.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2SpecFullData.scala @@ -12,7 +12,7 @@ import org.knora.webapi.{InternalSchema, SharedTestDataADM} class ResourcesResponderV2SpecFullData(implicit stringFormatter: StringFormatter) { - val expectedReadResourceV2ForZeitgloecklein = ReadResourceV2( + val expectedReadResourceV2ForZeitgloecklein: ReadResourceV2 = ReadResourceV2( label = "Zeitgl\u00F6cklein des Lebens und Leidens Christi", resourceIri = "http://rdfh.ch/0803/c5058f3a", permissions = "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|RV knora-admin:UnknownUser", @@ -219,7 +219,7 @@ class ResourcesResponderV2SpecFullData(implicit stringFormatter: StringFormatter deletionInfo = None ) - val expectedReadResourceV2ForZeitgloeckleinPreview = ReadResourceV2( + val expectedReadResourceV2ForZeitgloeckleinPreview: ReadResourceV2 = ReadResourceV2( label = "Zeitgl\u00F6cklein des Lebens und Leidens Christi", resourceIri = "http://rdfh.ch/0803/c5058f3a", permissions = "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|RV knora-admin:UnknownUser", @@ -234,7 +234,7 @@ class ResourcesResponderV2SpecFullData(implicit stringFormatter: StringFormatter deletionInfo = None ) - val expectedReadResourceV2ForReiseInsHeiligeland = ReadResourceV2( + val expectedReadResourceV2ForReiseInsHeiligeland: ReadResourceV2 = ReadResourceV2( label = "Reise ins Heilige Land", resourceIri = "http://rdfh.ch/0803/2a6221216701", permissions = "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|RV knora-admin:UnknownUser", @@ -683,7 +683,7 @@ class ResourcesResponderV2SpecFullData(implicit stringFormatter: StringFormatter deletionInfo = None ) - val expectedReadResourceV2ForReiseInsHeiligelandPreview = ReadResourceV2( + val expectedReadResourceV2ForReiseInsHeiligelandPreview: ReadResourceV2 = ReadResourceV2( resourceClassIri = "http://www.knora.org/ontology/0803/incunabula#book".toSmartIri, label = "Reise ins Heilige Land", creationDate = Instant.parse("2016-03-02T15:05:21Z"), @@ -698,22 +698,19 @@ class ResourcesResponderV2SpecFullData(implicit stringFormatter: StringFormatter deletionInfo = None ) - val expectedFullResourceResponseForZeitgloecklein = ReadResourcesSequenceV2( + val expectedFullResourceResponseForZeitgloecklein: ReadResourcesSequenceV2 = ReadResourcesSequenceV2( resources = Vector(expectedReadResourceV2ForZeitgloecklein), - numberOfResources = 1 ) - val expectedPreviewResourceResponseForZeitgloecklein = ReadResourcesSequenceV2( + val expectedPreviewResourceResponseForZeitgloecklein: ReadResourcesSequenceV2 = ReadResourcesSequenceV2( resources = Vector(expectedReadResourceV2ForZeitgloeckleinPreview), - numberOfResources = 1 ) - val expectedFullResourceResponseForReise = ReadResourcesSequenceV2( + val expectedFullResourceResponseForReise: ReadResourcesSequenceV2 = ReadResourcesSequenceV2( resources = Vector(expectedReadResourceV2ForReiseInsHeiligeland), - numberOfResources = 1 ) - val expectedFullResourceResponseForThingWithHistory = ReadResourcesSequenceV2( + val expectedFullResourceResponseForThingWithHistory: ReadResourcesSequenceV2 = ReadResourcesSequenceV2( resources = Vector( ReadResourceV2( versionDate = Some(Instant.parse("2019-02-12T08:05:10Z")), @@ -779,11 +776,10 @@ class ResourcesResponderV2SpecFullData(implicit stringFormatter: StringFormatter lastModificationDate = Some(Instant.parse("2019-02-13T09:05:10Z")), deletionInfo = None ) - ), - numberOfResources = 1 + ) ) - val expectedCompleteVersionHistoryResponse = ResourceVersionHistoryResponseV2(history = Vector( + val expectedCompleteVersionHistoryResponse: ResourceVersionHistoryResponseV2 = ResourceVersionHistoryResponseV2(history = Vector( ResourceHistoryEntry( versionDate = Instant.parse("2019-02-13T09:05:10Z"), author = "http://rdfh.ch/users/BhkfBc3hTeS_IDo-JgXRbQ" @@ -822,7 +818,7 @@ class ResourcesResponderV2SpecFullData(implicit stringFormatter: StringFormatter ) )) - val expectedPartialVersionHistoryResponse = ResourceVersionHistoryResponseV2(history = Vector( + val expectedPartialVersionHistoryResponse: ResourceVersionHistoryResponseV2 = ResourceVersionHistoryResponseV2(history = Vector( ResourceHistoryEntry( versionDate = Instant.parse("2019-02-13T09:00:10Z"), author = "http://rdfh.ch/users/9XBCrDV3SRa7kS1WwynB4Q" @@ -853,23 +849,19 @@ class ResourcesResponderV2SpecFullData(implicit stringFormatter: StringFormatter ) )) - val expectedFullResourceResponseForZeitgloeckleinAndReise = ReadResourcesSequenceV2( - resources = Vector(expectedReadResourceV2ForZeitgloecklein, expectedReadResourceV2ForReiseInsHeiligeland), - numberOfResources = 2 + val expectedFullResourceResponseForZeitgloeckleinAndReise: ReadResourcesSequenceV2 = ReadResourcesSequenceV2( + resources = Vector(expectedReadResourceV2ForZeitgloecklein, expectedReadResourceV2ForReiseInsHeiligeland) ) - val expectedPreviewResourceResponseForZeitgloeckleinAndReise = ReadResourcesSequenceV2( - resources = Vector(expectedReadResourceV2ForZeitgloeckleinPreview, expectedReadResourceV2ForReiseInsHeiligelandPreview), - numberOfResources = 2 + val expectedPreviewResourceResponseForZeitgloeckleinAndReise: ReadResourcesSequenceV2 = ReadResourcesSequenceV2( + resources = Vector(expectedReadResourceV2ForZeitgloeckleinPreview, expectedReadResourceV2ForReiseInsHeiligelandPreview) ) - val expectedFullResourceResponseForReiseAndZeitgloeckleinInversedOrder = ReadResourcesSequenceV2( - resources = Vector(expectedReadResourceV2ForReiseInsHeiligeland, expectedReadResourceV2ForZeitgloecklein), - numberOfResources = 2 + val expectedFullResourceResponseForReiseAndZeitgloeckleinInversedOrder: ReadResourcesSequenceV2 = ReadResourcesSequenceV2( + resources = Vector(expectedReadResourceV2ForReiseInsHeiligeland, expectedReadResourceV2ForZeitgloecklein) ) - val expectedFullResponseResponseForThingWithValueByUuid = ReadResourcesSequenceV2( - numberOfResources = 1, + val expectedFullResponseResponseForThingWithValueByUuid: ReadResourcesSequenceV2 = ReadResourcesSequenceV2( resources = Vector(ReadResourceV2( versionDate = None, label = "A thing with version history", @@ -902,8 +894,7 @@ class ResourcesResponderV2SpecFullData(implicit stringFormatter: StringFormatter )) ) - val expectedFullResponseResponseForThingWithValueByUuidAndVersionDate = ReadResourcesSequenceV2( - numberOfResources = 1, + val expectedFullResponseResponseForThingWithValueByUuidAndVersionDate: ReadResourcesSequenceV2 = ReadResourcesSequenceV2( resources = Vector(ReadResourceV2( versionDate = None, label = "A thing with version history", diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponseCheckerV2.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponseCheckerV2.scala index 331687d0c9..e04b7cbacb 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponseCheckerV2.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponseCheckerV2.scala @@ -32,7 +32,7 @@ object ResourcesResponseCheckerV2 { * @param expected the expected response. */ def compareReadResourcesSequenceV2Response(expected: ReadResourcesSequenceV2, received: ReadResourcesSequenceV2): Unit = { - assert(expected.numberOfResources == received.numberOfResources, "number of resources is not equal") + assert(expected.resources.size == received.resources.size, "number of resources is not equal") assert(expected.resources.size == received.resources.size, "number of resources are not equal") // compare the resources one by one: resources have to returned in the correct order diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponseCheckerV2SpecFullData.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponseCheckerV2SpecFullData.scala index b30299a0f8..665b81bd3d 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponseCheckerV2SpecFullData.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponseCheckerV2SpecFullData.scala @@ -13,7 +13,7 @@ import org.knora.webapi.{InternalSchema, SharedTestDataADM} class ResourcesResponseCheckerV2SpecFullData(implicit stringFormatter: StringFormatter) { // one title is missing - val expectedReadResourceV2ForReiseInsHeiligelandWrong = ReadResourceV2( + val expectedReadResourceV2ForReiseInsHeiligelandWrong: ReadResourceV2 = ReadResourceV2( label = "Reise ins Heilige Land", resourceIri = "http://rdfh.ch/2a6221216701", permissions = "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|RV knora-admin:UnknownUser", @@ -445,9 +445,8 @@ class ResourcesResponseCheckerV2SpecFullData(implicit stringFormatter: StringFor deletionInfo = None ) - val expectedFullResourceResponseForReiseWrong = ReadResourcesSequenceV2( - resources = Vector(expectedReadResourceV2ForReiseInsHeiligelandWrong), - numberOfResources = 1 + val expectedFullResourceResponseForReiseWrong: ReadResourcesSequenceV2 = ReadResourcesSequenceV2( + resources = Vector(expectedReadResourceV2ForReiseInsHeiligelandWrong) ) } diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/SearchResponderV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/SearchResponderV2Spec.scala index 6a947fea21..fe2a13750f 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/SearchResponderV2Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/SearchResponderV2Spec.scala @@ -123,7 +123,7 @@ class SearchResponderV2Spec extends CoreSpec() with ImplicitSender { expectMsgPF(timeout) { case response: ReadResourcesSequenceV2 => // TODO: do better testing once JSON-LD can be converted back into case classes - assert(response.numberOfResources == 18, s"18 books were expected, but ${response.numberOfResources} given.") + assert(response.resources.size == 18, s"18 books were expected, but ${response.resources.size} given.") } } @@ -141,7 +141,7 @@ class SearchResponderV2Spec extends CoreSpec() with ImplicitSender { expectMsgPF(timeout) { case response: ReadResourcesSequenceV2 => - assert(response.numberOfResources == 3, s"3 results were expected, but ${response.numberOfResources} given") + assert(response.resources.size == 3, s"3 results were expected, but ${response.resources.size} given") } } @@ -159,7 +159,7 @@ class SearchResponderV2Spec extends CoreSpec() with ImplicitSender { expectMsgPF(timeout) { case response: ReadResourcesSequenceV2 => - assert(response.numberOfResources == 3, s"3 results were expected, but ${response.numberOfResources} given") + assert(response.resources.size == 3, s"3 results were expected, but ${response.resources.size} given") } } @@ -175,7 +175,7 @@ class SearchResponderV2Spec extends CoreSpec() with ImplicitSender { expectMsgPF(timeout) { case response: ReadResourcesSequenceV2 => - assert(response.numberOfResources == 3, s"3 results were expected, but ${response.numberOfResources} given") + assert(response.resources.size == 3, s"3 results were expected, but ${response.resources.size} given") } } @@ -191,7 +191,7 @@ class SearchResponderV2Spec extends CoreSpec() with ImplicitSender { expectMsgPF(timeout) { case response: ReadResourcesSequenceV2 => - assert(response.numberOfResources == 3, s"3 results were expected, but ${response.numberOfResources} given") + assert(response.resources.size == 3, s"3 results were expected, but ${response.resources.size} given") } } @@ -208,7 +208,7 @@ class SearchResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case response: ReadResourcesSequenceV2 => response.numberOfResources should ===(19) + case response: ReadResourcesSequenceV2 => response.resources.size should ===(19) } } diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala index e4ee785520..928a77b524 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala @@ -178,13 +178,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case searchResponse: ReadResourcesSequenceV2 => - // Get the resource from the response. - resourcesSequenceToResource( - requestedresourceIri = resourceIri, - readResourcesSequence = searchResponse, - requestingUser = requestingUser - ) + case searchResponse: ReadResourcesSequenceV2 => searchResponse.toResource(resourceIri).toOntologySchema(ApiV2Complex) } } @@ -266,35 +260,12 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) } - private def resourcesSequenceToResource(requestedresourceIri: IRI, readResourcesSequence: ReadResourcesSequenceV2, requestingUser: UserADM): ReadResourceV2 = { - if (readResourcesSequence.numberOfResources == 0) { - throw AssertionException(s"Expected one resource, <$requestedresourceIri>, but no resources were returned") - } - - if (readResourcesSequence.numberOfResources > 1) { - throw AssertionException(s"More than one resource returned with IRI <$requestedresourceIri>") - } - - val resourceInfo = readResourcesSequence.resources.head - - if (resourceInfo.resourceIri == StringFormatter.ForbiddenResourceIri) { - throw ForbiddenException(s"User ${requestingUser.email} does not have permission to view resource <${resourceInfo.resourceIri}>") - } - - resourceInfo.toOntologySchema(ApiV2Complex) - } - private def getResourceLastModificationDate(resourceIri: IRI, requestingUser: UserADM): Option[Instant] = { responderManager ! ResourcesPreviewGetRequestV2(resourceIris = Seq(resourceIri), targetSchema = ApiV2Complex, requestingUser = requestingUser) expectMsgPF(timeout) { case previewResponse: ReadResourcesSequenceV2 => - val resourcePreview: ReadResourceV2 = resourcesSequenceToResource( - requestedresourceIri = resourceIri, - readResourcesSequence = previewResponse, - requestingUser = requestingUser - ) - + val resourcePreview: ReadResourceV2 = previewResponse.toResource(resourceIri) resourcePreview.lastModificationDate } } diff --git a/webapi/src/test/scala/org/knora/webapi/util/ConstructResponseUtilV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/util/ConstructResponseUtilV2Spec.scala index d96abcaae5..a618b7bb81 100644 --- a/webapi/src/test/scala/org/knora/webapi/util/ConstructResponseUtilV2Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/util/ConstructResponseUtilV2Spec.scala @@ -47,23 +47,30 @@ class ConstructResponseUtilV2Spec extends CoreSpec() with ImplicitSender { val resourceIri: IRI = "http://rdfh.ch/0803/c5058f3a" val turtleStr: String = FileUtil.readTextFile(new File("src/test/resources/test-data/constructResponseUtilV2/Zeitglöcklein.ttl")) val resourceRequestResponse: SparqlExtendedConstructResponse = SparqlExtendedConstructResponse.parseTurtleResponse(turtleStr, log).get - val queryResultsSeparated: RdfResources = ConstructResponseUtilV2.splitMainResourcesAndValueRdfData(constructQueryResults = resourceRequestResponse, requestingUser = incunabulaUser) + val mainResourcesAndValueRdfData: ConstructResponseUtilV2.MainResourcesAndValueRdfData = ConstructResponseUtilV2.splitMainResourcesAndValueRdfData( + constructQueryResults = resourceRequestResponse, + requestingUser = incunabulaUser + ) - val resourcesFuture: Future[Vector[ReadResourceV2]] = ConstructResponseUtilV2.createApiResponse( - queryResults = queryResultsSeparated, + val apiResponseFuture: Future[ReadResourcesSequenceV2] = ConstructResponseUtilV2.createApiResponse( + mainResourcesAndValueRdfData = mainResourcesAndValueRdfData, orderByResourceIri = Seq(resourceIri), mappings = Map.empty, queryStandoff = false, versionDate = None, + calculateMayHaveMoreResults = false, responderManager = responderManager, targetSchema = ApiV2Complex, settings = settings, requestingUser = incunabulaUser ) - val resources: Vector[ReadResourceV2] = Await.result(resourcesFuture, 10.seconds) - val resourceSequence = ReadResourcesSequenceV2(numberOfResources = 1, resources = resources) - ResourcesResponseCheckerV2.compareReadResourcesSequenceV2Response(expected = resourcesResponderV2SpecFullData.expectedFullResourceResponseForZeitgloecklein, received = resourceSequence) + val resourceSequence: ReadResourcesSequenceV2 = Await.result(apiResponseFuture, 10.seconds) + + ResourcesResponseCheckerV2.compareReadResourcesSequenceV2Response( + expected = resourcesResponderV2SpecFullData.expectedFullResourceResponseForZeitgloecklein, + received = resourceSequence + ) } } } From c25808ed070eaad1684e49827cf5b02cb89c81fe Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Wed, 4 Mar 2020 17:05:44 +0100 Subject: [PATCH 09/23] test(api-v2): Fix compile error. --- .../v2/SearchResponderV2SpecFullData.scala | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/SearchResponderV2SpecFullData.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/SearchResponderV2SpecFullData.scala index a222728c03..5011c2613f 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/SearchResponderV2SpecFullData.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/SearchResponderV2SpecFullData.scala @@ -17,8 +17,7 @@ class SearchResponderV2SpecFullData(implicit stringFormatter: StringFormatter) { implicit lazy val system: ActorSystem = ActorSystem("webapi") - val fulltextSearchForNarr = ReadResourcesSequenceV2( - numberOfResources = 25, + val fulltextSearchForNarr: ReadResourcesSequenceV2 = ReadResourcesSequenceV2( resources = Vector( ReadResourceV2( label = "p7v", @@ -773,8 +772,7 @@ class SearchResponderV2SpecFullData(implicit stringFormatter: StringFormatter) { ) ) - val fulltextSearchForDinge = ReadResourcesSequenceV2( - numberOfResources = 1, + val fulltextSearchForDinge: ReadResourcesSequenceV2 = ReadResourcesSequenceV2( resources = Vector(ReadResourceV2( label = "Ein Ding f\u00FCr jemanden, dem die Dinge gefallen", resourceIri = "http://rdfh.ch/0001/a-thing-with-text-values", @@ -1465,7 +1463,7 @@ class SearchResponderV2SpecFullData(implicit stringFormatter: StringFormatter) { ) // Dear Ben: I am aware of the fact that this code is not formatted properly and I know that this deeply disturbs you. But please leave it like this since otherwise I cannot possibly read and understand this query. - val constructQueryForBooksWithTitleZeitgloecklein = ConstructQuery( + val constructQueryForBooksWithTitleZeitgloecklein: ConstructQuery = ConstructQuery( constructClause = ConstructClause( statements = Vector( StatementPattern(QueryVariable("book"), IriRef("http://api.knora.org/ontology/knora-api/simple/v2#isMainResource".toSmartIri, None), XsdLiteral("true", "http://www.w3.org/2001/XMLSchema#boolean".toSmartIri), None), @@ -1496,8 +1494,7 @@ class SearchResponderV2SpecFullData(implicit stringFormatter: StringFormatter) { querySchema = Some(ApiV2Simple) ) - val booksWithTitleZeitgloeckleinResponse = ReadResourcesSequenceV2( - numberOfResources = 2, + val booksWithTitleZeitgloeckleinResponse: ReadResourcesSequenceV2 = ReadResourcesSequenceV2( resources = Vector( ReadResourceV2( label = "Zeitgl\u00F6cklein des Lebens und Leidens Christi", @@ -1563,7 +1560,7 @@ class SearchResponderV2SpecFullData(implicit stringFormatter: StringFormatter) { ) // Dear Ben: please see my comment above - val constructQueryForBooksWithoutTitleZeitgloecklein = ConstructQuery( + val constructQueryForBooksWithoutTitleZeitgloecklein: ConstructQuery = ConstructQuery( constructClause = ConstructClause( statements = Vector( StatementPattern(QueryVariable("book"), IriRef("http://api.knora.org/ontology/knora-api/simple/v2#isMainResource".toSmartIri, None), XsdLiteral("true", "http://www.w3.org/2001/XMLSchema#boolean".toSmartIri), None), From d8d1c3864c6a936f8b5fdfc655d4fac0fd3ed6fb Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Thu, 5 Mar 2020 15:12:16 +0100 Subject: [PATCH 10/23] feat(api-v2): Get rid of ForbiddenResource (ongoing). --- .../scala/org/knora/webapi/Settings.scala | 2 +- ...aseToApiV2ComplexTransformationRules.scala | 21 + ...BaseToApiV2SimpleTransformationRules.scala | 21 + .../resourcemessages/ResourceMessagesV2.scala | 17 +- .../responders/v2/ResourcesResponderV2.scala | 52 +- .../responders/v2/SearchResponderV2.scala | 13 +- .../webapi/util/ConstructResponseUtilV2.scala | 42 +- .../knoraApiOntologySimple.jsonld | 52 +- .../ontologyR2RV2/knoraApiOntologySimple.rdf | 352 +- .../ontologyR2RV2/knoraApiOntologySimple.ttl | 28 +- .../knoraApiOntologyWithValueObjects.jsonld | 142 +- .../knoraApiOntologyWithValueObjects.rdf | 2952 ++++++++--------- .../knoraApiOntologyWithValueObjects.ttl | 82 +- .../searchR2RV2/NarrFulltextSearch.jsonld | 1 + .../PagesOfNarrenschiffOrderedBySeqnum.jsonld | 1 + ...rrenschiffOrderedBySeqnumNextOffset.jsonld | 1 + .../ThingWithBooleanOptionalOffset0.jsonld | 1 + .../searchR2RV2/ThingWithHiddenThing.jsonld | 51 +- ...ingsWithOptionalDecimalGreaterThan1.jsonld | 97 +- ...> searchResponseWithHiddenResource.jsonld} | 24 - .../thingWithOptionalDateSortedDesc.jsonld | 73 +- .../webapi/e2e/v2/SearchRouteV2R2RSpec.scala | 2 +- .../responders/v2/SearchResponderV2Spec.scala | 8 +- .../responders/v2/ValuesResponderV2Spec.scala | 129 +- 24 files changed, 1810 insertions(+), 2354 deletions(-) rename webapi/src/test/resources/test-data/searchR2RV2/{searchResponseWithforbiddenResource.jsonld => searchResponseWithHiddenResource.jsonld} (91%) diff --git a/webapi/src/main/scala/org/knora/webapi/Settings.scala b/webapi/src/main/scala/org/knora/webapi/Settings.scala index 3b2be6b05f..03425102c4 100644 --- a/webapi/src/main/scala/org/knora/webapi/Settings.scala +++ b/webapi/src/main/scala/org/knora/webapi/Settings.scala @@ -220,7 +220,7 @@ class SettingsImpl(config: Config) extends Extension { case _ ⇒ throw new ConfigurationException(s"Config setting '$path' must be a finite duration") } - val prometheusEndpoint = config.getBoolean("app.monitoring.prometheus-endpoint") + val prometheusEndpoint: Boolean = config.getBoolean("app.monitoring.prometheus-endpoint") } diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/KnoraBaseToApiV2ComplexTransformationRules.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/KnoraBaseToApiV2ComplexTransformationRules.scala index 5b931ce0a9..4195c8ae69 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/KnoraBaseToApiV2ComplexTransformationRules.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/KnoraBaseToApiV2ComplexTransformationRules.scala @@ -86,6 +86,26 @@ object KnoraBaseToApiV2ComplexTransformationRules extends OntologyTransformation objectType = Some(OntologyConstants.Xsd.String) ) + private val MayHaveMoreResults: ReadPropertyInfoV2 = makeProperty( + propertyIri = OntologyConstants.KnoraApiV2Complex.MayHaveMoreResults, + propertyType = OntologyConstants.Owl.DatatypeProperty, + predicates = Seq( + makePredicate( + predicateIri = OntologyConstants.Rdfs.Label, + objectsWithLang = Map( + LanguageCodes.EN -> "May have more results" + ) + ), + makePredicate( + predicateIri = OntologyConstants.Rdfs.Comment, + objectsWithLang = Map( + LanguageCodes.EN -> "Indicates whether more results may be available for a search query" + ) + ) + ), + objectType = Some(OntologyConstants.Xsd.Boolean) + ) + private val UserHasPermission: ReadPropertyInfoV2 = makeProperty( propertyIri = OntologyConstants.KnoraApiV2Complex.UserHasPermission, propertyType = OntologyConstants.Owl.DatatypeProperty, @@ -1764,6 +1784,7 @@ object KnoraBaseToApiV2ComplexTransformationRules extends OntologyTransformation override val externalPropertiesToAdd: Map[SmartIri, ReadPropertyInfoV2] = Set( Label, Result, + MayHaveMoreResults, Error, UserHasPermission, VersionDate, diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/KnoraBaseToApiV2SimpleTransformationRules.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/KnoraBaseToApiV2SimpleTransformationRules.scala index 911a3fbebb..32ca2b7c07 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/KnoraBaseToApiV2SimpleTransformationRules.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/KnoraBaseToApiV2SimpleTransformationRules.scala @@ -67,6 +67,26 @@ object KnoraBaseToApiV2SimpleTransformationRules extends OntologyTransformationR objectType = Some(OntologyConstants.Xsd.String) ) + private val MayHaveMoreResults: ReadPropertyInfoV2 = makeProperty( + propertyIri = OntologyConstants.KnoraApiV2Simple.MayHaveMoreResults, + propertyType = OntologyConstants.Owl.DatatypeProperty, + predicates = Seq( + makePredicate( + predicateIri = OntologyConstants.Rdfs.Label, + objectsWithLang = Map( + LanguageCodes.EN -> "May have more results" + ) + ), + makePredicate( + predicateIri = OntologyConstants.Rdfs.Comment, + objectsWithLang = Map( + LanguageCodes.EN -> "Indicates whether more results may be available for a search query" + ) + ) + ), + objectType = Some(OntologyConstants.Xsd.Boolean) + ) + private val Error: ReadPropertyInfoV2 = makeProperty( propertyIri = OntologyConstants.KnoraApiV2Simple.Error, propertyType = OntologyConstants.Owl.DatatypeProperty, @@ -559,6 +579,7 @@ object KnoraBaseToApiV2SimpleTransformationRules extends OntologyTransformationR override val externalPropertiesToAdd: Map[SmartIri, ReadPropertyInfoV2] = Set( Label, Result, + MayHaveMoreResults, Error, ArkUrl, VersionArkUrl, diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index ab10ad6379..99b8655fd7 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -810,9 +810,11 @@ object DeleteOrEraseResourceRequestV2 extends KnoraJsonLDRequestReaderV2[DeleteO /** * Represents a sequence of resources read back from Knora. * - * @param resources a sequence of resources. + * @param resources a sequence of resources. */ -case class ReadResourcesSequenceV2(resources: Seq[ReadResourceV2], mayHaveMoreResults: Boolean = false) extends KnoraResponseV2 with KnoraReadV2[ReadResourcesSequenceV2] with UpdateResultInProject { +case class ReadResourcesSequenceV2(resources: Seq[ReadResourceV2], + hiddenResourceIris: Set[IRI] = Set.empty, + mayHaveMoreResults: Boolean = false) extends KnoraResponseV2 with KnoraReadV2[ReadResourcesSequenceV2] with UpdateResultInProject { override def toOntologySchema(targetSchema: ApiV2Schema): ReadResourcesSequenceV2 = { copy( @@ -897,6 +899,7 @@ case class ReadResourcesSequenceV2(resources: Seq[ReadResourceV2], mayHaveMoreRe schemaOptions = schemaOptions ) } + // #toJsonLDDocument /** @@ -904,14 +907,20 @@ case class ReadResourcesSequenceV2(resources: Seq[ReadResourceV2], mayHaveMoreRe * * @param requestedResourceIri the IRI of the expected resource. * @return the resource. + * @throws NotFoundException if the resource is not found. + * @throws BadRequestException if more than one resource was returned. */ def toResource(requestedResourceIri: IRI)(implicit stringFormatter: StringFormatter): ReadResourceV2 = { if (resources.isEmpty) { - throw AssertionException(s"Expected one resource, <$requestedResourceIri>, but no resources were returned") + throw NotFoundException(s"Expected <$requestedResourceIri>, but no resources were returned") } if (resources.size > 1) { - throw AssertionException(s"More than one resource returned") + throw BadRequestException(s"Expected one resource, <$requestedResourceIri>, but more than one was returned") + } + + if (resources.head.resourceIri != requestedResourceIri) { + throw NotFoundException(s"Expected resource <$requestedResourceIri>, but <${resources.head.resourceIri}> was returned") } resources.head diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index d01c83a284..d868579cbb 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -95,7 +95,7 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt // Convert the resource to the internal ontology schema. internalCreateResource: CreateResourceV2 <- Future(createResourceRequestV2.createResource.toOntologySchema(InternalSchema)) - // Check standoff link targets and list nodes that should exist. + // Check link targets and list nodes that should exist. _ <- checkStandoffLinkTargets(internalCreateResource.flatValues, createResourceRequestV2.requestingUser) _ <- checkListNodes(internalCreateResource.flatValues, createResourceRequestV2.requestingUser) @@ -695,7 +695,7 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt */ private def getLinkTargetClasses(internalCreateResources: Seq[CreateResourceV2], requestingUser: UserADM): Future[Map[IRI, SmartIri]] = { // Get the IRIs of the new and existing resources that are targets of links. - val (existingTargets: Set[IRI], newTargets: Set[IRI]) = internalCreateResources.flatMap(_.flatValues).foldLeft((Set.empty[IRI], Set.empty[IRI])) { + val (existingTargetIris: Set[IRI], newTargets: Set[IRI]) = internalCreateResources.flatMap(_.flatValues).foldLeft((Set.empty[IRI], Set.empty[IRI])) { case ((accExisting: Set[IRI], accNew: Set[IRI]), valueToCreate: CreateValueInNewResourceV2) => valueToCreate.valueContent match { case linkValueContentV2: LinkValueContentV2 => @@ -717,7 +717,7 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt for { // Get information about the existing resources that are targets of links. existingTargets: ReadResourcesSequenceV2 <- getResourcePreviewV2( - resourceIris = existingTargets.toSeq, + resourceIris = existingTargetIris.toSeq, targetSchema = ApiV2Complex, requestingUser = requestingUser ) @@ -851,7 +851,7 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt } } - checkResourceIris(standoffLinkTargetsThatShouldExist, requestingUser) + getResourcePreviewV2(standoffLinkTargetsThatShouldExist.toSeq, targetSchema = ApiV2Complex, requestingUser).map(_ => ()) } /** @@ -1006,7 +1006,7 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt requestingUser: UserADM): Future[ReadResourcesSequenceV2] = { val resourceIri = resourceReadyToCreate.sparqlTemplateResourceToCreate.resourceIri - for { + val resourceFuture: Future[ReadResourcesSequenceV2] = for { resourcesResponse: ReadResourcesSequenceV2 <- getResourcesV2( resourceIris = Seq(resourceIri), requestingUser = requestingUser, @@ -1014,11 +1014,7 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt schemaOptions = SchemaOptions.ForStandoffWithTextValues ) - resource: ReadResourceV2 = try { - resourcesResponse.toResource(requestedResourceIri = resourceIri) - } catch { - case _: NotFoundException => throw UpdateNotPerformedException(s"Resource <$resourceIri> was not created. Please report this as a possible bug.") - } + resource: ReadResourceV2 = resourcesResponse.toResource(requestedResourceIri = resourceIri) _ = if (resource.resourceClassIri.toString != resourceReadyToCreate.sparqlTemplateResourceToCreate.resourceClassIri) { throw AssertionException(s"Resource <$resourceIri> was saved, but it has the wrong resource class") @@ -1068,6 +1064,10 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt } yield ReadResourcesSequenceV2( resources = Seq(resource.copy(values = Map.empty)) ) + + resourceFuture.recover { + case _: NotFoundException => throw UpdateNotPerformedException(s"Resource <$resourceIri> was not created. Please report this as a possible bug.") + } } /** @@ -1216,6 +1216,11 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt requestingUser = requestingUser ) + _ = checkResourceIris( + targetResourceIris = resourceIris.toSet, + resourcesSequence = apiResponse + ) + _ = valueUuid match { case Some(definedValueUuid) => if (!apiResponse.resources.exists(_.values.values.exists(_.exists(_.valueHasUUID == definedValueUuid)))) { @@ -1264,8 +1269,12 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt settings = settings, requestingUser = requestingUser ) - } yield apiResponse + _ = checkResourceIris( + targetResourceIris = resourceIris.toSet, + resourcesSequence = apiResponse + ) + } yield apiResponse } /** @@ -1533,17 +1542,22 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt } /** - * Given a set of resource IRIs, checks that they point to Knora resources. - * If not, throws an exception. + * Checks that requested resources were found and that the user has permission to see them. If not, throws an exception. * * @param targetResourceIris the IRIs to be checked. - * @param requestingUser the user making the request. + * @param resourcesSequence the result of requesting those IRIs. */ - private def checkResourceIris(targetResourceIris: Set[IRI], requestingUser: UserADM): Future[Unit] = { - if (targetResourceIris.isEmpty) { - FastFuture.successful(()) - } else { - getResourcePreviewV2(targetResourceIris.toSeq, targetSchema = ApiV2Complex, requestingUser).map(_ => ()) + private def checkResourceIris(targetResourceIris: Set[IRI], resourcesSequence: ReadResourcesSequenceV2): Unit = { + val hiddenTargetResourceIris: Set[IRI] = targetResourceIris.intersect(resourcesSequence.hiddenResourceIris) + + if (hiddenTargetResourceIris.nonEmpty) { + throw ForbiddenException(s"You do not have permission to view one or more resources: ${hiddenTargetResourceIris.map(iri => s"<$iri>").mkString(", ")}") + } + + val missingResourceIris: Set[IRI] = targetResourceIris -- resourcesSequence.resources.map(_.resourceIri).toSet + + if (missingResourceIris.nonEmpty) { + throw NotFoundException(s"One or more resources were not found: ${missingResourceIris.map(iri => s"<$iri>").mkString(", ")}") } } diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala index e68d64c45b..6968302775 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala @@ -215,10 +215,7 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand // the prequery returned no results, no further query is necessary Future( - ConstructResponseUtilV2.MainResourcesAndValueRdfData( - resources = Map.empty[IRI, ConstructResponseUtilV2.ResourceWithValueRdfData], - hiddenResourceCount = 0 - ) + ConstructResponseUtilV2.MainResourcesAndValueRdfData(resources = Map.empty[IRI, ConstructResponseUtilV2.ResourceWithValueRdfData]) ) } @@ -482,11 +479,15 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand inputQuery ) - } yield ConstructResponseUtilV2.MainResourcesAndValueRdfData(resources = queryResWithFullGraphPatternOnlyRequestedValues, queryResultsFilteredForPermissions.hiddenResourceCount) + } yield ConstructResponseUtilV2.MainResourcesAndValueRdfData( + resources = queryResWithFullGraphPatternOnlyRequestedValues, + hiddenMainResourceIris = queryResultsFilteredForPermissions.hiddenMainResourceIris, + hiddenDependentResourceIris = queryResultsFilteredForPermissions.hiddenDependentResourceIris + ) } else { // the prequery returned no results, no further query is necessary - Future(ConstructResponseUtilV2.MainResourcesAndValueRdfData(Map.empty[IRI, ConstructResponseUtilV2.ResourceWithValueRdfData], 0)) + Future(ConstructResponseUtilV2.MainResourcesAndValueRdfData(resources = Map.empty[IRI, ConstructResponseUtilV2.ResourceWithValueRdfData])) } // Find out whether to query standoff along with text values. This boolean value will be passed to diff --git a/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala b/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala index 59b2bd16db..d937a5ba77 100644 --- a/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala @@ -290,17 +290,31 @@ object ConstructResponseUtilV2 { */ case class MappingAndXSLTransformation(mapping: MappingXMLtoStandoff, standoffEntities: StandoffEntityInfoGetResponseV2, XSLTransformation: Option[String]) - case class MainResourcesAndValueRdfData(resources: RdfResources, hiddenResourceCount: Int) + /** + * Represents a tree structure of resources, values and dependent resources returned by a SPARQL CONSTRUCT query. + * + * @param resources a map of resource Iris to [[ResourceWithValueRdfData]]. The resource Iris represent main resources, dependent + * resources are contained in the link values as nested structures. + * @param hiddenMainResourceIris the IRIs of main resources that were hidden because the user does not have permission + * to see them. + * @param hiddenDependentResourceIris the IRIs of dependent resources that were hidden because the user does not have + * permission to see them. + */ + case class MainResourcesAndValueRdfData(resources: RdfResources, + hiddenMainResourceIris: Set[IRI] = Set.empty, + hiddenDependentResourceIris: Set[IRI] = Set.empty) /** * A [[SparqlConstructResponse]] may contain both resources and value RDF data objects as well as standoff. - * This method turns a graph (i.e. triples) into a structure organized by the principle of resources and their values, i.e. a map of resource Iris to [[ResourceWithValueRdfData]]. + * This method turns a graph (i.e. triples) into a structure organized by the principle of resources and their values, + * i.e. a map of resource Iris to [[ResourceWithValueRdfData]]. * The resource Iris represent main resources, dependent resources are contained in the link values as nested structures. * * @param constructQueryResults the results of a SPARQL construct query representing resources and their values. - * @return a Map[resource IRI -> [[ResourceWithValueRdfData]]]. + * @return an instance of [[MainResourcesAndValueRdfData]]. */ - def splitMainResourcesAndValueRdfData(constructQueryResults: SparqlExtendedConstructResponse, requestingUser: UserADM)(implicit stringFormatter: StringFormatter): MainResourcesAndValueRdfData = { + def splitMainResourcesAndValueRdfData(constructQueryResults: SparqlExtendedConstructResponse, + requestingUser: UserADM)(implicit stringFormatter: StringFormatter): MainResourcesAndValueRdfData = { // An intermediate data structure containing RDF assertions about an entity and the user's permission on the entity. case class RdfWithUserPermission(assertions: ConstructPredicateObjects, maybeUserPermission: Option[EntityPermission]) @@ -498,9 +512,9 @@ object ConstructResponseUtilV2 { case (resourceIri: IRI, resource: ResourceWithValueRdfData) if resource.isMainResource => resourceIri }.toSet - val mainResourceIrisNotVisibleCount: Int = hiddenResources.collect { + val mainResourceIrisNotVisible: Set[IRI] = hiddenResources.collect { case (resourceIri: IRI, resource: ResourceWithValueRdfData) if resource.isMainResource => resourceIri - }.toSet.size + }.toSet val dependentResourceIrisVisible: Set[IRI] = visibleResources.collect { case (resourceIri: IRI, resource: ResourceWithValueRdfData) if !resource.isMainResource => resourceIri @@ -667,7 +681,11 @@ object ConstructResponseUtilV2 { resourceIri -> transformedResource }.toMap - MainResourcesAndValueRdfData(resources = mainResourcesNested, hiddenResourceCount = mainResourceIrisNotVisibleCount) + MainResourcesAndValueRdfData( + resources = mainResourcesNested, + hiddenMainResourceIris = mainResourceIrisNotVisible, + hiddenDependentResourceIris = dependentResourceIrisNotVisible + ) } /** @@ -1262,7 +1280,7 @@ object ConstructResponseUtilV2 { resourceWithValueRdfData = mainResourcesAndValueRdfData.resources(resourceIri), mappings = mappings, queryStandoff = queryStandoff, - versionDate = None, + versionDate = versionDate, responderManager = responderManager, targetSchema = targetSchema, settings = settings, @@ -1274,7 +1292,11 @@ object ConstructResponseUtilV2 { for { resources <- Future.sequence(readResourceFutures) mayHaveMoreResults = calculateMayHaveMoreResults && - settings.maxResultsPerSearchResultPage > (visibleResourceIris.size + mainResourcesAndValueRdfData.hiddenResourceCount) - } yield ReadResourcesSequenceV2(resources = resources, mayHaveMoreResults = mayHaveMoreResults) + settings.v2ResultsPerPage == (visibleResourceIris.size + mainResourcesAndValueRdfData.hiddenMainResourceIris.size) + } yield ReadResourcesSequenceV2( + resources = resources, + hiddenResourceIris = mainResourcesAndValueRdfData.hiddenMainResourceIris ++ mainResourcesAndValueRdfData.hiddenDependentResourceIris, + mayHaveMoreResults = mayHaveMoreResults + ) } } \ No newline at end of file diff --git a/webapi/src/test/resources/test-data/ontologyR2RV2/knoraApiOntologySimple.jsonld b/webapi/src/test/resources/test-data/ontologyR2RV2/knoraApiOntologySimple.jsonld index de89833e3a..15a9ead6d1 100644 --- a/webapi/src/test/resources/test-data/ontologyR2RV2/knoraApiOntologySimple.jsonld +++ b/webapi/src/test/resources/test-data/ontologyR2RV2/knoraApiOntologySimple.jsonld @@ -210,50 +210,6 @@ "owl:onDatatype" : { "@id" : "xsd:anyURI" } - }, { - "@id" : "knora-api:ForbiddenResource", - "@type" : "owl:Class", - "rdfs:comment" : "A ForbiddenResource is a proxy for a resource that the client has insufficient permissions to see.", - "rdfs:label" : "A ForbiddenResource is a proxy for a resource that the client has insufficient permissions to see.", - "rdfs:subClassOf" : [ { - "@id" : "knora-api:Resource" - }, { - "@type" : "owl:Restriction", - "owl:cardinality" : 1, - "owl:onProperty" : { - "@id" : "knora-api:arkUrl" - } - }, { - "@type" : "owl:Restriction", - "owl:minCardinality" : 0, - "owl:onProperty" : { - "@id" : "knora-api:hasComment" - } - }, { - "@type" : "owl:Restriction", - "owl:minCardinality" : 0, - "owl:onProperty" : { - "@id" : "knora-api:hasIncomingLink" - } - }, { - "@type" : "owl:Restriction", - "owl:minCardinality" : 0, - "owl:onProperty" : { - "@id" : "knora-api:hasStandoffLinkTo" - } - }, { - "@type" : "owl:Restriction", - "owl:cardinality" : 1, - "owl:onProperty" : { - "@id" : "knora-api:versionArkUrl" - } - }, { - "@type" : "owl:Restriction", - "owl:cardinality" : 1, - "owl:onProperty" : { - "@id" : "rdfs:label" - } - } ] }, { "@id" : "knora-api:Geom", "@type" : "rdfs:Datatype", @@ -936,6 +892,14 @@ "rdfs:subPropertyOf" : { "@id" : "knora-api:hasLinkTo" } + }, { + "@id" : "knora-api:mayHaveMoreResults", + "@type" : "owl:DatatypeProperty", + "knora-api:objectType" : { + "@id" : "xsd:boolean" + }, + "rdfs:comment" : "Indicates whether more results may be available for a search query", + "rdfs:label" : "May have more results" }, { "@id" : "knora-api:objectType", "@type" : "rdf:Property", diff --git a/webapi/src/test/resources/test-data/ontologyR2RV2/knoraApiOntologySimple.rdf b/webapi/src/test/resources/test-data/ontologyR2RV2/knoraApiOntologySimple.rdf index 96bb0a57f7..7608d87604 100644 --- a/webapi/src/test/resources/test-data/ontologyR2RV2/knoraApiOntologySimple.rdf +++ b/webapi/src/test/resources/test-data/ontologyR2RV2/knoraApiOntologySimple.rdf @@ -12,24 +12,24 @@ A generic class for representing annotations Annotation - - - - - - - + + + + + + + Represents something in the world, or an abstract thing Resource - - - - - + + + + + - + 1 @@ -39,7 +39,7 @@ - + 1 @@ -51,7 +51,7 @@ - + 0 @@ -63,7 +63,7 @@ - + 0 @@ -75,7 +75,7 @@ - + 1 @@ -86,7 +86,7 @@ - + 1 @@ -96,7 +96,7 @@ - + 1 @@ -106,29 +106,29 @@ Represents a file containing audio data Representation (Audio) - - - - - - + + + + + + A resource that can store a file Representation - - - - - - + + + + + + - + 1 - + 1 @@ -140,19 +140,19 @@ - + 0 - + 0 - + 1 - + 1 @@ -161,7 +161,7 @@ Color literal - + #([0-9a-fA-F]{3}){1,2} @@ -170,18 +170,18 @@ Represents a file containg 3D data Representation (3D) - - - - - - + + + + + + - + 1 - + 1 @@ -193,19 +193,19 @@ - + 0 - + 0 - + 1 - + 1 @@ -214,7 +214,7 @@ Date literal - + (GREGORIAN|JULIAN):\d{1,4}(-\d{1,2}(-\d{1,2})?)?( BC| AD| BCE| CE)?(:\d{1,4}(-\d{1,2}(-\d{1,2})?)?( BC| AD| BCE| CE)?)? @@ -222,18 +222,18 @@ Representation (Document) - - - - - - + + + + + + - + 1 - + 1 @@ -245,19 +245,19 @@ - + 0 - + 0 - + 1 - + 1 @@ -266,41 +266,6 @@ File URI - - A ForbiddenResource is a proxy for a resource that the client has insufficient permissions to see. - A ForbiddenResource is a proxy for a resource that the client has insufficient permissions to see. - - - - - - - - - - 1 - - - - 0 - - - - 0 - - - - 0 - - - - 1 - - - - 1 - - Represents a geometry specification in JSON. Geometry specification @@ -311,7 +276,7 @@ Geoname code - + \d{1,8} @@ -321,7 +286,7 @@ Interval literal - + \d+(\.\d+)?,\d+(\.\d+)? @@ -331,27 +296,27 @@ Represents a generic link object Link Object - - - - - - - + + + + + + + - + 1 - + 0 - + 0 - + 1 @@ -363,15 +328,15 @@ - + 0 - + 1 - + 1 @@ -384,22 +349,22 @@ A resource containing moving image data Representation (Movie) - - - - - - + + + + + + - + 1 - + 0 - + 1 @@ -411,15 +376,15 @@ - + 0 - + 1 - + 1 @@ -428,21 +393,21 @@ Represents a geometric region of a resource. The geometry is represented currently as JSON string. Region - - - - - - - - - + + + + + + + + + - + 1 - + 1 @@ -454,11 +419,11 @@ - + 1 - + 1 @@ -470,15 +435,15 @@ - + 0 - + 0 - + 1 @@ -490,19 +455,19 @@ - + 1 - + 1 - + 1 - + 1 @@ -514,39 +479,39 @@ - + 0 - + 0 - + 1 - + 1 - + 1 - + 0 - + 0 - + 1 - + 1 @@ -554,26 +519,26 @@ A resource that can contain a two-dimensional still image file Representation (Image) - - - - - - + + + + + + - + 1 - + 0 - + 0 - + 1 @@ -585,11 +550,11 @@ - + 1 - + 1 @@ -597,26 +562,26 @@ A resource containing a text file Representation (Text) - - - - - - + + + + + + - + 1 - + 0 - + 0 - + 1 @@ -628,11 +593,11 @@ - + 1 - + 1 @@ -640,34 +605,34 @@ a TextRepresentation representing an XSL transformation that can be applied to an XML created from standoff. The transformation's result is ecptected to be HTML. a TextRepresentation representing an XSL transformation that can be applied to an XML created from standoff. The transformation's result is ecptected to be HTML. - - - - - - + + + + + + - + 1 - + 0 - + 0 - + 1 - + 1 - + 1 @@ -708,6 +673,11 @@ is part of + + + Indicates whether more results may be available for a search query + May have more results + Specifies the required type of the objects of a property Object type diff --git a/webapi/src/test/resources/test-data/ontologyR2RV2/knoraApiOntologySimple.ttl b/webapi/src/test/resources/test-data/ontologyR2RV2/knoraApiOntologySimple.ttl index 4b95503a3b..11c7cf5743 100644 --- a/webapi/src/test/resources/test-data/ontologyR2RV2/knoraApiOntologySimple.ttl +++ b/webapi/src/test/resources/test-data/ontologyR2RV2/knoraApiOntologySimple.ttl @@ -225,29 +225,6 @@ knora-api:File a rdfs:Datatype; rdfs:label "File URI"; owl:onDatatype xsd:anyURI . -knora-api:ForbiddenResource a owl:Class; - rdfs:comment "A ForbiddenResource is a proxy for a resource that the client has insufficient permissions to see."; - rdfs:label "A ForbiddenResource is a proxy for a resource that the client has insufficient permissions to see."; - rdfs:subClassOf knora-api:Resource, [ a owl:Restriction; - owl:cardinality 1; - owl:onProperty knora-api:arkUrl - ], [ a owl:Restriction; - owl:minCardinality 0; - owl:onProperty knora-api:hasComment - ], [ a owl:Restriction; - owl:minCardinality 0; - owl:onProperty knora-api:hasIncomingLink - ], [ a owl:Restriction; - owl:minCardinality 0; - owl:onProperty knora-api:hasStandoffLinkTo - ], [ a owl:Restriction; - owl:cardinality 1; - owl:onProperty knora-api:versionArkUrl - ], [ a owl:Restriction; - owl:cardinality 1; - owl:onProperty rdfs:label - ] . - knora-api:Geom a rdfs:Datatype; rdfs:comment "Represents a geometry specification in JSON."; rdfs:label "Geometry specification"; @@ -518,6 +495,11 @@ knora-api:isPartOf a owl:ObjectProperty; rdfs:label "is part of"; rdfs:subPropertyOf knora-api:hasLinkTo . +knora-api:mayHaveMoreResults a owl:DatatypeProperty; + knora-api:objectType xsd:boolean; + rdfs:comment "Indicates whether more results may be available for a search query"; + rdfs:label "May have more results" . + knora-api:objectType a rdf:Property; rdfs:comment "Specifies the required type of the objects of a property"; rdfs:label "Object type" . diff --git a/webapi/src/test/resources/test-data/ontologyR2RV2/knoraApiOntologyWithValueObjects.jsonld b/webapi/src/test/resources/test-data/ontologyR2RV2/knoraApiOntologyWithValueObjects.jsonld index 85200870ea..e80945ac3b 100644 --- a/webapi/src/test/resources/test-data/ontologyR2RV2/knoraApiOntologyWithValueObjects.jsonld +++ b/webapi/src/test/resources/test-data/ontologyR2RV2/knoraApiOntologyWithValueObjects.jsonld @@ -1603,140 +1603,6 @@ "@id" : "knora-api:versionArkUrl" } } ] - }, { - "@id" : "knora-api:ForbiddenResource", - "@type" : "owl:Class", - "knora-api:isResourceClass" : true, - "rdfs:comment" : "A ForbiddenResource is a proxy for a resource that the client has insufficient permissions to see.", - "rdfs:label" : "A ForbiddenResource is a proxy for a resource that the client has insufficient permissions to see.", - "rdfs:subClassOf" : [ { - "@id" : "knora-api:Resource" - }, { - "@type" : "owl:Restriction", - "knora-api:isInherited" : true, - "owl:cardinality" : 1, - "owl:onProperty" : { - "@id" : "knora-api:arkUrl" - } - }, { - "@type" : "owl:Restriction", - "knora-api:isInherited" : true, - "owl:cardinality" : 1, - "owl:onProperty" : { - "@id" : "knora-api:attachedToProject" - } - }, { - "@type" : "owl:Restriction", - "knora-api:isInherited" : true, - "owl:cardinality" : 1, - "owl:onProperty" : { - "@id" : "knora-api:attachedToUser" - } - }, { - "@type" : "owl:Restriction", - "knora-api:isInherited" : true, - "owl:cardinality" : 1, - "owl:onProperty" : { - "@id" : "knora-api:creationDate" - } - }, { - "@type" : "owl:Restriction", - "knora-api:isInherited" : true, - "owl:maxCardinality" : 1, - "owl:onProperty" : { - "@id" : "knora-api:deleteComment" - } - }, { - "@type" : "owl:Restriction", - "knora-api:isInherited" : true, - "owl:maxCardinality" : 1, - "owl:onProperty" : { - "@id" : "knora-api:deleteDate" - } - }, { - "@type" : "owl:Restriction", - "knora-api:isInherited" : true, - "owl:maxCardinality" : 1, - "owl:onProperty" : { - "@id" : "knora-api:deletedBy" - } - }, { - "@type" : "owl:Restriction", - "owl:minCardinality" : 0, - "owl:onProperty" : { - "@id" : "knora-api:hasComment" - } - }, { - "@type" : "owl:Restriction", - "knora-api:isInherited" : true, - "owl:minCardinality" : 0, - "owl:onProperty" : { - "@id" : "knora-api:hasIncomingLinkValue" - } - }, { - "@type" : "owl:Restriction", - "knora-api:isInherited" : true, - "owl:cardinality" : 1, - "owl:onProperty" : { - "@id" : "knora-api:hasPermissions" - } - }, { - "@type" : "owl:Restriction", - "knora-api:isInherited" : true, - "owl:minCardinality" : 0, - "owl:onProperty" : { - "@id" : "knora-api:hasStandoffLinkTo" - } - }, { - "@type" : "owl:Restriction", - "knora-api:isInherited" : true, - "owl:minCardinality" : 0, - "owl:onProperty" : { - "@id" : "knora-api:hasStandoffLinkToValue" - } - }, { - "@type" : "owl:Restriction", - "knora-api:isInherited" : true, - "owl:maxCardinality" : 1, - "owl:onProperty" : { - "@id" : "knora-api:isDeleted" - } - }, { - "@type" : "owl:Restriction", - "knora-api:isInherited" : true, - "owl:maxCardinality" : 1, - "owl:onProperty" : { - "@id" : "knora-api:lastModificationDate" - } - }, { - "@type" : "owl:Restriction", - "knora-api:isInherited" : true, - "owl:cardinality" : 1, - "owl:onProperty" : { - "@id" : "knora-api:userHasPermission" - } - }, { - "@type" : "owl:Restriction", - "knora-api:isInherited" : true, - "owl:cardinality" : 1, - "owl:onProperty" : { - "@id" : "knora-api:versionArkUrl" - } - }, { - "@type" : "owl:Restriction", - "knora-api:isInherited" : true, - "owl:maxCardinality" : 1, - "owl:onProperty" : { - "@id" : "knora-api:versionDate" - } - }, { - "@type" : "owl:Restriction", - "knora-api:isInherited" : true, - "owl:cardinality" : 1, - "owl:onProperty" : { - "@id" : "rdfs:label" - } - } ] }, { "@id" : "knora-api:GeomValue", "@type" : "owl:Class", @@ -6471,6 +6337,14 @@ }, "rdfs:comment" : "Represents the name of a mapping", "rdfs:label" : "Name of a mapping (will be part of the mapping's Iri)" + }, { + "@id" : "knora-api:mayHaveMoreResults", + "@type" : "owl:DatatypeProperty", + "knora-api:objectType" : { + "@id" : "xsd:boolean" + }, + "rdfs:comment" : "Indicates whether more results may be available for a search query", + "rdfs:label" : "May have more results" }, { "@id" : "knora-api:movingImageFileValueHasDimX", "@type" : "owl:DatatypeProperty", diff --git a/webapi/src/test/resources/test-data/ontologyR2RV2/knoraApiOntologyWithValueObjects.rdf b/webapi/src/test/resources/test-data/ontologyR2RV2/knoraApiOntologyWithValueObjects.rdf index 7da0eecde7..d13c30275c 100644 --- a/webapi/src/test/resources/test-data/ontologyR2RV2/knoraApiOntologyWithValueObjects.rdf +++ b/webapi/src/test/resources/test-data/ontologyR2RV2/knoraApiOntologyWithValueObjects.rdf @@ -17,50 +17,50 @@ A generic class for representing annotations Annotation - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + true Represents something in the world, or an abstract thing Resource - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - + true 1 @@ -71,7 +71,7 @@ - + true 1 @@ -82,7 +82,7 @@ - + true 1 @@ -93,7 +93,7 @@ - + true 1 @@ -104,7 +104,7 @@ - + true 1 @@ -114,7 +114,7 @@ - + true 1 @@ -124,7 +124,7 @@ - + true 1 @@ -134,7 +134,7 @@ - + 1 @@ -149,7 +149,7 @@ - + true 0 @@ -164,7 +164,7 @@ - + true 1 @@ -173,7 +173,7 @@ - + true 0 @@ -188,7 +188,7 @@ - + true 0 @@ -203,7 +203,7 @@ - + 1 @@ -217,7 +217,7 @@ - + 1 @@ -230,7 +230,7 @@ - + true 1 @@ -240,7 +240,7 @@ - + true 1 @@ -249,7 +249,7 @@ - + true 1 @@ -260,7 +260,7 @@ - + true 1 @@ -271,7 +271,7 @@ - + true 1 @@ -282,7 +282,7 @@ - + true 1 @@ -293,53 +293,53 @@ true Represents an audio file - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + true - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - + true 1 - + true 1 - + 1 @@ -351,22 +351,22 @@ - + true 1 - + true 1 - + true 1 - + true 1 @@ -379,7 +379,7 @@ - + true 1 @@ -392,22 +392,22 @@ - + true 1 - + true 1 - + true 1 - + true 1 @@ -418,7 +418,7 @@ - + true 1 @@ -429,7 +429,7 @@ - + true 1 @@ -441,7 +441,7 @@ - + true 1 @@ -453,7 +453,7 @@ - + true 1 @@ -463,85 +463,85 @@ Represents a file containing audio data Representation (Audio) - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + true A resource that can store a file Representation - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + 1 @@ -556,62 +556,62 @@ - + true 0 - + true 1 - + true 0 - + true 0 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + - + 1 @@ -628,105 +628,105 @@ Represents a boolean value - - - - - - - - - - - - - - + + + + + + + + + + + + + + true The base class of classes representing Knora values - - - - - - - - - - - - - + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -734,7 +734,7 @@ - + 1 @@ -753,87 +753,87 @@ Represents a color in HTML format, e.g. "#33eeff" - - - - - - - - - - - - - - + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -842,93 +842,93 @@ true This represents some 3D-object with mesh data, point cloud, etc. - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -938,61 +938,61 @@ Represents a file containg 3D data Representation (3D) - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + 1 @@ -1007,69 +1007,69 @@ - + true 0 - + true 1 - + true 0 - + true 0 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - - - - - - - - - + + + + + + + + + - + 1 @@ -1081,7 +1081,7 @@ - + 1 @@ -1093,7 +1093,7 @@ - + 1 @@ -1105,7 +1105,7 @@ - + 1 @@ -1117,7 +1117,7 @@ - + 1 @@ -1129,7 +1129,7 @@ - + 1 @@ -1141,7 +1141,7 @@ - + 1 @@ -1153,7 +1153,7 @@ - + 1 @@ -1165,7 +1165,7 @@ - + 1 @@ -1182,135 +1182,135 @@ Represents a Knora date value - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -1318,7 +1318,7 @@ - + 1 @@ -1337,87 +1337,87 @@ Represents an arbitrary-precision decimal value - - - - - - - - - - - - - - + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -1425,51 +1425,51 @@ true - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + 1 @@ -1481,7 +1481,7 @@ - + 1 @@ -1493,7 +1493,7 @@ - + 1 @@ -1505,52 +1505,52 @@ - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -1559,61 +1559,61 @@ true Representation (Document) - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + 1 @@ -1628,287 +1628,174 @@ - + true 0 - + true 1 - + true 0 - + true 0 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + 1 - + 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - - true - A ForbiddenResource is a proxy for a resource that the client has insufficient permissions to see. - A ForbiddenResource is a proxy for a resource that the client has insufficient permissions to see. - - - - - - - - - - - - - - - - - - - - - - true - 1 - - - - true - 1 - - - - true - 1 - - - - true - 1 - - - - true - 1 - - - - true - 1 - - - - true - 1 - - - - 0 - - - - true - 0 - - - - true - 1 - - - - true - 0 - - - - true - 0 - - - - true - 1 - - - - true - 1 - - - - true - 1 - - - - true - 1 - - - - true - 1 - - - - true - 1 - - true Represents a geometrical objects as JSON string - - - - - - - - - - - - - - + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + 1 @@ -1920,42 +1807,42 @@ - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -1963,47 +1850,47 @@ true - - - - - - - - - - - - - - + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + 1 @@ -2015,42 +1902,42 @@ - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -2058,7 +1945,7 @@ - + 1 @@ -2077,97 +1964,97 @@ Represents an integer value - - - - - - - - - - - - - - + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - - + + - + 1 @@ -2179,7 +2066,7 @@ - + 1 @@ -2196,93 +2083,93 @@ Represents a time interval, e.g. in an audio recording - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -2294,72 +2181,72 @@ Represents a generic link object Link Object - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + 0 - + true 0 - + 1 @@ -2374,7 +2261,7 @@ - + 1 @@ -2389,47 +2276,47 @@ - + true 1 - + true 0 - + true 0 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -2439,60 +2326,60 @@ A reification node that describes direct links between resources - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + 1 @@ -2504,7 +2391,7 @@ - + 1 @@ -2516,7 +2403,7 @@ - + 1 @@ -2528,7 +2415,7 @@ - + 1 @@ -2540,103 +2427,103 @@ - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 Represents a flat or hierarchical list - - + + - + 1 - + 1 true - - - - - - - - - - - - - - + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + 1 @@ -2648,32 +2535,32 @@ - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -2682,72 +2569,72 @@ true Represents a moving image file - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + 1 @@ -2759,7 +2646,7 @@ - + 1 @@ -2771,7 +2658,7 @@ - + 1 @@ -2783,7 +2670,7 @@ - + 1 @@ -2795,32 +2682,32 @@ - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -2830,66 +2717,66 @@ A resource containing moving image data Representation (Movie) - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 0 - + 1 @@ -2904,47 +2791,47 @@ - + true 1 - + true 0 - + true 0 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -2956,65 +2843,65 @@ Represents a geometric region of a resource. The geometry is represented currently as JSON string. Region - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + 1 @@ -3030,11 +2917,11 @@ - + 1 - + 1 @@ -3049,32 +2936,32 @@ - + true 0 - + true 1 - + true 0 - + true 0 - + true 1 - + 1 @@ -3089,7 +2976,7 @@ - + 1 @@ -3104,67 +2991,67 @@ - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + 1 @@ -3178,121 +3065,121 @@ - + true 0 - + true 1 - + true 0 - + true 0 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + 1 - + 1 - + 1 - + 1 - + 1 - + 1 - + 1 - + 0 - + 1 - + 0 - + 0 - + 1 - + 1 - + 1 - + 1 - + 1 - + 1 @@ -3301,39 +3188,39 @@ Represents a boolean in a TextValue - - - - - - - - - - - + + + + + + + + + + + true Represents a knora-base value type in a TextValue - - - - - - - - - - + + + + + + + + + + - + true 1 - + true 1 @@ -3343,7 +3230,7 @@ - + true 1 @@ -3353,7 +3240,7 @@ - + true 1 @@ -3363,7 +3250,7 @@ - + true 1 @@ -3375,7 +3262,7 @@ - + true 1 @@ -3385,7 +3272,7 @@ - + true 1 @@ -3395,7 +3282,7 @@ - + true 1 @@ -3405,7 +3292,7 @@ - + true 1 @@ -3416,7 +3303,7 @@ - + true 1 @@ -3428,7 +3315,7 @@ - + true 1 @@ -3443,69 +3330,69 @@ Represents a color in a TextValue - - - - - - - - - - - + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -3513,63 +3400,63 @@ true Represents a standoff markup tag - - - - - - - - - - + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -3579,117 +3466,117 @@ Represents a date in a TextValue - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -3699,69 +3586,69 @@ Represents a decimal (floating point) value in a TextValue - - - - - - - - - - - + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -3771,69 +3658,69 @@ Represents an integer value in a TextValue - - - - - - - - - - - + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -3843,39 +3730,39 @@ Represents an internal reference in a TextValue - - - - - - - - - - - + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + 1 @@ -3884,32 +3771,32 @@ - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -3919,75 +3806,75 @@ Represents an interval in a TextValue - - - - - - - - - - - - + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -3996,39 +3883,39 @@ true Represents a reference to a Knora resource in a TextValue - - - - - - - - - - - + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + 1 @@ -4037,73 +3924,73 @@ - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + 1 - + 1 - + 1 - + 1 - + 1 - + 1 - + 1 - + 1 - + 1 - + 1 @@ -4112,73 +3999,73 @@ Represents a timestamp in a TextValue - - - - - - - - - - - + + + + + + + + + + + - + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -4194,73 +4081,73 @@ Represents an arbitrary URI in a TextValue - - - - - - - - - - - + + + + + + + + + + + - + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -4277,71 +4164,71 @@ true A file containing a two-dimensional still image - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + 1 @@ -4353,7 +4240,7 @@ - + 1 @@ -4365,7 +4252,7 @@ - + 1 @@ -4377,32 +4264,32 @@ - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -4412,81 +4299,81 @@ A resource that can contain a two-dimensional still image file Representation (Image) - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 0 - + true 1 - + true 0 - + true 0 - + 1 @@ -4501,32 +4388,32 @@ - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -4535,93 +4422,93 @@ true A text file such as plain Unicode text, LaTeX, TEI/XML, etc. - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -4631,81 +4518,81 @@ A resource containing a text file Representation (Text) - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 0 - + true 1 - + true 0 - + true 0 - + 1 @@ -4720,32 +4607,32 @@ - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -4753,63 +4640,63 @@ true - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + 1 @@ -4821,7 +4708,7 @@ - + 1 @@ -4833,7 +4720,7 @@ - + 1 @@ -4845,7 +4732,7 @@ - + 1 @@ -4857,7 +4744,7 @@ - + 1 @@ -4869,7 +4756,7 @@ - + 1 @@ -4881,7 +4768,7 @@ - + 0 @@ -4893,37 +4780,37 @@ - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + 1 @@ -4932,92 +4819,92 @@ Represents a timestamp - - - - - - - - - - - - - - + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + 1 @@ -5026,140 +4913,140 @@ Represents a URI - - - - - - - - - - - - - - + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + 1 - + 1 - + 1 - + 1 - + 1 - + 1 - + 1 - + 1 - + 1 - + 1 - + 1 - + 1 - + 1 @@ -5168,110 +5055,110 @@ a TextRepresentation representing an XSL transformation that can be applied to an XML created from standoff. The transformation's result is ecptected to be HTML. a TextRepresentation representing an XSL transformation that can be applied to an XML created from standoff. The transformation's result is ecptected to be HTML. - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 0 - + true 1 - + true 0 - + true 0 - + 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 - + true 1 @@ -5403,6 +5290,11 @@ Represents the name of a mapping Name of a mapping (will be part of the mapping's Iri) + + + Indicates whether more results may be available for a search query + May have more results + Specifies the new modification date of a resource diff --git a/webapi/src/test/resources/test-data/ontologyR2RV2/knoraApiOntologyWithValueObjects.ttl b/webapi/src/test/resources/test-data/ontologyR2RV2/knoraApiOntologyWithValueObjects.ttl index c912e30620..a64ed08015 100644 --- a/webapi/src/test/resources/test-data/ontologyR2RV2/knoraApiOntologyWithValueObjects.ttl +++ b/webapi/src/test/resources/test-data/ontologyR2RV2/knoraApiOntologyWithValueObjects.ttl @@ -1387,83 +1387,6 @@ knora-api:hasDocumentFileValue a owl:ObjectProperty; rdfs:label "has document"; rdfs:subPropertyOf knora-api:hasFileValue . -knora-api:ForbiddenResource a owl:Class; - knora-api:isResourceClass true; - rdfs:comment "A ForbiddenResource is a proxy for a resource that the client has insufficient permissions to see."; - rdfs:label "A ForbiddenResource is a proxy for a resource that the client has insufficient permissions to see."; - rdfs:subClassOf knora-api:Resource, [ a owl:Restriction; - knora-api:isInherited true; - owl:cardinality 1; - owl:onProperty knora-api:arkUrl - ], [ a owl:Restriction; - knora-api:isInherited true; - owl:cardinality 1; - owl:onProperty knora-api:attachedToProject - ], [ a owl:Restriction; - knora-api:isInherited true; - owl:cardinality 1; - owl:onProperty knora-api:attachedToUser - ], [ a owl:Restriction; - knora-api:isInherited true; - owl:cardinality 1; - owl:onProperty knora-api:creationDate - ], [ a owl:Restriction; - knora-api:isInherited true; - owl:maxCardinality 1; - owl:onProperty knora-api:deleteComment - ], [ a owl:Restriction; - knora-api:isInherited true; - owl:maxCardinality 1; - owl:onProperty knora-api:deleteDate - ], [ a owl:Restriction; - knora-api:isInherited true; - owl:maxCardinality 1; - owl:onProperty knora-api:deletedBy - ], [ a owl:Restriction; - owl:minCardinality 0; - owl:onProperty knora-api:hasComment - ], [ a owl:Restriction; - knora-api:isInherited true; - owl:minCardinality 0; - owl:onProperty knora-api:hasIncomingLinkValue - ], [ a owl:Restriction; - knora-api:isInherited true; - owl:cardinality 1; - owl:onProperty knora-api:hasPermissions - ], [ a owl:Restriction; - knora-api:isInherited true; - owl:minCardinality 0; - owl:onProperty knora-api:hasStandoffLinkTo - ], [ a owl:Restriction; - knora-api:isInherited true; - owl:minCardinality 0; - owl:onProperty knora-api:hasStandoffLinkToValue - ], [ a owl:Restriction; - knora-api:isInherited true; - owl:maxCardinality 1; - owl:onProperty knora-api:isDeleted - ], [ a owl:Restriction; - knora-api:isInherited true; - owl:maxCardinality 1; - owl:onProperty knora-api:lastModificationDate - ], [ a owl:Restriction; - knora-api:isInherited true; - owl:cardinality 1; - owl:onProperty knora-api:userHasPermission - ], [ a owl:Restriction; - knora-api:isInherited true; - owl:cardinality 1; - owl:onProperty knora-api:versionArkUrl - ], [ a owl:Restriction; - knora-api:isInherited true; - owl:maxCardinality 1; - owl:onProperty knora-api:versionDate - ], [ a owl:Restriction; - knora-api:isInherited true; - owl:cardinality 1; - owl:onProperty rdfs:label - ] . - knora-api:GeomValue a owl:Class; knora-api:isValueClass true; rdfs:comment "Represents a geometrical objects as JSON string"; @@ -3836,6 +3759,11 @@ knora-api:mappingHasName a owl:DatatypeProperty; rdfs:comment "Represents the name of a mapping"; rdfs:label "Name of a mapping (will be part of the mapping's Iri)" . +knora-api:mayHaveMoreResults a owl:DatatypeProperty; + knora-api:objectType xsd:boolean; + rdfs:comment "Indicates whether more results may be available for a search query"; + rdfs:label "May have more results" . + knora-api:newModificationDate a owl:DatatypeProperty; knora-api:objectType xsd:dateTimeStamp; rdfs:comment "Specifies the new modification date of a resource"; diff --git a/webapi/src/test/resources/test-data/searchR2RV2/NarrFulltextSearch.jsonld b/webapi/src/test/resources/test-data/searchR2RV2/NarrFulltextSearch.jsonld index 28c0cec81b..1cb125689d 100644 --- a/webapi/src/test/resources/test-data/searchR2RV2/NarrFulltextSearch.jsonld +++ b/webapi/src/test/resources/test-data/searchR2RV2/NarrFulltextSearch.jsonld @@ -1175,6 +1175,7 @@ }, "rdfs:label" : "i4r" } ], + "knora-api:mayHaveMoreResults" : true, "@context" : { "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", diff --git a/webapi/src/test/resources/test-data/searchR2RV2/PagesOfNarrenschiffOrderedBySeqnum.jsonld b/webapi/src/test/resources/test-data/searchR2RV2/PagesOfNarrenschiffOrderedBySeqnum.jsonld index 2c83b84a24..7c4897c638 100644 --- a/webapi/src/test/resources/test-data/searchR2RV2/PagesOfNarrenschiffOrderedBySeqnum.jsonld +++ b/webapi/src/test/resources/test-data/searchR2RV2/PagesOfNarrenschiffOrderedBySeqnum.jsonld @@ -2350,6 +2350,7 @@ }, "rdfs:label" : "b4r" } ], + "knora-api:mayHaveMoreResults" : true, "@context" : { "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", diff --git a/webapi/src/test/resources/test-data/searchR2RV2/PagesOfNarrenschiffOrderedBySeqnumNextOffset.jsonld b/webapi/src/test/resources/test-data/searchR2RV2/PagesOfNarrenschiffOrderedBySeqnumNextOffset.jsonld index 63a102aedd..bfd19a4c03 100644 --- a/webapi/src/test/resources/test-data/searchR2RV2/PagesOfNarrenschiffOrderedBySeqnumNextOffset.jsonld +++ b/webapi/src/test/resources/test-data/searchR2RV2/PagesOfNarrenschiffOrderedBySeqnumNextOffset.jsonld @@ -2350,6 +2350,7 @@ }, "rdfs:label" : "c8v" } ], + "knora-api:mayHaveMoreResults" : true, "@context" : { "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", diff --git a/webapi/src/test/resources/test-data/searchR2RV2/ThingWithBooleanOptionalOffset0.jsonld b/webapi/src/test/resources/test-data/searchR2RV2/ThingWithBooleanOptionalOffset0.jsonld index 247fd3c667..d319b4e6dc 100644 --- a/webapi/src/test/resources/test-data/searchR2RV2/ThingWithBooleanOptionalOffset0.jsonld +++ b/webapi/src/test/resources/test-data/searchR2RV2/ThingWithBooleanOptionalOffset0.jsonld @@ -623,6 +623,7 @@ }, "rdfs:label" : "A containing thing" } ], + "knora-api:mayHaveMoreResults" : true, "@context" : { "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", diff --git a/webapi/src/test/resources/test-data/searchR2RV2/ThingWithHiddenThing.jsonld b/webapi/src/test/resources/test-data/searchR2RV2/ThingWithHiddenThing.jsonld index a5f9f39163..9f321c9cd4 100644 --- a/webapi/src/test/resources/test-data/searchR2RV2/ThingWithHiddenThing.jsonld +++ b/webapi/src/test/resources/test-data/searchR2RV2/ThingWithHiddenThing.jsonld @@ -1,7 +1,7 @@ { "@id" : "http://rdfh.ch/0001/55UrkgTKR2SEQgnsLWI9mg", "@type" : "anything:Thing", - "anything:hasOtherThingValue" : [ { + "anything:hasOtherThingValue" : { "@id" : "http://rdfh.ch/0001/55UrkgTKR2SEQgnsLWI9mg/values/MIbQMDn6T12QMS0GDlEDSg", "@type" : "knora-api:LinkValue", "knora-api:arkUrl" : { @@ -48,54 +48,7 @@ "@type" : "xsd:anyURI", "@value" : "http://0.0.0.0:3336/ark:/72163/1/0001/55UrkgTKR2SEQgnsLWI9mgR/MIbQMDn6T12QMS0GDlEDSgu.20191129T100000673298Z" } - }, { - "@id" : "http://rdfh.ch/0001/55UrkgTKR2SEQgnsLWI9mg/values/bRS6vcbaQxqU-DF0pWhZog", - "@type" : "knora-api:LinkValue", - "knora-api:arkUrl" : { - "@type" : "xsd:anyURI", - "@value" : "http://0.0.0.0:3336/ark:/72163/1/0001/55UrkgTKR2SEQgnsLWI9mgR/bRS6vcbaQxqU=DF0pWhZogS" - }, - "knora-api:attachedToUser" : { - "@id" : "http://rdfh.ch/users/9XBCrDV3SRa7kS1WwynB4Q" - }, - "knora-api:hasPermissions" : "V knora-admin:UnknownUser|M knora-admin:ProjectMember", - "knora-api:linkValueHasTarget" : { - "@id" : "http://rdfh.ch/0000/forbiddenResource", - "@type" : "knora-api:ForbiddenResource", - "knora-api:arkUrl" : { - "@type" : "xsd:anyURI", - "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV" - }, - "knora-api:attachedToProject" : { - "@id" : "http://www.knora.org/ontology/knora-admin#SystemProject" - }, - "knora-api:attachedToUser" : { - "@id" : "http://rdfh.ch/users/root" - }, - "knora-api:creationDate" : { - "@type" : "xsd:dateTimeStamp", - "@value" : "2017-10-06T11:05:37Z" - }, - "knora-api:hasPermissions" : "V knora-admin:UnknownUser", - "knora-api:userHasPermission" : "V", - "knora-api:versionArkUrl" : { - "@type" : "xsd:anyURI", - "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV.20171006T110537Z" - }, - "rdfs:label" : "This resource is a proxy for a resource you are not allowed to see" - }, - "knora-api:userHasPermission" : "V", - "knora-api:valueCreationDate" : { - "@type" : "xsd:dateTimeStamp", - "@value" : "2019-11-29T10:00:00.673298Z" - }, - "knora-api:valueHasComment" : "link value pointing to hidden resource", - "knora-api:valueHasUUID" : "bRS6vcbaQxqU-DF0pWhZog", - "knora-api:versionArkUrl" : { - "@type" : "xsd:anyURI", - "@value" : "http://0.0.0.0:3336/ark:/72163/1/0001/55UrkgTKR2SEQgnsLWI9mgR/bRS6vcbaQxqU=DF0pWhZogS.20191129T100000673298Z" - } - } ], + }, "knora-api:arkUrl" : { "@type" : "xsd:anyURI", "@value" : "http://0.0.0.0:3336/ark:/72163/1/0001/55UrkgTKR2SEQgnsLWI9mgR" diff --git a/webapi/src/test/resources/test-data/searchR2RV2/ThingsWithOptionalDecimalGreaterThan1.jsonld b/webapi/src/test/resources/test-data/searchR2RV2/ThingsWithOptionalDecimalGreaterThan1.jsonld index 23bacefcd3..540e86ebe5 100644 --- a/webapi/src/test/resources/test-data/searchR2RV2/ThingsWithOptionalDecimalGreaterThan1.jsonld +++ b/webapi/src/test/resources/test-data/searchR2RV2/ThingsWithOptionalDecimalGreaterThan1.jsonld @@ -483,30 +483,6 @@ "@value" : "http://0.0.0.0:3336/ark:/72163/1/0001/a=thing=with=pictureE.20160302T150510Z" }, "rdfs:label" : "A thing with a picture" - }, { - "@id" : "http://rdfh.ch/0000/forbiddenResource", - "@type" : "knora-api:ForbiddenResource", - "knora-api:arkUrl" : { - "@type" : "xsd:anyURI", - "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV" - }, - "knora-api:attachedToProject" : { - "@id" : "http://www.knora.org/ontology/knora-admin#SystemProject" - }, - "knora-api:attachedToUser" : { - "@id" : "http://rdfh.ch/users/root" - }, - "knora-api:creationDate" : { - "@type" : "xsd:dateTimeStamp", - "@value" : "2017-10-06T11:05:37Z" - }, - "knora-api:hasPermissions" : "V knora-admin:UnknownUser", - "knora-api:userHasPermission" : "V", - "knora-api:versionArkUrl" : { - "@type" : "xsd:anyURI", - "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV.20171006T110537Z" - }, - "rdfs:label" : "This resource is a proxy for a resource you are not allowed to see" }, { "@id" : "http://rdfh.ch/0001/a-thing-with-text-valuesLanguage", "@type" : "anything:Thing", @@ -579,79 +555,8 @@ "@value" : "http://0.0.0.0:3336/ark:/72163/1/0001/cL5AwEioRLOm6Vrqwl1RmQ8.20161017T171604915Z" }, "rdfs:label" : "Oscar" - }, { - "@id" : "http://rdfh.ch/0000/forbiddenResource", - "@type" : "knora-api:ForbiddenResource", - "knora-api:arkUrl" : { - "@type" : "xsd:anyURI", - "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV" - }, - "knora-api:attachedToProject" : { - "@id" : "http://www.knora.org/ontology/knora-admin#SystemProject" - }, - "knora-api:attachedToUser" : { - "@id" : "http://rdfh.ch/users/root" - }, - "knora-api:creationDate" : { - "@type" : "xsd:dateTimeStamp", - "@value" : "2017-10-06T11:05:37Z" - }, - "knora-api:hasPermissions" : "V knora-admin:UnknownUser", - "knora-api:userHasPermission" : "V", - "knora-api:versionArkUrl" : { - "@type" : "xsd:anyURI", - "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV.20171006T110537Z" - }, - "rdfs:label" : "This resource is a proxy for a resource you are not allowed to see" - }, { - "@id" : "http://rdfh.ch/0000/forbiddenResource", - "@type" : "knora-api:ForbiddenResource", - "knora-api:arkUrl" : { - "@type" : "xsd:anyURI", - "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV" - }, - "knora-api:attachedToProject" : { - "@id" : "http://www.knora.org/ontology/knora-admin#SystemProject" - }, - "knora-api:attachedToUser" : { - "@id" : "http://rdfh.ch/users/root" - }, - "knora-api:creationDate" : { - "@type" : "xsd:dateTimeStamp", - "@value" : "2017-10-06T11:05:37Z" - }, - "knora-api:hasPermissions" : "V knora-admin:UnknownUser", - "knora-api:userHasPermission" : "V", - "knora-api:versionArkUrl" : { - "@type" : "xsd:anyURI", - "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV.20171006T110537Z" - }, - "rdfs:label" : "This resource is a proxy for a resource you are not allowed to see" - }, { - "@id" : "http://rdfh.ch/0000/forbiddenResource", - "@type" : "knora-api:ForbiddenResource", - "knora-api:arkUrl" : { - "@type" : "xsd:anyURI", - "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV" - }, - "knora-api:attachedToProject" : { - "@id" : "http://www.knora.org/ontology/knora-admin#SystemProject" - }, - "knora-api:attachedToUser" : { - "@id" : "http://rdfh.ch/users/root" - }, - "knora-api:creationDate" : { - "@type" : "xsd:dateTimeStamp", - "@value" : "2017-10-06T11:05:37Z" - }, - "knora-api:hasPermissions" : "V knora-admin:UnknownUser", - "knora-api:userHasPermission" : "V", - "knora-api:versionArkUrl" : { - "@type" : "xsd:anyURI", - "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV.20171006T110537Z" - }, - "rdfs:label" : "This resource is a proxy for a resource you are not allowed to see" } ], + "knora-api:mayHaveMoreResults" : true, "@context" : { "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", diff --git a/webapi/src/test/resources/test-data/searchR2RV2/searchResponseWithforbiddenResource.jsonld b/webapi/src/test/resources/test-data/searchR2RV2/searchResponseWithHiddenResource.jsonld similarity index 91% rename from webapi/src/test/resources/test-data/searchR2RV2/searchResponseWithforbiddenResource.jsonld rename to webapi/src/test/resources/test-data/searchR2RV2/searchResponseWithHiddenResource.jsonld index 261d1a4f25..14f1dd204d 100644 --- a/webapi/src/test/resources/test-data/searchR2RV2/searchResponseWithforbiddenResource.jsonld +++ b/webapi/src/test/resources/test-data/searchR2RV2/searchResponseWithHiddenResource.jsonld @@ -1,29 +1,5 @@ { "@graph" : [ { - "@id" : "http://rdfh.ch/0000/forbiddenResource", - "@type" : "knora-api:ForbiddenResource", - "knora-api:arkUrl" : { - "@type" : "xsd:anyURI", - "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV" - }, - "knora-api:attachedToProject" : { - "@id" : "http://www.knora.org/ontology/knora-admin#SystemProject" - }, - "knora-api:attachedToUser" : { - "@id" : "http://rdfh.ch/users/root" - }, - "knora-api:creationDate" : { - "@type" : "xsd:dateTimeStamp", - "@value" : "2017-10-06T11:05:37Z" - }, - "knora-api:hasPermissions" : "V knora-admin:UnknownUser", - "knora-api:userHasPermission" : "V", - "knora-api:versionArkUrl" : { - "@type" : "xsd:anyURI", - "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV.20171006T110537Z" - }, - "rdfs:label" : "This resource is a proxy for a resource you are not allowed to see" - }, { "@id" : "http://rdfh.ch/0001/a-thing-with-text-valuesLanguage", "@type" : "anything:Thing", "knora-api:arkUrl" : { diff --git a/webapi/src/test/resources/test-data/searchR2RV2/thingWithOptionalDateSortedDesc.jsonld b/webapi/src/test/resources/test-data/searchR2RV2/thingWithOptionalDateSortedDesc.jsonld index 7f09e295e5..9f3b67cfff 100644 --- a/webapi/src/test/resources/test-data/searchR2RV2/thingWithOptionalDateSortedDesc.jsonld +++ b/webapi/src/test/resources/test-data/searchR2RV2/thingWithOptionalDateSortedDesc.jsonld @@ -551,30 +551,6 @@ "@value" : "http://0.0.0.0:3336/ark:/72163/1/0001/a=thing=with=pictureE.20160302T150510Z" }, "rdfs:label" : "A thing with a picture" - }, { - "@id" : "http://rdfh.ch/0000/forbiddenResource", - "@type" : "knora-api:ForbiddenResource", - "knora-api:arkUrl" : { - "@type" : "xsd:anyURI", - "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV" - }, - "knora-api:attachedToProject" : { - "@id" : "http://www.knora.org/ontology/knora-admin#SystemProject" - }, - "knora-api:attachedToUser" : { - "@id" : "http://rdfh.ch/users/root" - }, - "knora-api:creationDate" : { - "@type" : "xsd:dateTimeStamp", - "@value" : "2017-10-06T11:05:37Z" - }, - "knora-api:hasPermissions" : "V knora-admin:UnknownUser", - "knora-api:userHasPermission" : "V", - "knora-api:versionArkUrl" : { - "@type" : "xsd:anyURI", - "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV.20171006T110537Z" - }, - "rdfs:label" : "This resource is a proxy for a resource you are not allowed to see" }, { "@id" : "http://rdfh.ch/0001/a-thing-with-text-valuesLanguage", "@type" : "anything:Thing", @@ -647,55 +623,8 @@ "@value" : "http://0.0.0.0:3336/ark:/72163/1/0001/cL5AwEioRLOm6Vrqwl1RmQ8.20161017T171604915Z" }, "rdfs:label" : "Oscar" - }, { - "@id" : "http://rdfh.ch/0000/forbiddenResource", - "@type" : "knora-api:ForbiddenResource", - "knora-api:arkUrl" : { - "@type" : "xsd:anyURI", - "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV" - }, - "knora-api:attachedToProject" : { - "@id" : "http://www.knora.org/ontology/knora-admin#SystemProject" - }, - "knora-api:attachedToUser" : { - "@id" : "http://rdfh.ch/users/root" - }, - "knora-api:creationDate" : { - "@type" : "xsd:dateTimeStamp", - "@value" : "2017-10-06T11:05:37Z" - }, - "knora-api:hasPermissions" : "V knora-admin:UnknownUser", - "knora-api:userHasPermission" : "V", - "knora-api:versionArkUrl" : { - "@type" : "xsd:anyURI", - "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV.20171006T110537Z" - }, - "rdfs:label" : "This resource is a proxy for a resource you are not allowed to see" - }, { - "@id" : "http://rdfh.ch/0000/forbiddenResource", - "@type" : "knora-api:ForbiddenResource", - "knora-api:arkUrl" : { - "@type" : "xsd:anyURI", - "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV" - }, - "knora-api:attachedToProject" : { - "@id" : "http://www.knora.org/ontology/knora-admin#SystemProject" - }, - "knora-api:attachedToUser" : { - "@id" : "http://rdfh.ch/users/root" - }, - "knora-api:creationDate" : { - "@type" : "xsd:dateTimeStamp", - "@value" : "2017-10-06T11:05:37Z" - }, - "knora-api:hasPermissions" : "V knora-admin:UnknownUser", - "knora-api:userHasPermission" : "V", - "knora-api:versionArkUrl" : { - "@type" : "xsd:anyURI", - "@value" : "http://0.0.0.0:3336/ark:/72163/1/0000/forbiddenResourceV.20171006T110537Z" - }, - "rdfs:label" : "This resource is a proxy for a resource you are not allowed to see" } ], + "knora-api:mayHaveMoreResults" : true, "@context" : { "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", 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 6daf8e9b96..fa342dd26c 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 @@ -126,7 +126,7 @@ class SearchRouteV2R2RSpec extends R2RSpec { // the response involves forbidden resource - val expectedAnswerJSONLD = readOrWriteTextFile(responseAs[String], new File("src/test/resources/test-data/searchR2RV2/searchResponseWithforbiddenResource.jsonld"), writeTestDataFiles) + val expectedAnswerJSONLD = readOrWriteTextFile(responseAs[String], new File("src/test/resources/test-data/searchR2RV2/searchResponseWithHiddenResource.jsonld"), writeTestDataFiles) compareJSONLDForResourcesResponse(expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = responseAs[String]) diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/SearchResponderV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/SearchResponderV2Spec.scala index fe2a13750f..3c2f948bd2 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/SearchResponderV2Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/SearchResponderV2Spec.scala @@ -174,8 +174,8 @@ class SearchResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case response: ReadResourcesSequenceV2 => - assert(response.resources.size == 3, s"3 results were expected, but ${response.resources.size} given") + case response: ResourceCountV2 => + assert(response.numberOfResources == 3, s"3 results were expected, but ${response.numberOfResources} given") } } @@ -190,8 +190,8 @@ class SearchResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case response: ReadResourcesSequenceV2 => - assert(response.resources.size == 3, s"3 results were expected, but ${response.resources.size} given") + case response: ResourceCountV2 => + assert(response.numberOfResources == 3, s"3 results were expected, but ${response.numberOfResources} given") } } diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala index 928a77b524..266a1eea6f 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala @@ -398,8 +398,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => - msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -491,7 +490,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -585,7 +584,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -775,7 +774,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[BadRequestException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[BadRequestException]) } } @@ -802,7 +801,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[NotFoundException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[NotFoundException]) } } @@ -826,7 +825,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[ForbiddenException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[ForbiddenException]) } } @@ -889,7 +888,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -1010,7 +1009,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -1077,7 +1076,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -1203,7 +1202,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -1313,7 +1312,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -1387,7 +1386,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -1456,7 +1455,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -1480,7 +1479,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[NotFoundException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[NotFoundException]) } } @@ -1549,7 +1548,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -1618,7 +1617,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -1687,7 +1686,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -1758,7 +1757,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { responderManager ! createValueRequest expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -1783,7 +1782,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { responderManager ! createValueRequest expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[BadRequestException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[BadRequestException]) } } @@ -1803,7 +1802,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[BadRequestException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[BadRequestException]) } } @@ -1828,9 +1827,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => - // msg.cause.isInstanceOf[NotFoundException] should ===(true) - msg.cause.isInstanceOf[NotFoundException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[NotFoundException]) } } @@ -1854,9 +1851,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => - // msg.cause.isInstanceOf[NotFoundException] should ===(true) - msg.cause.isInstanceOf[NotFoundException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[NotFoundException]) } } @@ -1881,8 +1876,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => - msg.cause.isInstanceOf[BadRequestException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[BadRequestException]) } } @@ -1905,7 +1899,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[OntologyConstraintException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[OntologyConstraintException]) } } @@ -1929,7 +1923,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[OntologyConstraintException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[OntologyConstraintException]) } // The cardinality of incunabula:seqnum in incunabula:page is 0-1, and page http://rdfh.ch/0803/4f11adaf already has a seqnum. @@ -1949,8 +1943,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => - msg.cause.isInstanceOf[OntologyConstraintException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[OntologyConstraintException]) } } @@ -2154,7 +2147,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[NotFoundException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[NotFoundException]) } } @@ -2179,7 +2172,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[ForbiddenException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[ForbiddenException]) } } @@ -2253,7 +2246,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[ForbiddenException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[ForbiddenException]) } } @@ -2280,7 +2273,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[BadRequestException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[BadRequestException]) } } @@ -2307,7 +2300,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[NotFoundException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[NotFoundException]) } } @@ -2378,7 +2371,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[ForbiddenException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[ForbiddenException]) } } @@ -2401,7 +2394,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[BadRequestException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[BadRequestException]) } } @@ -2424,7 +2417,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[NotFoundException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[NotFoundException]) } } @@ -2449,7 +2442,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -2589,7 +2582,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -2666,7 +2659,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -2745,7 +2738,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -2769,7 +2762,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -2836,7 +2829,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -2903,7 +2896,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -2986,7 +2979,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -3053,7 +3046,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -3120,7 +3113,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -3194,7 +3187,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -3263,7 +3256,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -3288,7 +3281,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[NotFoundException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[NotFoundException]) } } @@ -3357,7 +3350,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -3426,7 +3419,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -3495,7 +3488,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -3571,7 +3564,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { responderManager ! updateValueRequest expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -3651,7 +3644,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { responderManager ! updateValueRequest expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -3771,7 +3764,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[BadRequestException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[BadRequestException]) } } @@ -3806,7 +3799,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { responderManager ! updateValueRequest expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[DuplicateValueException]) } } @@ -3908,7 +3901,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { expectMsgPF(timeout) { case msg: akka.actor.Status.Failure => - msg.cause.isInstanceOf[ForbiddenException] should ===(true) + assert(msg.cause.isInstanceOf[ForbiddenException]) } } @@ -3942,8 +3935,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => - msg.cause.isInstanceOf[SipiException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[SipiException]) } } @@ -3965,7 +3957,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[ForbiddenException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[ForbiddenException]) } } @@ -4007,7 +3999,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[BadRequestException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[BadRequestException]) } } @@ -4086,7 +4078,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[OntologyConstraintException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[OntologyConstraintException]) } } @@ -4130,8 +4122,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => - msg.cause.isInstanceOf[ForbiddenException] should ===(true) + case msg: akka.actor.Status.Failure => assert(msg.cause.isInstanceOf[ForbiddenException]) } } From da23fc690385060867b2210bb7b2971953d5d2d2 Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Thu, 5 Mar 2020 15:25:39 +0100 Subject: [PATCH 11/23] feat(api-v2): Remove ForbiddenResource (ongoing). --- .../webapi/responders/v2/ValuesResponderV2.scala | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala index 407f732776..2c0fb9050d 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala @@ -1717,7 +1717,7 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde propertyIri: SmartIri, unverifiedValue: UnverifiedValueV2, requestingUser: UserADM): Future[VerifiedValueV2] = { - for { + val verifiedValueFuture: Future[VerifiedValueV2] = for { resourcesRequest <- Future { ResourcesGetRequestV2( resourceIris = Seq(resourceIri), @@ -1730,12 +1730,7 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } resourcesResponse <- (responderManager ? resourcesRequest).mapTo[ReadResourcesSequenceV2] - - resource = try { - resourcesResponse.toResource(resourceIri) - } catch { - case _: NotFoundException => throw UpdateNotPerformedException(s"Resource <$resourceIri> was not created. Please report this as a possible bug.") - } + resource = resourcesResponse.toResource(resourceIri) propertyValues = resource.values.getOrElse(propertyIri, throw UpdateNotPerformedException()) valueInTriplestore: ReadValueV2 = propertyValues.find(_.valueIri == unverifiedValue.newValueIri).getOrElse(throw UpdateNotPerformedException()) @@ -1760,6 +1755,10 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde value = unverifiedValue.valueContent, permissions = unverifiedValue.permissions ) + + verifiedValueFuture.recover { + case _: NotFoundException => throw UpdateNotPerformedException(s"Resource <$resourceIri> was not found. Please report this as a possible bug.") + } } /** From baeff8ab10c3acb6d9bef9346a33944615aad525 Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Thu, 5 Mar 2020 15:31:37 +0100 Subject: [PATCH 12/23] feat(api-v2): Remove ForbiddenResource (ongoing). --- .../knora/webapi/responders/v2/StandoffResponderV2.scala | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala index 46b8ab9c6c..83286d6c33 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala @@ -103,10 +103,6 @@ class StandoffResponderV2(responderData: ResponderData) extends Responder(respon // separate resources and values mainResourcesAndValueRdfData: ConstructResponseUtilV2.MainResourcesAndValueRdfData = ConstructResponseUtilV2.splitMainResourcesAndValueRdfData(constructQueryResults = resourceRequestResponse, requestingUser = getStandoffRequestV2.requestingUser) - _ = if (mainResourcesAndValueRdfData.resources.keySet != Set(getStandoffRequestV2.resourceIri)) { - throw NotFoundException(s"Resource <${getStandoffRequestV2.resourceIri}> was not found (maybe you do not have permission to see it, or it is marked as deleted)") - } - readResourcesSequenceV2: ReadResourcesSequenceV2 <- ConstructResponseUtilV2.createApiResponse( mainResourcesAndValueRdfData = mainResourcesAndValueRdfData, orderByResourceIri = Seq(getStandoffRequestV2.resourceIri), @@ -120,7 +116,7 @@ class StandoffResponderV2(responderData: ResponderData) extends Responder(respon requestingUser = getStandoffRequestV2.requestingUser ) - readResourceV2 = readResourcesSequenceV2.resources.headOption.getOrElse(throw NotFoundException(s"Resource <${getStandoffRequestV2.resourceIri}> not found")) + readResourceV2 = readResourcesSequenceV2.toResource(getStandoffRequestV2.resourceIri) valueObj: ReadValueV2 = readResourceV2.values.values.flatten.find(_.valueIri == getStandoffRequestV2.valueIri).getOrElse(throw NotFoundException(s"Value <${getStandoffRequestV2.valueIri}> not found in resource <${getStandoffRequestV2.resourceIri}> (maybe you do not have permission to see it, or it is marked as deleted)")) From 144863d09be379892657a3f85d17b06ff85f5f2e Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Thu, 5 Mar 2020 16:24:29 +0100 Subject: [PATCH 13/23] feat(api-v2): Remove ForbiddenResource (ongoing). --- .../v2/responder/resourcemessages/ResourceMessagesV2.scala | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index 99b8655fd7..bbb3a3b798 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -908,9 +908,14 @@ case class ReadResourcesSequenceV2(resources: Seq[ReadResourceV2], * @param requestedResourceIri the IRI of the expected resource. * @return the resource. * @throws NotFoundException if the resource is not found. + * @throws ForbiddenException if the user does not have permission to see the requested resource. * @throws BadRequestException if more than one resource was returned. */ def toResource(requestedResourceIri: IRI)(implicit stringFormatter: StringFormatter): ReadResourceV2 = { + if (hiddenResourceIris.contains(requestedResourceIri)) { + throw ForbiddenException(s"You do not have permission to see resource <$requestedResourceIri>") + } + if (resources.isEmpty) { throw NotFoundException(s"Expected <$requestedResourceIri>, but no resources were returned") } From f3ed00c37ae64e6759a565c49540d8563a8134fc Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Thu, 5 Mar 2020 16:29:15 +0100 Subject: [PATCH 14/23] refactor(api-v2): Move checkResourceIris into ReadResourcesSequenceV2. --- .../resourcemessages/ResourceMessagesV2.scala | 21 ++++++++++++++++ .../responders/v2/ResourcesResponderV2.scala | 24 ++----------------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index bbb3a3b798..1a6012e359 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -931,6 +931,27 @@ case class ReadResourcesSequenceV2(resources: Seq[ReadResourceV2], resources.head } + + /** + * Checks that requested resources were found and that the user has permission to see them. If not, throws an exception. + * + * @param targetResourceIris the IRIs to be checked. + * @param resourcesSequence the result of requesting those IRIs. + */ + def checkResourceIris(targetResourceIris: Set[IRI], resourcesSequence: ReadResourcesSequenceV2): Unit = { + val hiddenTargetResourceIris: Set[IRI] = targetResourceIris.intersect(resourcesSequence.hiddenResourceIris) + + if (hiddenTargetResourceIris.nonEmpty) { + throw ForbiddenException(s"You do not have permission to see one or more resources: ${hiddenTargetResourceIris.map(iri => s"<$iri>").mkString(", ")}") + } + + val missingResourceIris: Set[IRI] = targetResourceIris -- resourcesSequence.resources.map(_.resourceIri).toSet + + if (missingResourceIris.nonEmpty) { + throw NotFoundException(s"One or more resources were not found: ${missingResourceIris.map(iri => s"<$iri>").mkString(", ")}") + } + } + /** * Considers this [[ReadResourcesSequenceV2]] to be the result of an update operation in a single project * (since Knora never updates resources in more than one project at a time), and returns information about that diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index d868579cbb..878f0e0ac4 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -1216,7 +1216,7 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt requestingUser = requestingUser ) - _ = checkResourceIris( + _ = apiResponse.checkResourceIris( targetResourceIris = resourceIris.toSet, resourcesSequence = apiResponse ) @@ -1270,7 +1270,7 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt requestingUser = requestingUser ) - _ = checkResourceIris( + _ = apiResponse.checkResourceIris( targetResourceIris = resourceIris.toSet, resourcesSequence = apiResponse ) @@ -1541,26 +1541,6 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt } yield tei } - /** - * Checks that requested resources were found and that the user has permission to see them. If not, throws an exception. - * - * @param targetResourceIris the IRIs to be checked. - * @param resourcesSequence the result of requesting those IRIs. - */ - private def checkResourceIris(targetResourceIris: Set[IRI], resourcesSequence: ReadResourcesSequenceV2): Unit = { - val hiddenTargetResourceIris: Set[IRI] = targetResourceIris.intersect(resourcesSequence.hiddenResourceIris) - - if (hiddenTargetResourceIris.nonEmpty) { - throw ForbiddenException(s"You do not have permission to view one or more resources: ${hiddenTargetResourceIris.map(iri => s"<$iri>").mkString(", ")}") - } - - val missingResourceIris: Set[IRI] = targetResourceIris -- resourcesSequence.resources.map(_.resourceIri).toSet - - if (missingResourceIris.nonEmpty) { - throw NotFoundException(s"One or more resources were not found: ${missingResourceIris.map(iri => s"<$iri>").mkString(", ")}") - } - } - /** * Gets a graph of resources that are reachable via links to or from a given resource. * From 5de2b47593b6dd029e1647d68c016be170b25f49 Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Thu, 5 Mar 2020 18:01:34 +0100 Subject: [PATCH 15/23] style(api-v2): Clean up a few things. --- .../resourcemessages/ResourceMessagesV2.scala | 6 +++++- .../knora/webapi/responders/v2/SearchResponderV2.scala | 10 ++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index 1a6012e359..556e1f3a4f 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -810,7 +810,9 @@ object DeleteOrEraseResourceRequestV2 extends KnoraJsonLDRequestReaderV2[DeleteO /** * Represents a sequence of resources read back from Knora. * - * @param resources a sequence of resources. + * @param resources a sequence of resources. + * @param hiddenResourceIris the IRIs of resources that were requested but that the user did not have permission to see. + * @param mayHaveMoreResults `true` if more resources matching the request may be available. */ case class ReadResourcesSequenceV2(resources: Seq[ReadResourceV2], hiddenResourceIris: Set[IRI] = Set.empty, @@ -937,6 +939,8 @@ case class ReadResourcesSequenceV2(resources: Seq[ReadResourceV2], * * @param targetResourceIris the IRIs to be checked. * @param resourcesSequence the result of requesting those IRIs. + * @throws NotFoundException if the requested resources are not found. + * @throws ForbiddenException if the user does not have permission to see the requested resources. */ def checkResourceIris(targetResourceIris: Set[IRI], resourcesSequence: ReadResourcesSequenceV2): Unit = { val hiddenTargetResourceIris: Set[IRI] = targetResourceIris.intersect(resourcesSequence.hiddenResourceIris) diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala index 6968302775..cbc22f4e9a 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala @@ -215,7 +215,7 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand // the prequery returned no results, no further query is necessary Future( - ConstructResponseUtilV2.MainResourcesAndValueRdfData(resources = Map.empty[IRI, ConstructResponseUtilV2.ResourceWithValueRdfData]) + ConstructResponseUtilV2.MainResourcesAndValueRdfData(resources = Map.empty) ) } @@ -479,15 +479,13 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand inputQuery ) - } yield ConstructResponseUtilV2.MainResourcesAndValueRdfData( - resources = queryResWithFullGraphPatternOnlyRequestedValues, - hiddenMainResourceIris = queryResultsFilteredForPermissions.hiddenMainResourceIris, - hiddenDependentResourceIris = queryResultsFilteredForPermissions.hiddenDependentResourceIris + } yield queryResultsFilteredForPermissions.copy( + resources = queryResWithFullGraphPatternOnlyRequestedValues ) } else { // the prequery returned no results, no further query is necessary - Future(ConstructResponseUtilV2.MainResourcesAndValueRdfData(resources = Map.empty[IRI, ConstructResponseUtilV2.ResourceWithValueRdfData])) + Future(ConstructResponseUtilV2.MainResourcesAndValueRdfData(resources = Map.empty)) } // Find out whether to query standoff along with text values. This boolean value will be passed to From ba6157851c59bafb68db6b685e76c52a7df1bf33 Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Fri, 6 Mar 2020 09:44:30 +0100 Subject: [PATCH 16/23] docs(gravsearch): Update docs. --- .../paradox/03-apis/api-v2/query-language.md | 21 ++++++++++++++----- .../05-internals/design/api-v2/gravsearch.md | 4 ---- 2 files changed, 16 insertions(+), 9 deletions(-) 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 dce0eb64a4..0e93180cfb 100644 --- a/docs/src/paradox/03-apis/api-v2/query-language.md +++ b/docs/src/paradox/03-apis/api-v2/query-language.md @@ -186,9 +186,21 @@ that refer to events that took place within a certain date range. Each matching resource is returned with the values that the user has permission to see. If the user does not have permission to see a matching -main resource, it is replaced by a proxy resource called -`knora-api:ForbiddenResource`. If a user does not have permission to see -a matching dependent resource, only its IRI is returned. +main resource, it is hidden in the results. If a user does not have +permission to see a matching dependent resource, the link value is hidden. + +## Paging + +Gravsearch results are returned in pages. The maximum number of main +resources per page is determined by Knora (and can be configured +in `application.conf` via the setting `app/v2/resources-sequence/results-per-page`). +If some resources have been filtered out because the user does not have +permission to see them, a page could contain fewer results, or no results. +If it is possible that more results are available in subsequent pages, +the Gravsearch response will contain the predicate `knora-api:mayHaveMoreResults` +with the boolean value `true`, otherwise it will not contain this predicate. +Therefore, to retrieve all available results, the client must request each page +one at a time, until the response does not contain `knora-api:mayHaveMoreResults`. ## Inference @@ -227,8 +239,7 @@ clauses use the following patterns, with the specified restrictions: - `OFFSET`: the `OFFSET` is needed for paging. It does not actually refer to the number of triples to be returned, but to the requested page of results. The default value is 0, which refers - to the first page of results. The number of results per page is - defined in `app/v2` in `application.conf`. + to the first page of results. - `ORDER BY`: In SPARQL, the result of a `CONSTRUCT` query is an unordered set of triples. However, a Gravsearch query returns an ordered list of resources, which can be ordered by the values of diff --git a/docs/src/paradox/05-internals/design/api-v2/gravsearch.md b/docs/src/paradox/05-internals/design/api-v2/gravsearch.md index 882588347f..96d1ff1526 100644 --- a/docs/src/paradox/05-internals/design/api-v2/gravsearch.md +++ b/docs/src/paradox/05-internals/design/api-v2/gravsearch.md @@ -243,7 +243,3 @@ For each main resource, a check is performed for the presence of all resources a The method `getRequestedValuesFromResultsWithFullGraphPattern` filters out those resources and values that the user does not want to be returned by the query. All the resources and values not present in the input query's CONSTRUCT clause are filtered out. This only happens after permission checking. - -The main resources that have been filtered out due to insufficient permissions are represented by the placeholder `ForbiddenResource`. -This placeholder stands for a main resource that cannot be returned, nevertheless it informs the client that such a resource exists. -This is necessary for a consistent behaviour when doing paging. From 40fa1845314f7ef96e41d60fd62c31525525ffab Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Thu, 12 Mar 2020 09:12:55 +0100 Subject: [PATCH 17/23] test(gravsearch): Add client test data with paging. --- .../scala/org/knora/webapi/SharedTestDataADM.scala | 10 ++++++++++ .../org/knora/webapi/routing/v2/SearchRouteV2.scala | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/webapi/src/main/scala/org/knora/webapi/SharedTestDataADM.scala b/webapi/src/main/scala/org/knora/webapi/SharedTestDataADM.scala index 73e7b0a16a..d8fb4f90a5 100644 --- a/webapi/src/main/scala/org/knora/webapi/SharedTestDataADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/SharedTestDataADM.scala @@ -1610,6 +1610,16 @@ object SharedTestDataADM { | ?thing anything:hasOtherThing . |}""".stripMargin + val gravsearchThingsWithPaging: String = + """PREFIX anything: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?thing knora-api:isMainResource true . + |} WHERE { + | ?thing a anything:Thing . + |}""".stripMargin + val createResourceWithValues: String = """{ | "@type" : "anything:Thing", diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala index 6090def81a..30ac8295a8 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala @@ -346,7 +346,8 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit // Search results to return in test data. private val testSearches: Map[String, IRI] = Map( "things" -> SharedTestDataADM.gravsearchComplexThingSmallerThanDecimal, - "thing-links" -> SharedTestDataADM.gravsearchThingLinks + "thing-links" -> SharedTestDataADM.gravsearchThingLinks, + "things-with-paging" -> SharedTestDataADM.gravsearchThingsWithPaging ) private def getSearchTestResponses: Future[Set[TestDataFileContent]] = { From b46c4f0e35d27cf5176ff1ebdbdd2ea796487589 Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Fri, 20 Mar 2020 14:28:54 +0100 Subject: [PATCH 18/23] docs(gravsearch): Update design docs. --- .../05-internals/design/api-v2/gravsearch.md | 43 +++++++++++-------- .../v2/search/MainQueryResultProcessor.scala | 3 +- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/docs/src/paradox/05-internals/design/api-v2/gravsearch.md b/docs/src/paradox/05-internals/design/api-v2/gravsearch.md index 96d1ff1526..e8bf3104c8 100644 --- a/docs/src/paradox/05-internals/design/api-v2/gravsearch.md +++ b/docs/src/paradox/05-internals/design/api-v2/gravsearch.md @@ -221,25 +221,30 @@ the main query can specifically ask for more detailed information on these resou #### Generating the Main Query -The classes involved in generating prequeries can be found in `org.knora.webapi.responders.v2.search.gravsearch.mainquery`. - -The main query is a SPARQL CONSTRUCT query. Its generation is handled by the method `GravsearchMainQueryGenerator.createMainQuery`. -It takes three arguments: `mainResourceIris: Set[IriRef], dependentResourceIris: Set[IriRef], valueObjectIris: Set[IRI]`. -From the given Iris, statements are generated that ask for complete information on *exactly* these resources and values. -For any given resource Iri, only the values present in `valueObjectIris` are to be queried. -This is achieved by using SPARQL's `VALUES` expression for the main resource and dependent resources as well as for values. +The classes involved in generating the main query can be found in +`org.knora.webapi.responders.v2.search.gravsearch.mainquery`. + +The main query is a SPARQL CONSTRUCT query. Its generation is handled by the +method `GravsearchMainQueryGenerator.createMainQuery`. +It takes three arguments: `mainResourceIris: Set[IriRef], dependentResourceIris: +Set[IriRef], valueObjectIris: Set[IRI]`. From the given Iris, statements are +generated that ask for complete information on *exactly* these resources and +values. For any given resource Iri, only the values present in +`valueObjectIris` are to be queried. This is achieved by using SPARQL's +`VALUES` expression for the main resource and dependent resources as well as +for values. #### Processing the Main Query's results -When processing the main query's results, permissions are checked and resources and values that the user did not explicitly ask for in the input query are filtered out. This is implemented in `MainQueryResultProcessor`. - -The method `getMainQueryResultsWithFullGraphPattern` takes the main query's results as an input and makes sure that the client has sufficient permissions on the results. -A main resource and its dependent resources and values are only returned if the user has view permissions on all the resources and value objects present in the main query. -Otherwise the method suppresses the main resource. -To do the permission checking, the results of the main query are passed to `ConstructResponseUtilV2` which transforms a `SparqlConstructResponse` (a set of RDF triples) -into a structure organized by main resource Iris. In this structure, dependent resources and values are nested can be accessed via their main resource. -`SparqlConstructResponse` suppresses all resources and values the user has insufficient permissions on. -For each main resource, a check is performed for the presence of all resources and values after permission checking. - -The method `getRequestedValuesFromResultsWithFullGraphPattern` filters out those resources and values that the user does not want to be returned by the query. -All the resources and values not present in the input query's CONSTRUCT clause are filtered out. This only happens after permission checking. +To do the permission checking, the results of the main query are passed to +`ConstructResponseUtilV2.splitMainResourcesAndValueRdfData`, +which transforms a `SparqlConstructResponse` (a set of RDF triples) +into a structure organized by main resource Iris. In this structure, dependent +resources and values are nested can be accessed via their main resource, +and resources and values that the user does not have permission to see are +filtered out. As a result, a page of results may contain fewer than the maximum +allowed number of results per page, even if more pages of results are available. + +`MainQueryResultProcessor.getRequestedValuesFromResultsWithFullGraphPattern` +then filters out values that the user did not explicitly ask for in the input +query. diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/MainQueryResultProcessor.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/MainQueryResultProcessor.scala index 871ada421b..1e07f2d94e 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/search/MainQueryResultProcessor.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/search/MainQueryResultProcessor.scala @@ -20,8 +20,7 @@ package org.knora.webapi.responders.v2.search import org.knora.webapi._ -import org.knora.webapi.messages.admin.responder.usersmessages.UserADM -import org.knora.webapi.messages.store.triplestoremessages.{SparqlExtendedConstructResponse, SparqlSelectResponse, VariableResultsRow} +import org.knora.webapi.messages.store.triplestoremessages.{SparqlSelectResponse, VariableResultsRow} import org.knora.webapi.responders.v2.search.gravsearch.mainquery.GravsearchMainQueryGenerator import org.knora.webapi.responders.v2.search.gravsearch.prequery.NonTriplestoreSpecificGravsearchToPrequeryGenerator import org.knora.webapi.responders.v2.search.gravsearch.types.GravsearchTypeInspectionResult From 8e81c679459b9e2fe7dc0dd51b0794155ac25454 Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Fri, 20 Mar 2020 15:14:42 +0100 Subject: [PATCH 19/23] feat(upgrade): Increment knora-base version. - Simplify Gravsearch paging. - Add design docs. --- .../05-internals/design/api-v2/gravsearch.md | 6 ++++++ knora-ontologies/knora-base.ttl | 2 +- .../src/main/scala/org.knora.upgrade/Main.scala | 3 ++- .../src/main/scala/org/knora/webapi/package.scala | 2 +- .../webapi/util/ConstructResponseUtilV2.scala | 15 +++++---------- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/docs/src/paradox/05-internals/design/api-v2/gravsearch.md b/docs/src/paradox/05-internals/design/api-v2/gravsearch.md index e8bf3104c8..4cad65f0eb 100644 --- a/docs/src/paradox/05-internals/design/api-v2/gravsearch.md +++ b/docs/src/paradox/05-internals/design/api-v2/gravsearch.md @@ -248,3 +248,9 @@ allowed number of results per page, even if more pages of results are available. `MainQueryResultProcessor.getRequestedValuesFromResultsWithFullGraphPattern` then filters out values that the user did not explicitly ask for in the input query. + +Finally, `ConstructResponseUtilV2.createApiResponse` transforms the query +results into an API response (a `ReadResourcesSequenceV2`). If the number +of main resources found (even if filtered out because of permissions) is equal +to the maximum allowed page size, the predicate +`knora-api:mayHaveMoreResults: true` is included in the response. diff --git a/knora-ontologies/knora-base.ttl b/knora-ontologies/knora-base.ttl index 298793df7e..defaaca016 100644 --- a/knora-ontologies/knora-base.ttl +++ b/knora-ontologies/knora-base.ttl @@ -33,7 +33,7 @@ :attachedToProject knora-admin:SystemProject ; - :ontologyVersion "knora-base v7" . + :ontologyVersion "knora-base v8" . diff --git a/upgrade/src/main/scala/org.knora.upgrade/Main.scala b/upgrade/src/main/scala/org.knora.upgrade/Main.scala index b4293ae619..5dc3cdcebe 100644 --- a/upgrade/src/main/scala/org.knora.upgrade/Main.scala +++ b/upgrade/src/main/scala/org.knora.upgrade/Main.scala @@ -51,7 +51,8 @@ object Main extends App { PluginForKnoraBaseVersion(versionNumber = 4, plugin = new UpgradePluginPR1372, prBasedVersionString = Some("PR 1372")), PluginForKnoraBaseVersion(versionNumber = 5, plugin = new NoopPlugin, prBasedVersionString = Some("PR 1440")), PluginForKnoraBaseVersion(versionNumber = 6, plugin = new NoopPlugin), // PR 1206 - PluginForKnoraBaseVersion(versionNumber = 7, plugin = new NoopPlugin) // PR 1403 + PluginForKnoraBaseVersion(versionNumber = 7, plugin = new NoopPlugin), // PR 1403 + PluginForKnoraBaseVersion(versionNumber = 8, plugin = new NoopPlugin) // PR 1615 ) /** diff --git a/webapi/src/main/scala/org/knora/webapi/package.scala b/webapi/src/main/scala/org/knora/webapi/package.scala index 4ad30fa950..f2f6138055 100644 --- a/webapi/src/main/scala/org/knora/webapi/package.scala +++ b/webapi/src/main/scala/org/knora/webapi/package.scala @@ -27,7 +27,7 @@ package object webapi { * The version of `knora-base` and of the other built-in ontologies that this version of Knora requires. * Must be the same as the object of `knora-base:ontologyVersion` in the `knora-base` ontology being used. */ - val KnoraBaseVersion: String = "knora-base v7" + val KnoraBaseVersion: String = "knora-base v8" /** * `IRI` is a synonym for `String`, used to improve code readability. diff --git a/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala b/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala index d937a5ba77..0efd0fceca 100644 --- a/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala @@ -295,14 +295,11 @@ object ConstructResponseUtilV2 { * * @param resources a map of resource Iris to [[ResourceWithValueRdfData]]. The resource Iris represent main resources, dependent * resources are contained in the link values as nested structures. - * @param hiddenMainResourceIris the IRIs of main resources that were hidden because the user does not have permission + * @param hiddenResourceIris the IRIs of resources that were hidden because the user does not have permission * to see them. - * @param hiddenDependentResourceIris the IRIs of dependent resources that were hidden because the user does not have - * permission to see them. */ case class MainResourcesAndValueRdfData(resources: RdfResources, - hiddenMainResourceIris: Set[IRI] = Set.empty, - hiddenDependentResourceIris: Set[IRI] = Set.empty) + hiddenResourceIris: Set[IRI] = Set.empty) /** * A [[SparqlConstructResponse]] may contain both resources and value RDF data objects as well as standoff. @@ -683,8 +680,7 @@ object ConstructResponseUtilV2 { MainResourcesAndValueRdfData( resources = mainResourcesNested, - hiddenMainResourceIris = mainResourceIrisNotVisible, - hiddenDependentResourceIris = dependentResourceIrisNotVisible + hiddenResourceIris = mainResourceIrisNotVisible ++ dependentResourceIrisNotVisible ) } @@ -1291,11 +1287,10 @@ object ConstructResponseUtilV2 { for { resources <- Future.sequence(readResourceFutures) - mayHaveMoreResults = calculateMayHaveMoreResults && - settings.v2ResultsPerPage == (visibleResourceIris.size + mainResourcesAndValueRdfData.hiddenMainResourceIris.size) + mayHaveMoreResults = calculateMayHaveMoreResults && orderByResourceIri.size == settings.v2ResultsPerPage } yield ReadResourcesSequenceV2( resources = resources, - hiddenResourceIris = mainResourcesAndValueRdfData.hiddenMainResourceIris ++ mainResourcesAndValueRdfData.hiddenDependentResourceIris, + hiddenResourceIris = mainResourcesAndValueRdfData.hiddenResourceIris, mayHaveMoreResults = mayHaveMoreResults ) } From 65b9380de5f93e175d29ba7325b0347d981551c7 Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Mon, 23 Mar 2020 13:21:30 +0100 Subject: [PATCH 20/23] fix(knora-ontologies): Remove extra rdfs:comment. --- knora-ontologies/knora-base.ttl | 2 -- 1 file changed, 2 deletions(-) diff --git a/knora-ontologies/knora-base.ttl b/knora-ontologies/knora-base.ttl index defaaca016..94246c57e5 100644 --- a/knora-ontologies/knora-base.ttl +++ b/knora-ontologies/knora-base.ttl @@ -457,8 +457,6 @@ "a lien vers"@fr , "ha Link verso"@it ; - rdfs:comment "Represents a direct connection between two resources"@en ; - :isEditable true ; :objectClassConstraint :LinkValue ; From 6e964606936157a6198f41a7250bb86f58c6065f Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Tue, 7 Apr 2020 09:45:39 +0200 Subject: [PATCH 21/23] refactor: Fixes for review. --- .../05-internals/design/api-v2/gravsearch.md | 3 +- .../resourcemessages/ResourceMessagesV2.scala | 2 +- .../webapi/util/ConstructResponseUtilV2.scala | 285 +++++++++++------- .../knora/webapi/util/StringFormatter.scala | 30 +- 4 files changed, 212 insertions(+), 108 deletions(-) diff --git a/docs/src/paradox/05-internals/design/api-v2/gravsearch.md b/docs/src/paradox/05-internals/design/api-v2/gravsearch.md index 8f519b35ad..610e1af8c5 100644 --- a/docs/src/paradox/05-internals/design/api-v2/gravsearch.md +++ b/docs/src/paradox/05-internals/design/api-v2/gravsearch.md @@ -247,7 +247,7 @@ To do the permission checking, the results of the main query are passed to `ConstructResponseUtilV2.splitMainResourcesAndValueRdfData`, which transforms a `SparqlConstructResponse` (a set of RDF triples) into a structure organized by main resource Iris. In this structure, dependent -resources and values are nested can be accessed via their main resource, +resources and values are nested and can be accessed via their main resource, and resources and values that the user does not have permission to see are filtered out. As a result, a page of results may contain fewer than the maximum allowed number of results per page, even if more pages of results are available. @@ -286,5 +286,6 @@ as an optimisation if the triplestore provides it. For example, the virtual prop `knora-api:standoffTagHasStartAncestor` is equivalent to `knora-base:standoffTagHasStartParent*`, but with GraphDB it is implemented using a custom inference rule (in `KnoraRules.pie`) and is therefore more efficient. If Knora is not using the triplestore's inference, + `SparqlTransformer.transformStatementInWhereForNoInference` replaces `knora-api:standoffTagHasStartAncestor` with `knora-base:standoffTagHasStartParent*`. diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index 556e1f3a4f..af5618a8f6 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -810,7 +810,7 @@ object DeleteOrEraseResourceRequestV2 extends KnoraJsonLDRequestReaderV2[DeleteO /** * Represents a sequence of resources read back from Knora. * - * @param resources a sequence of resources. + * @param resources a sequence of resources that the user has permission to see. * @param hiddenResourceIris the IRIs of resources that were requested but that the user did not have permission to see. * @param mayHaveMoreResults `true` if more resources matching the request may be available. */ diff --git a/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala b/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala index 0efd0fceca..28b8f79bc7 100644 --- a/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala @@ -293,10 +293,10 @@ object ConstructResponseUtilV2 { /** * Represents a tree structure of resources, values and dependent resources returned by a SPARQL CONSTRUCT query. * - * @param resources a map of resource Iris to [[ResourceWithValueRdfData]]. The resource Iris represent main resources, dependent - * resources are contained in the link values as nested structures. - * @param hiddenResourceIris the IRIs of resources that were hidden because the user does not have permission - * to see them. + * @param resources a map of resource Iris to [[ResourceWithValueRdfData]]. The resource Iris represent main resources, dependent + * resources are contained in the link values as nested structures. + * @param hiddenResourceIris the IRIs of resources that were hidden because the user does not have permission + * to see them. */ case class MainResourcesAndValueRdfData(resources: RdfResources, hiddenResourceIris: Set[IRI] = Set.empty) @@ -501,7 +501,7 @@ object ConstructResponseUtilV2 { // Identify the resources that the user has permission to see. - val (visibleResources, hiddenResources) = flatResourcesWithValues.partition { + val (visibleResources: RdfResources, hiddenResources: RdfResources) = flatResourcesWithValues.partition { case (_: IRI, resource: ResourceWithValueRdfData) => resource.userPermission.nonEmpty } @@ -565,123 +565,193 @@ object ConstructResponseUtilV2 { resourceIri -> incomingLinksForRes } - /** - * Given a resource IRI, finds any link values in the resource, and recursively embeds the target resource in each link value. - * - * @param resourceIri the IRI of the resource to start with. - * @param alreadyTraversed a set (initially empty) of the IRIs of resources that this function has already - * traversed, to prevent an infinite loop if a cycle is encountered. - * @return the same resource, with any nested resources attached to it. - */ - def nestResources(resourceIri: IRI, alreadyTraversed: Set[IRI] = Set.empty[IRI]): ResourceWithValueRdfData = { - val resource = visibleResources(resourceIri) - - val transformedValuePropertyAssertions: RdfPropertyValues = resource.valuePropertyAssertions.map { - case (propIri, values) => - val transformedValues: Seq[ValueRdfData] = values.foldLeft(Vector.empty[ValueRdfData]) { - case (acc: Vector[ValueRdfData], value: ValueRdfData) => - if (value.valueObjectClass.toString == OntologyConstants.KnoraBase.LinkValue) { - val dependentResourceIri: IRI = value.requireIriObject(OntologyConstants.Rdf.Object.toSmartIri) - - if (alreadyTraversed(dependentResourceIri)) { - acc :+ value - } else { - // Do we have the dependent resource? - if (dependentResourceIrisVisible.contains(dependentResourceIri)) { - // Yes. Nest it in the link value. - val dependentResource: ResourceWithValueRdfData = nestResources(dependentResourceIri, alreadyTraversed + resourceIri) + val mainResourcesNested: Map[IRI, ResourceWithValueRdfData] = mainResourceIrisVisible.map { + resourceIri => + val transformedResource = nestResources( + resourceIri = resourceIri, + flatResourcesWithValues = flatResourcesWithValues, + visibleResources = visibleResources, + dependentResourceIrisVisible = dependentResourceIrisVisible, + dependentResourceIrisNotVisible = dependentResourceIrisNotVisible, + incomingLinksForResource = incomingLinksForResource + ) - acc :+ value.copy( - nestedResource = Some(dependentResource) - ) - } else if (dependentResourceIrisNotVisible.contains(dependentResourceIri)) { - // No, because the user doesn't have permission to see it. Skip the link value. - acc - } else { - // We don't have the dependent resource because it is marked as deleted. Just - // return the link value without a nested resource. - acc :+ value - } - } - } else { - acc :+ value - } - } + resourceIri -> transformedResource + }.toMap - propIri -> transformedValues - } + MainResourcesAndValueRdfData( + resources = mainResourcesNested, + hiddenResourceIris = mainResourceIrisNotVisible ++ dependentResourceIrisNotVisible + ) + } - // incomingLinksForResource contains incoming link values for each resource - // flatResourcesWithValues contains the complete information + /** + * Given a resource IRI, finds any link values in the resource, and recursively embeds the target resource in each link value. + * + * @param resourceIri the IRI of the resource to start with. + * @param flatResourcesWithValues the complete set of resources with their values, before permission filtering. + * @param visibleResources the resources that the user has permission to see. + * @param dependentResourceIrisVisible the IRIs of dependent resources that the user has permission to see. + * @param dependentResourceIrisNotVisible the IRIs of dependent resources that the user does not have permission to see. + * @param incomingLinksForResource a map of resource IRIs to resources that link to each resource. + * @param alreadyTraversed a set (initially empty) of the IRIs of resources that this function has already + * traversed, to prevent an infinite loop if a cycle is encountered. + * @return the same resource, with any nested resources attached to it. + */ + private def nestResources(resourceIri: IRI, + flatResourcesWithValues: RdfResources, + visibleResources: RdfResources, + dependentResourceIrisVisible: Set[IRI], + dependentResourceIrisNotVisible: Set[IRI], + incomingLinksForResource: Map[IRI, RdfResources], + alreadyTraversed: Set[IRI] = Set.empty[IRI])(implicit stringFormatter: StringFormatter): ResourceWithValueRdfData = { + val resource = visibleResources(resourceIri) + + val transformedValuePropertyAssertions: RdfPropertyValues = resource.valuePropertyAssertions.map { + case (propIri: SmartIri, values: Seq[ValueRdfData]) => + val transformedValues: Seq[ValueRdfData] = transformValuesByNestingResources( + resourceIri = resourceIri, + values = values, + flatResourcesWithValues = flatResourcesWithValues, + visibleResources = visibleResources, + dependentResourceIrisVisible = dependentResourceIrisVisible, + dependentResourceIrisNotVisible = dependentResourceIrisNotVisible, + incomingLinksForResource = incomingLinksForResource, + alreadyTraversed = alreadyTraversed + resourceIri + ) - // filter out those resources that already have been processed - // and the main resources (they are already present on the top level of the response) - // - // the main resources point to dependent resources and would be treated as incoming links of dependent resources - // this would create circular dependencies + propIri -> transformedValues + } - // resources that point to this resource - val referringResources: RdfResources = incomingLinksForResource(resourceIri).filterNot { - case (incomingResIri: IRI, _: ResourceWithValueRdfData) => - alreadyTraversed(incomingResIri) || flatResourcesWithValues(incomingResIri).isMainResource - } + // incomingLinksForResource contains incoming link values for each resource + // flatResourcesWithValues contains the complete information - // link value assertions that point to this resource - val incomingLinkAssertions: RdfPropertyValues = referringResources.values.foldLeft(emptyRdfPropertyValues) { - case (acc: RdfPropertyValues, assertions: ResourceWithValueRdfData) => + // filter out those resources that already have been processed + // and the main resources (they are already present on the top level of the response) + // + // the main resources point to dependent resources and would be treated as incoming links of dependent resources + // this would create circular dependencies - val values: RdfPropertyValues = assertions.valuePropertyAssertions.flatMap { - case (propIri: SmartIri, values: Seq[ValueRdfData]) => + // resources that point to this resource + val referringResources: RdfResources = incomingLinksForResource(resourceIri).filterNot { + case (incomingResIri: IRI, _: ResourceWithValueRdfData) => + alreadyTraversed(incomingResIri) || flatResourcesWithValues(incomingResIri).isMainResource + } - // check if the property Iri already exists (there could be several instances of the same property) - if (acc.get(propIri).nonEmpty) { - // add values to property Iri (keeping the already existing values) - acc + (propIri -> (acc(propIri) ++ values).sortBy(_.subjectIri)) - } else { - // prop Iri does not exists yet, add it - acc + (propIri -> values.sortBy(_.subjectIri)) - } - } + // link value assertions that point to this resource + val incomingLinkAssertions: RdfPropertyValues = referringResources.values.foldLeft(emptyRdfPropertyValues) { + case (acc: RdfPropertyValues, assertions: ResourceWithValueRdfData) => - values - } + val values: RdfPropertyValues = assertions.valuePropertyAssertions.flatMap { + case (propIri: SmartIri, values: Seq[ValueRdfData]) => - if (incomingLinkAssertions.nonEmpty) { - // create a virtual property representing an incoming link - val incomingProps: (SmartIri, Seq[ValueRdfData]) = OntologyConstants.KnoraBase.HasIncomingLinkValue.toSmartIri -> incomingLinkAssertions.values.toSeq.flatten.map { - linkValue: ValueRdfData => + // check if the property Iri already exists (there could be several instances of the same property) + if (acc.get(propIri).nonEmpty) { + // add values to property Iri (keeping the already existing values) + acc + (propIri -> (acc(propIri) ++ values).sortBy(_.subjectIri)) + } else { + // prop Iri does not exists yet, add it + acc + (propIri -> values.sortBy(_.subjectIri)) + } + } - // get the source of the link value (it points to the resource that is currently processed) - val sourceIri: IRI = linkValue.requireIriObject(OntologyConstants.Rdf.Subject.toSmartIri) - val source = Some(nestResources(resourceIri = sourceIri, alreadyTraversed = alreadyTraversed + resourceIri)) + values + } - linkValue.copy( - nestedResource = source, - isIncomingLink = true + if (incomingLinkAssertions.nonEmpty) { + // create a virtual property representing an incoming link + val incomingProps: (SmartIri, Seq[ValueRdfData]) = OntologyConstants.KnoraBase.HasIncomingLinkValue.toSmartIri -> incomingLinkAssertions.values.toSeq.flatten.map { + linkValue: ValueRdfData => + + // get the source of the link value (it points to the resource that is currently processed) + val sourceIri: IRI = linkValue.requireIriObject(OntologyConstants.Rdf.Subject.toSmartIri) + val source = Some( + nestResources( + resourceIri = sourceIri, + flatResourcesWithValues = flatResourcesWithValues, + visibleResources = visibleResources, + dependentResourceIrisVisible = dependentResourceIrisVisible, + dependentResourceIrisNotVisible = dependentResourceIrisNotVisible, + incomingLinksForResource = incomingLinksForResource, + alreadyTraversed = alreadyTraversed + resourceIri ) - } + ) - resource.copy( - valuePropertyAssertions = transformedValuePropertyAssertions + incomingProps - ) - } else { - resource.copy( - valuePropertyAssertions = transformedValuePropertyAssertions - ) + linkValue.copy( + nestedResource = source, + isIncomingLink = true + ) } + resource.copy( + valuePropertyAssertions = transformedValuePropertyAssertions + incomingProps + ) + } else { + resource.copy( + valuePropertyAssertions = transformedValuePropertyAssertions + ) } + } - val mainResourcesNested: Map[IRI, ResourceWithValueRdfData] = mainResourceIrisVisible.map { - resourceIri => - val transformedResource = nestResources(resourceIri) - resourceIri -> transformedResource - }.toMap + /** + * Transforms a resource's values by nesting dependent resources in link values. + * + * @param resourceIri the IRI of the resource. + * @param values the values of the resource. + * @param flatResourcesWithValues the complete set of resources with their values, before permission filtering. + * @param visibleResources the resources that the user has permission to see. + * @param dependentResourceIrisVisible the IRIs of dependent resources that the user has permission to see. + * @param dependentResourceIrisNotVisible the IRIs of dependent resources that the user does not have permission to see. + * @param incomingLinksForResource a map of resource IRIs to resources that link to each resource. + * @param alreadyTraversed a set (initially empty) of the IRIs of resources that this function has already + * traversed, to prevent an infinite loop if a cycle is encountered. + * @return the transformed values. + */ + private def transformValuesByNestingResources(resourceIri: IRI, + values: Seq[ValueRdfData], + flatResourcesWithValues: RdfResources, + visibleResources: RdfResources, + dependentResourceIrisVisible: Set[IRI], + dependentResourceIrisNotVisible: Set[IRI], + incomingLinksForResource: Map[IRI, RdfResources], + alreadyTraversed: Set[IRI])(implicit stringFormatter: StringFormatter): Seq[ValueRdfData] = { + values.foldLeft(Vector.empty[ValueRdfData]) { + case (acc: Vector[ValueRdfData], value: ValueRdfData) => + if (value.valueObjectClass.toString == OntologyConstants.KnoraBase.LinkValue) { + val dependentResourceIri: IRI = value.requireIriObject(OntologyConstants.Rdf.Object.toSmartIri) + + if (alreadyTraversed(dependentResourceIri)) { + acc :+ value + } else { + // Do we have the dependent resource? + if (dependentResourceIrisVisible.contains(dependentResourceIri)) { + // Yes. Nest it in the link value. + val dependentResource: ResourceWithValueRdfData = nestResources( + resourceIri = dependentResourceIri, + flatResourcesWithValues = flatResourcesWithValues, + visibleResources = visibleResources, + dependentResourceIrisVisible = dependentResourceIrisVisible, + dependentResourceIrisNotVisible = dependentResourceIrisNotVisible, + incomingLinksForResource = incomingLinksForResource, + alreadyTraversed = alreadyTraversed + ) - MainResourcesAndValueRdfData( - resources = mainResourcesNested, - hiddenResourceIris = mainResourceIrisNotVisible ++ dependentResourceIrisNotVisible - ) + acc :+ value.copy( + nestedResource = Some(dependentResource) + ) + } else if (dependentResourceIrisNotVisible.contains(dependentResourceIri)) { + // No, because the user doesn't have permission to see it. Skip the link value. + acc + } else { + // We don't have the dependent resource because it is marked as deleted. Just + // return the link value without a nested resource. + acc :+ value + } + } + } else { + acc :+ value + } + } } /** @@ -1245,7 +1315,9 @@ object ConstructResponseUtilV2 { * Creates an API response. * * @param mainResourcesAndValueRdfData the query results. - * @param orderByResourceIri the order in which the resources should be returned. + * @param orderByResourceIri the order in which the resources should be returned. This sequence + * contains the resource IRIs received from the triplestore before filtering + * for permissions. * @param mappings the mappings to convert standoff to XML, if any. * @param queryStandoff if `true`, make separate queries to get the standoff for text values. * @param versionDate if defined, represents the requested time in the the resources' version history. @@ -1268,7 +1340,7 @@ object ConstructResponseUtilV2 { val visibleResourceIris: Seq[IRI] = orderByResourceIri.filter(resourceIri => mainResourcesAndValueRdfData.resources.keySet.contains(resourceIri)) - // iterate over orderByResourceIris and construct the response in the correct order + // iterate over visibleResourceIris and construct the response in the correct order val readResourceFutures: Vector[Future[ReadResourceV2]] = visibleResourceIris.map { resourceIri: IRI => constructReadResourceV2( @@ -1287,6 +1359,9 @@ object ConstructResponseUtilV2 { for { resources <- Future.sequence(readResourceFutures) + + // If we got a full page of results from the triplestore (before filtering for permissions), there + // might be at least one more page of results that the user could request. mayHaveMoreResults = calculateMayHaveMoreResults && orderByResourceIri.size == settings.v2ResultsPerPage } yield ReadResourcesSequenceV2( resources = resources, diff --git a/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala b/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala index fe642971a8..c86cac85d1 100644 --- a/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala +++ b/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala @@ -40,7 +40,6 @@ import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM import org.knora.webapi.messages.store.triplestoremessages.{SparqlAskRequest, SparqlAskResponse} import org.knora.webapi.messages.v1.responder.projectmessages.ProjectInfoV1 import org.knora.webapi.messages.v2.responder.KnoraContentV2 -import org.knora.webapi.messages.v2.responder.resourcemessages.ReadResourceV2 import org.knora.webapi.messages.v2.responder.standoffmessages._ import org.knora.webapi.util.IriConversions._ import org.knora.webapi.util.JavaUtil.Optional @@ -3021,6 +3020,35 @@ class StringFormatter private(val maybeSettings: Option[SettingsImpl] = None, ma s"http://$IriDomain/permissions/$shortcode/$knoraPermissionUuid" } + /** + * Creates an IRI for a `knora-base:Map`. + * + * @param mapPath the map's path, which must be a sequence of names separated by slashes (`/`). Each name must + * be a valid XML [[https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-NCName NCName]]. + * @return the IRI of the map. + */ + def makeMapIri(mapPath: String): IRI = { + s"http://$IriDomain/maps/$mapPath" + } + + /** + * Extracts the path of a persistent map from the IRI of a `knora-base:Map`. + * + * @param mapIri the IRI of the `knora-base:Map`. + * @return the map's path. + */ + def mapIriToMapPath(mapIri: IRI): String = { + mapIri.stripPrefix(s"http://$IriDomain/maps/") + } + + /** + * Creates a random IRI for a `knora-base:MapEntry`. + */ + def makeRandomMapEntryIri: IRI = { + val mapEntryUuid = makeRandomBase64EncodedUuid + s"http://$IriDomain/map-entries/$mapEntryUuid" + } + /** * Converts a camel-case string like `FooBar` into a string like `foo-bar`. * From 35fafe4f6ae9afc0a534b254095da889bab43ca7 Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Tue, 7 Apr 2020 11:38:34 +0200 Subject: [PATCH 22/23] fix(api-v2): If we filtered out all the link values of a property, filter out the property. --- webapi/_test_data/all_data/anything-data.ttl | 55 ++++++++++++++++++ .../webapi/util/ConstructResponseUtilV2.scala | 4 ++ .../ThingWithOneHiddenResource.jsonld | 56 +++++++++++++++++++ .../e2e/v2/ResourcesRouteV2E2ESpec.scala | 17 ++++++ 4 files changed, 132 insertions(+) create mode 100644 webapi/src/test/resources/test-data/resourcesR2RV2/ThingWithOneHiddenResource.jsonld diff --git a/webapi/_test_data/all_data/anything-data.ttl b/webapi/_test_data/all_data/anything-data.ttl index 687426ae70..d180e400c2 100644 --- a/webapi/_test_data/all_data/anything-data.ttl +++ b/webapi/_test_data/all_data/anything-data.ttl @@ -1618,6 +1618,61 @@ knora-base:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:UnknownUser"; knora-base:attachedToUser . + a anything:Thing; + knora-base:attachedToUser ; + knora-base:attachedToProject ; + knora-base:hasPermissions "V knora-admin:UnknownUser|M knora-admin:ProjectMember"; + knora-base:creationDate "2020-04-07T09:12:56.710717Z"^^xsd:dateTime; + anything:hasOtherThingValue ; + anything:hasOtherThing ; + anything:hasInteger ; + rdfs:label "thing with one hidden thing"; + knora-base:isDeleted false . + + a knora-base:LinkValue; + knora-base:valueHasUUID "UgSp5mXTTSKdI02ZU1KIAA"^^xsd:string; + rdf:subject ; + rdf:predicate anything:hasOtherThing; + rdf:object ; + knora-base:isDeleted false; + knora-base:attachedToUser ; + knora-base:hasPermissions "V knora-admin:UnknownUser|M knora-admin:ProjectMember"; + knora-base:valueCreationDate "2020-04-07T09:12:56.710717Z"^^xsd:dateTime; + knora-base:valueHasString "http://rdfh.ch/0001/XTxSMt0ySraVmwXD-bD2wQ"; + knora-base:valueHasComment "link value pointing to hidden resource"; + knora-base:valueHasRefCount "1"^^xsd:int . + + a knora-base:IntValue; + knora-base:attachedToUser ; + knora-base:hasPermissions "V knora-admin:UnknownUser|M knora-admin:ProjectMember"; + knora-base:valueHasUUID "U1PwfNaVRQebbOSFWNdMqQ"^^xsd:string; + knora-base:isDeleted false; + knora-base:valueCreationDate "2020-04-07T09:12:56.710717Z"^^xsd:dateTime; + knora-base:valueHasInteger 123454321; + knora-base:valueHasOrder 0; + knora-base:valueHasComment "visible int value in main resource"; + knora-base:valueHasString "123454321" . + + a anything:Thing; + knora-base:attachedToUser ; + knora-base:attachedToProject ; + knora-base:hasPermissions "V knora-admin:Creator"; + knora-base:creationDate "2020-04-07T09:12:56.710717Z"^^xsd:dateTime; + anything:hasInteger ; + rdfs:label "hidden thing"; + knora-base:isDeleted false . + + a knora-base:IntValue; + knora-base:attachedToUser ; + knora-base:hasPermissions "V knora-admin:Creator"; + knora-base:valueHasUUID "PVPAa37xR--K_wxQwlvSsg"^^xsd:string; + knora-base:isDeleted false; + knora-base:valueCreationDate "2020-04-07T09:12:56.710717Z"^^xsd:dateTime; + knora-base:valueHasInteger 123454321; + knora-base:valueHasOrder 0; + knora-base:valueHasComment "hidden int value in hidden resource"; + knora-base:valueHasString "123454321" . + a knora-base:ListNode; knora-base:isRootNode true; rdfs:label "Tree list root"@en; diff --git a/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala b/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala index 28b8f79bc7..d0ed7674e9 100644 --- a/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/util/ConstructResponseUtilV2.scala @@ -621,6 +621,10 @@ object ConstructResponseUtilV2 { ) propIri -> transformedValues + }.filter { + case (_: SmartIri, values: Seq[ValueRdfData]) => + // If we filtered out all the values for the property, filter out the property, too. + values.nonEmpty } // incomingLinksForResource contains incoming link values for each resource diff --git a/webapi/src/test/resources/test-data/resourcesR2RV2/ThingWithOneHiddenResource.jsonld b/webapi/src/test/resources/test-data/resourcesR2RV2/ThingWithOneHiddenResource.jsonld new file mode 100644 index 0000000000..93d9a087b6 --- /dev/null +++ b/webapi/src/test/resources/test-data/resourcesR2RV2/ThingWithOneHiddenResource.jsonld @@ -0,0 +1,56 @@ +{ + "@id" : "http://rdfh.ch/0001/0JhgKcqoRIeRRG6ownArSw", + "@type" : "anything:Thing", + "anything:hasInteger" : { + "@id" : "http://rdfh.ch/0001/0JhgKcqoRIeRRG6ownArSw/values/U1PwfNaVRQebbOSFWNdMqQ", + "@type" : "knora-api:IntValue", + "knora-api:arkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0001/0JhgKcqoRIeRRG6ownArSwb/U1PwfNaVRQebbOSFWNdMqQ7" + }, + "knora-api:attachedToUser" : { + "@id" : "http://rdfh.ch/users/9XBCrDV3SRa7kS1WwynB4Q" + }, + "knora-api:hasPermissions" : "V knora-admin:UnknownUser|M knora-admin:ProjectMember", + "knora-api:intValueAsInt" : 123454321, + "knora-api:userHasPermission" : "V", + "knora-api:valueCreationDate" : { + "@type" : "xsd:dateTimeStamp", + "@value" : "2020-04-07T09:12:56.710717Z" + }, + "knora-api:valueHasComment" : "visible int value in main resource", + "knora-api:valueHasUUID" : "U1PwfNaVRQebbOSFWNdMqQ", + "knora-api:versionArkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0001/0JhgKcqoRIeRRG6ownArSwb/U1PwfNaVRQebbOSFWNdMqQ7.20200407T091256710717Z" + } + }, + "knora-api:arkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0001/0JhgKcqoRIeRRG6ownArSwb" + }, + "knora-api:attachedToProject" : { + "@id" : "http://rdfh.ch/projects/0001" + }, + "knora-api:attachedToUser" : { + "@id" : "http://rdfh.ch/users/9XBCrDV3SRa7kS1WwynB4Q" + }, + "knora-api:creationDate" : { + "@type" : "xsd:dateTimeStamp", + "@value" : "2020-04-07T09:12:56.710717Z" + }, + "knora-api:hasPermissions" : "V knora-admin:UnknownUser|M knora-admin:ProjectMember", + "knora-api:userHasPermission" : "V", + "knora-api:versionArkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0001/0JhgKcqoRIeRRG6ownArSwb.20200407T091256710717Z" + }, + "rdfs:label" : "thing with one hidden thing", + "@context" : { + "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", + "rdfs" : "http://www.w3.org/2000/01/rdf-schema#", + "xsd" : "http://www.w3.org/2001/XMLSchema#", + "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#" + } +} \ No newline at end of file diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala index 5dc4365cac..3d8c3a7029 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala @@ -330,6 +330,23 @@ class ResourcesRouteV2E2ESpec extends E2ESpec(ResourcesRouteV2E2ESpec.config) { ) } + "perform a full resource request with a link to a resource that the user doesn't have permission to see" in { + val request = Get(s"$baseApiUrl/v2/resources/${URLEncoder.encode("http://rdfh.ch/0001/0JhgKcqoRIeRRG6ownArSw", "UTF-8")}") + + val response: HttpResponse = singleAwaitingRequest(request) + val responseAsString = responseToString(response) + assert(response.status == StatusCodes.OK, responseAsString) + val expectedAnswerJSONLD = readOrWriteTextFile(responseAsString, new File("src/test/resources/test-data/resourcesR2RV2/ThingWithOneHiddenResource.jsonld"), writeTestDataFiles) + compareJSONLDForResourcesResponse(expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = responseAsString) + + // Check that the resource corresponds to the ontology. + instanceChecker.check( + instanceResponse = responseAsString, + expectedClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, + knoraRouteGet = doGetRequest + ) + } + "perform a full resource request for a past version of a resource, using a URL-encoded xsd:dateTimeStamp" in { val request = Get(s"$baseApiUrl/v2/resources/${URLEncoder.encode("http://rdfh.ch/0001/thing-with-history", "UTF-8")}?version=${URLEncoder.encode("2019-02-12T08:05:10.351Z", "UTF-8")}") val response: HttpResponse = singleAwaitingRequest(request) From b990663952d03fa3208a6c68d726a9a1aeea32a8 Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Tue, 7 Apr 2020 17:11:38 +0200 Subject: [PATCH 23/23] test(api-v2): Test getting a resource with a link to a deleted resource. --- webapi/_test_data/all_data/anything-data.ttl | 56 +++++++++++++ .../ThingWithOneDeletedResource.jsonld | 82 +++++++++++++++++++ .../e2e/v2/ResourcesRouteV2E2ESpec.scala | 17 ++++ 3 files changed, 155 insertions(+) create mode 100644 webapi/src/test/resources/test-data/resourcesR2RV2/ThingWithOneDeletedResource.jsonld diff --git a/webapi/_test_data/all_data/anything-data.ttl b/webapi/_test_data/all_data/anything-data.ttl index d180e400c2..827d93fc24 100644 --- a/webapi/_test_data/all_data/anything-data.ttl +++ b/webapi/_test_data/all_data/anything-data.ttl @@ -1673,6 +1673,62 @@ knora-base:valueHasComment "hidden int value in hidden resource"; knora-base:valueHasString "123454321" . + a anything:Thing; + knora-base:attachedToUser ; + knora-base:attachedToProject ; + knora-base:hasPermissions "V knora-admin:UnknownUser|M knora-admin:ProjectMember"; + knora-base:creationDate "2020-04-07T09:12:56.710717Z"^^xsd:dateTime; + anything:hasOtherThingValue ; + anything:hasOtherThing ; + anything:hasInteger ; + rdfs:label "thing with one deleted thing"; + knora-base:isDeleted false . + + a knora-base:LinkValue; + knora-base:valueHasUUID "Nlcc7XWXQtmEITsIRQ5z4w"^^xsd:string; + rdf:subject ; + rdf:predicate anything:hasOtherThing; + rdf:object ; + knora-base:isDeleted false; + knora-base:attachedToUser ; + knora-base:hasPermissions "V knora-admin:UnknownUser|M knora-admin:ProjectMember"; + knora-base:valueCreationDate "2020-04-07T09:12:56.710717Z"^^xsd:dateTime; + knora-base:valueHasString "http://rdfh.ch/0001/XTxSMt0ySraVmwXD-bD2wQ"; + knora-base:valueHasComment "link value pointing to deleted resource"; + knora-base:valueHasRefCount "1"^^xsd:int . + + a knora-base:IntValue; + knora-base:attachedToUser ; + knora-base:hasPermissions "V knora-admin:UnknownUser|M knora-admin:ProjectMember"; + knora-base:valueHasUUID "a-40v6WiT4GHa79Kqwojjw"^^xsd:string; + knora-base:isDeleted false; + knora-base:valueCreationDate "2020-04-07T09:12:56.710717Z"^^xsd:dateTime; + knora-base:valueHasInteger 123454321; + knora-base:valueHasOrder 0; + knora-base:valueHasComment "int value in main resource"; + knora-base:valueHasString "123454321" . + + a anything:Thing; + knora-base:attachedToUser ; + knora-base:attachedToProject ; + knora-base:hasPermissions "V knora-admin:UnknownUser|M knora-admin:ProjectMember"; + knora-base:creationDate "2020-04-07T09:12:56.710717Z"^^xsd:dateTime; + anything:hasInteger ; + rdfs:label "deleted thing"; + knora-base:isDeleted true; + knora-base:deleteDate "2020-04-07T14:59:28.960124Z"^^xsd:dateTime . + + a knora-base:IntValue; + knora-base:attachedToUser ; + knora-base:hasPermissions "V knora-admin:UnknownUser|M knora-admin:ProjectMember"; + knora-base:valueHasUUID "7BIm9QAiQqKixcgXDWf12Q"^^xsd:string; + knora-base:isDeleted false; + knora-base:valueCreationDate "2020-04-07T09:12:56.710717Z"^^xsd:dateTime; + knora-base:valueHasInteger 123454321; + knora-base:valueHasOrder 0; + knora-base:valueHasComment "int value in deleted resource"; + knora-base:valueHasString "123454321" . + a knora-base:ListNode; knora-base:isRootNode true; rdfs:label "Tree list root"@en; diff --git a/webapi/src/test/resources/test-data/resourcesR2RV2/ThingWithOneDeletedResource.jsonld b/webapi/src/test/resources/test-data/resourcesR2RV2/ThingWithOneDeletedResource.jsonld new file mode 100644 index 0000000000..760014ef5d --- /dev/null +++ b/webapi/src/test/resources/test-data/resourcesR2RV2/ThingWithOneDeletedResource.jsonld @@ -0,0 +1,82 @@ +{ + "@id" : "http://rdfh.ch/0001/l8f8FVEiSCeq9A1p8gBR-A", + "@type" : "anything:Thing", + "anything:hasInteger" : { + "@id" : "http://rdfh.ch/0001/l8f8FVEiSCeq9A1p8gBR-A/values/a-40v6WiT4GHa79Kqwojjw", + "@type" : "knora-api:IntValue", + "knora-api:arkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0001/l8f8FVEiSCeq9A1p8gBR=A6/a=40v6WiT4GHa79Kqwojjw0" + }, + "knora-api:attachedToUser" : { + "@id" : "http://rdfh.ch/users/9XBCrDV3SRa7kS1WwynB4Q" + }, + "knora-api:hasPermissions" : "V knora-admin:UnknownUser|M knora-admin:ProjectMember", + "knora-api:intValueAsInt" : 123454321, + "knora-api:userHasPermission" : "V", + "knora-api:valueCreationDate" : { + "@type" : "xsd:dateTimeStamp", + "@value" : "2020-04-07T09:12:56.710717Z" + }, + "knora-api:valueHasComment" : "int value in main resource", + "knora-api:valueHasUUID" : "a-40v6WiT4GHa79Kqwojjw", + "knora-api:versionArkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0001/l8f8FVEiSCeq9A1p8gBR=A6/a=40v6WiT4GHa79Kqwojjw0.20200407T091256710717Z" + } + }, + "anything:hasOtherThingValue" : { + "@id" : "http://rdfh.ch/0001/l8f8FVEiSCeq9A1p8gBR-A/values/Nlcc7XWXQtmEITsIRQ5z4w", + "@type" : "knora-api:LinkValue", + "knora-api:arkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0001/l8f8FVEiSCeq9A1p8gBR=A6/Nlcc7XWXQtmEITsIRQ5z4wY" + }, + "knora-api:attachedToUser" : { + "@id" : "http://rdfh.ch/users/9XBCrDV3SRa7kS1WwynB4Q" + }, + "knora-api:hasPermissions" : "V knora-admin:UnknownUser|M knora-admin:ProjectMember", + "knora-api:linkValueHasTargetIri" : { + "@id" : "http://rdfh.ch/0001/PHbbrEsVR32q5D_ioKt6pA" + }, + "knora-api:userHasPermission" : "V", + "knora-api:valueCreationDate" : { + "@type" : "xsd:dateTimeStamp", + "@value" : "2020-04-07T09:12:56.710717Z" + }, + "knora-api:valueHasComment" : "link value pointing to deleted resource", + "knora-api:valueHasUUID" : "Nlcc7XWXQtmEITsIRQ5z4w", + "knora-api:versionArkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0001/l8f8FVEiSCeq9A1p8gBR=A6/Nlcc7XWXQtmEITsIRQ5z4wY.20200407T091256710717Z" + } + }, + "knora-api:arkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0001/l8f8FVEiSCeq9A1p8gBR=A6" + }, + "knora-api:attachedToProject" : { + "@id" : "http://rdfh.ch/projects/0001" + }, + "knora-api:attachedToUser" : { + "@id" : "http://rdfh.ch/users/9XBCrDV3SRa7kS1WwynB4Q" + }, + "knora-api:creationDate" : { + "@type" : "xsd:dateTimeStamp", + "@value" : "2020-04-07T09:12:56.710717Z" + }, + "knora-api:hasPermissions" : "V knora-admin:UnknownUser|M knora-admin:ProjectMember", + "knora-api:userHasPermission" : "V", + "knora-api:versionArkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0001/l8f8FVEiSCeq9A1p8gBR=A6.20200407T091256710717Z" + }, + "rdfs:label" : "thing with one deleted thing", + "@context" : { + "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", + "rdfs" : "http://www.w3.org/2000/01/rdf-schema#", + "xsd" : "http://www.w3.org/2001/XMLSchema#", + "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#" + } +} \ No newline at end of file diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala index 3d8c3a7029..d8b4cd1562 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala @@ -347,6 +347,23 @@ class ResourcesRouteV2E2ESpec extends E2ESpec(ResourcesRouteV2E2ESpec.config) { ) } + "perform a full resource request with a link to a resource that is marked as deleted" in { + val request = Get(s"$baseApiUrl/v2/resources/${URLEncoder.encode("http://rdfh.ch/0001/l8f8FVEiSCeq9A1p8gBR-A", "UTF-8")}") + + val response: HttpResponse = singleAwaitingRequest(request) + val responseAsString = responseToString(response) + assert(response.status == StatusCodes.OK, responseAsString) + val expectedAnswerJSONLD = readOrWriteTextFile(responseAsString, new File("src/test/resources/test-data/resourcesR2RV2/ThingWithOneDeletedResource.jsonld"), writeTestDataFiles) + compareJSONLDForResourcesResponse(expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = responseAsString) + + // Check that the resource corresponds to the ontology. + instanceChecker.check( + instanceResponse = responseAsString, + expectedClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, + knoraRouteGet = doGetRequest + ) + } + "perform a full resource request for a past version of a resource, using a URL-encoded xsd:dateTimeStamp" in { val request = Get(s"$baseApiUrl/v2/resources/${URLEncoder.encode("http://rdfh.ch/0001/thing-with-history", "UTF-8")}?version=${URLEncoder.encode("2019-02-12T08:05:10.351Z", "UTF-8")}") val response: HttpResponse = singleAwaitingRequest(request)