From 84f7c148216fc63a1df7b1994668e335aab12c51 Mon Sep 17 00:00:00 2001 From: Sepideh Alassi Date: Mon, 3 May 2021 16:52:27 +0200 Subject: [PATCH] feat(api-v2): Return events describing version history of resources and values of a project ordered by data (DSP-1528) (#1844) * feature(api-v2): route to get history of resources within project + message tests * feat(api-v2): add templates for getting all resources of a project * refactor(api-v2): rename twirl templates for getting resources of a project by class * test(api-v2): tests for returning resource IRIs of a project * fix(application.conf): increase TCP connecting-timeout * feat (api-v2): return version history of all resources in a project * feat(api-v2): get full representation of each resource in each time stamp in its history * feat(api-v2): model event for response * feat (histEvent): return a createResource event for each resource within a project as it was initially created * fix (historyEvents): fix test * feat(historyEvents): extract full value histories * feat (historyEvents): return create/update/delete events for values of a resource * feat (historyEvents): update Permission event * fix (historyEvent) use isAfter method of instant * fix(testData): creation date of the resource a-thing-picture cannot be after creationdate of its values * feat(historyEvents): return all resource and value histories of a project as events * feat (historyEvents): sort events by date * feat(historyEvent) serialize project history response as JSONLD * feat(historyEvent): return deleted values as part of full representation if asked, make delete events for them * test(historyEvent): e2e test * test (historyEvent): test for serialization of event metadata * test (historyEvents): e2e test * feat(historyEvent): serialize value contents * fix (historyEvent): fix the failing test * refactor (historyEvent): some refactoring and clean up * docs(historyEvent): add documentation * fix (historyEvents): fix the failing test * fix (historyEvent): fix the remaining wrong dates * refactor(historyEvents): some restructuring * feat(historyEvent): create an event for a deleted resource * feat (historyEvent): return delete comment when getting the full representation of the resource * fix (historyEvent): fix the build problem * refactor (docs): clean up documentation Co-authored-by: Benjamin Geer --- docs/03-apis/api-v2/editing-resources.md | 2 +- .../api-v2/reading-and-searching-resources.md | 130 ++++++ test_data/all_data/anything-data.ttl | 5 +- .../resourcesR2RV2/ThingWithPicture.jsonld | 4 +- ...get-still-image-file-value-response.jsonld | 4 +- .../webapi/messages/OntologyConstants.scala | 4 + .../util/ConstructResponseUtilV2.scala | 32 +- .../resourcemessages/ResourceMessagesV2.scala | 287 ++++++++++++- .../resourceAndValueEventsUtil.scala | 14 + .../responders/v2/ResourcesResponderV2.scala | 384 +++++++++++++++++- .../responders/v2/SearchResponderV2.scala | 3 +- .../responders/v2/StandoffResponderV2.scala | 1 + .../webapi/routing/v2/ResourcesRouteV2.scala | 31 ++ ...getAllResourcesInProjectPrequery.scala.txt | 40 ++ ...esourcesInProjectPrequeryGraphDB.scala.txt | 44 ++ ...sourcesInProjectPrequeryStandard.scala.txt | 42 ++ .../getResourcePropertiesAndValues.scala.txt | 4 + ...sourcePropertiesAndValuesGraphDB.scala.txt | 35 +- ...ourcePropertiesAndValuesStandard.scala.txt | 38 +- .../getResourceValueVersionHistory.scala.txt | 12 +- ...sourceValueVersionHistoryGraphDB.scala.txt | 21 +- ...ourceValueVersionHistoryStandard.scala.txt | 21 +- ...sourcesByClassInProjectPrequery.scala.txt} | 18 +- ...ByClassInProjectPrequeryGraphDB.scala.txt} | 12 +- ...yClassInProjectPrequeryStandard.scala.txt} | 14 +- .../scala/org/knora/webapi/e2e/v2/BUILD.bazel | 1 + .../e2e/v2/ResourcesRouteV2E2ESpec.scala | 9 + .../webapi/e2e/v2/ValuesRouteV2E2ESpec.scala | 2 +- .../responder/resourcesmessages/BUILD.bazel | 22 + .../ResourcesMessagesV2Spec.scala | 36 ++ .../knora/webapi/responders/v2/BUILD.bazel | 2 +- .../v2/ResourcesResponderV2Spec.scala | 250 +++++++++++- 32 files changed, 1434 insertions(+), 90 deletions(-) create mode 100644 webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/resourceAndValueEventsUtil.scala create mode 100644 webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getAllResourcesInProjectPrequery.scala.txt create mode 100644 webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getAllResourcesInProjectPrequeryGraphDB.scala.txt create mode 100644 webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getAllResourcesInProjectPrequeryStandard.scala.txt rename webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/{getResourcesInProjectPrequery.scala.txt => getResourcesByClassInProjectPrequery.scala.txt} (79%) rename webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/{getResourcesInProjectPrequeryGraphDB.scala.txt => getResourcesByClassInProjectPrequeryGraphDB.scala.txt} (84%) rename webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/{getResourcesInProjectPrequeryStandard.scala.txt => getResourcesByClassInProjectPrequeryStandard.scala.txt} (83%) create mode 100644 webapi/src/test/scala/org/knora/webapi/messages/v2/responder/resourcesmessages/BUILD.bazel create mode 100644 webapi/src/test/scala/org/knora/webapi/messages/v2/responder/resourcesmessages/ResourcesMessagesV2Spec.scala diff --git a/docs/03-apis/api-v2/editing-resources.md b/docs/03-apis/api-v2/editing-resources.md index c5f0a8f574..6e454ed16a 100644 --- a/docs/03-apis/api-v2/editing-resources.md +++ b/docs/03-apis/api-v2/editing-resources.md @@ -277,7 +277,7 @@ Here is an example: "knora-api:lastModificationDate" : { "@type" : "xsd:dateTimeStamp", "@value" : "2017-11-20T15:55:17Z" - } + }, "knora-api:newModificationDate" : { "@type" : "xsd:dateTimeStamp", "@value" : "2018-12-21T16:56:18Z" diff --git a/docs/03-apis/api-v2/reading-and-searching-resources.md b/docs/03-apis/api-v2/reading-and-searching-resources.md index 2f76ca6cd7..539cff32e3 100644 --- a/docs/03-apis/api-v2/reading-and-searching-resources.md +++ b/docs/03-apis/api-v2/reading-and-searching-resources.md @@ -558,3 +558,133 @@ The `orderByProperty` parameter is optional; if it is not supplied, resources wi be sorted alphabetically by resource IRI (an arbitrary but consistent order). The value of `page` is a 0-based integer page number. Paging works as it does in [Gravsearch](query-language.md)). + +### Get the Version History of Resources and Values of a Project + +To get a list of the changes that have been made to resources and values of a project since their creation ordered by date +use this route: + +``` +HTTP GET to http://host/v2/resources/projectHistory/projectIRI +``` + +The project IRI must be URL-encoded. The response is a list of events describing changes made to the resource and its values, + in chronological order. Each entry has the properties: + `knora-api:eventType` (the type of the operation performed on a specific date. The operation can be either + `createResource`, `deleteResource`, `createValue`, `updateValueContent`, `updateValuePermissions`, or `deleteValue`.), +`knora-api:versionDate` (the date when the change was made), +`knora-api:author` (the IRI of the user who made the change), +`knora-api:eventBody` (the information necessary to make the same request). For example: + +```jsonld +{ + "@graph" : [ + { + "knora-api:eventType": "createResource", + "knora-api:author": { + "@id": "http://rdfh.ch/users/9XBCrDV3SRa7kS1WwynB4Q" + }, + "knora-api:eventBody": { + "rdfs:label": "A thing with version history", + "knora-api:resourceIri": "http://rdfh.ch/0001/thing-with-history", + "knora-api:resourceClassIri": "http://www.knora.org/ontology/0001/anything#Thing", + "knora-api:hasPermissions": "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:UnknownUser", + "knora-api:creationDate": { + "@value": "2019-02-08T15:05:10Z", + "@type": "xsd:dateTimeStamp" + }, + "knora-api:attachedToProject": { + "@id": "http://rdfh.ch/projects/0001" + } + }, + "knora-api:versionDate": { + "@value": "2019-02-08T15:05:10Z", + "@type": "xsd:dateTimeStamp" + } + }, + { + "knora-api:eventType": "createValue", + "knora-api:author": { + "@id": "http://rdfh.ch/users/9XBCrDV3SRa7kS1WwynB4Q" + }, + "knora-api:eventBody": { + "knora-api:resourceIri": "http://rdfh.ch/0001/thing-with-history", + "knora-api:resourceClassIri": "http://www.knora.org/ontology/0001/anything#Thing", + "knora-api:valueCreationDate": { + "@value": "2019-02-10T10:30:10Z", + "@type": "xsd:dateTimeStamp" + }, + "knora-api:valueHasUUID": "IZGOjVqxTfSNO4ieKyp0SA", + "knora-api:hasPermissions": "V knora-admin:UnknownUser|M knora-admin:ProjectMember", + "@type": "knora-base:LinkValue", + "http://www.knora.org/ontology/0001/anything#hasOtherThingValue": { + "knora-api:linkValueHasTargetIri": { + "@id": "http://rdfh.ch/0001/2qMtTWvVRXWMBcRNlduvCQ" + } + }, + "rdf:Property": "http://www.knora.org/ontology/0001/anything#hasOtherThingValue", + "@id": "http://rdfh.ch/0001/thing-with-history/values/3a" + }, + "knora-api:versionDate": { + "@value": "2019-02-10T10:30:10Z", + "@type": "xsd:dateTimeStamp" + } + }, + { + "knora-api:eventType": "updateValueContent", + "knora-api:author": { + "@id": "http://rdfh.ch/users/BhkfBc3hTeS_IDo-JgXRbQ" + }, + "knora-api:eventBody": { + "knora-api:resourceIri": "http://rdfh.ch/0001/thing-with-history", + "knora-api:resourceClassIri": "http://www.knora.org/ontology/0001/anything#Thing" + "http://www.knora.org/ontology/0001/anything#hasText": { + "knora-api:valueAsString": "two" + }, + "knora-api:valueCreationDate": { + "@value": "2019-02-11T10:05:10Z", + "@type": "xsd:dateTimeStamp" + }, + "knora-base:previousValue": "http://rdfh.ch/0001/thing-with-history/values/2a", + "knora-api:valueHasUUID": "W5fm67e0QDWxRZumcXcs6g", + "@type": "knora-base:TextValue", + "rdf:Property": "http://www.knora.org/ontology/0001/anything#hasText", + "@id": "http://rdfh.ch/0001/thing-with-history/values/2b" + }, + "knora-api:versionDate": { + "@value": "2019-02-11T10:05:10Z", + "@type": "xsd:dateTimeStamp" + } + }, + { + "knora-api:eventType": "deleteValue", + "knora-api:author": { + "@id": "http://rdfh.ch/users/9XBCrDV3SRa7kS1WwynB4Q" + }, + "knora-api:eventBody": { + "knora-api:resourceIri": "http://rdfh.ch/0001/thing-with-history", + "knora-api:resourceClassIri": "http://www.knora.org/ontology/0001/anything#Thing", + "knora-base:previousValue": "http://rdfh.ch/0001/thing-with-history/values/3a", + "knora-api:deleteDate": { + "@type": "xsd:dateTimeStamp", + "@value": "2019-02-13T09:00:10Z" + }, + "knora-api:isDeleted": true, + "@type": "knora-base:LinkValue", + "rdf:Property": "http://www.knora.org/ontology/0001/anything#hasOtherThingValue", + "@id": "http://rdfh.ch/0001/thing-with-history/values/3b" + }, + "knora-api:versionDate": { + "@value": "2019-02-13T09:00:10Z", + "@type": "xsd:dateTimeStamp" + } + } + ], + "@context" : { + "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "rdfs" : "http://www.w3.org/2000/01/rdf-schema#", + "xsd" : "http://www.w3.org/2001/XMLSchema#", + "knora-api" : "http://api.knora.org/ontology/knora-api/v2#" + } +} +``` \ No newline at end of file diff --git a/test_data/all_data/anything-data.ttl b/test_data/all_data/anything-data.ttl index b9f016eaad..575880a268 100644 --- a/test_data/all_data/anything-data.ttl +++ b/test_data/all_data/anything-data.ttl @@ -711,7 +711,7 @@ knora-base:attachedToProject ; rdfs:label "A thing with a picture"; knora-base:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|RV knora-admin:UnknownUser"; - knora-base:creationDate "2016-03-02T15:05:10Z"^^xsd:dateTime; + knora-base:creationDate "2011-03-02T15:05:10Z"^^xsd:dateTime; knora-base:hasStillImageFileValue . a knora-base:StillImageFileValue; @@ -1716,7 +1716,8 @@ anything:hasInteger ; rdfs:label "deleted thing"; knora-base:isDeleted true; - knora-base:deleteDate "2020-04-07T14:59:28.960124Z"^^xsd:dateTime . + knora-base:deleteDate "2020-04-07T14:59:28.960124Z"^^xsd:dateTime ; + knora-base:deleteComment "a comment for the deleted thing."^^xsd:string . a knora-base:IntValue; knora-base:attachedToUser ; diff --git a/test_data/resourcesR2RV2/ThingWithPicture.jsonld b/test_data/resourcesR2RV2/ThingWithPicture.jsonld index 881cf4eb6b..5678be167e 100644 --- a/test_data/resourcesR2RV2/ThingWithPicture.jsonld +++ b/test_data/resourcesR2RV2/ThingWithPicture.jsonld @@ -25,7 +25,7 @@ }, "rdfs:label": "A thing with a picture", "knora-api:versionArkUrl": { - "@value": "http://0.0.0.0:3336/ark:/72163/1/0001/a=thing=picture0.20160302T150510Z", + "@value": "http://0.0.0.0:3336/ark:/72163/1/0001/a=thing=picture0.20110302T150510Z", "@type": "xsd:anyURI" }, "knora-api:attachedToProject": { @@ -33,7 +33,7 @@ }, "knora-api:userHasPermission": "RV", "knora-api:creationDate": { - "@value": "2016-03-02T15:05:10Z", + "@value": "2011-03-02T15:05:10Z", "@type": "xsd:dateTimeStamp" }, "knora-api:attachedToUser": { diff --git a/test_data/valuesE2EV2/get-still-image-file-value-response.jsonld b/test_data/valuesE2EV2/get-still-image-file-value-response.jsonld index a88fcc416a..f7add2f6ac 100644 --- a/test_data/valuesE2EV2/get-still-image-file-value-response.jsonld +++ b/test_data/valuesE2EV2/get-still-image-file-value-response.jsonld @@ -39,7 +39,7 @@ "@id": "http://rdfh.ch/0001/a-thing-picture/values/goZ7JFRNSeqF-dNxsqAS7Q" }, "knora-api:versionArkUrl": { - "@value": "http://0.0.0.0:3336/ark:/72163/1/0001/a=thing=picture0.20160302T150510Z", + "@value": "http://0.0.0.0:3336/ark:/72163/1/0001/a=thing=picture0.20110302T150510Z", "@type": "xsd:anyURI" }, "knora-api:attachedToProject": { @@ -47,7 +47,7 @@ }, "knora-api:userHasPermission": "CR", "knora-api:creationDate": { - "@value": "2016-03-02T15:05:10Z", + "@value": "2011-03-02T15:05:10Z", "@type": "xsd:dateTimeStamp" }, "knora-api:attachedToUser": { diff --git a/webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala b/webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala index 97690e17b1..031aaaadd7 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala @@ -735,6 +735,10 @@ object OntologyConstants { val Result: IRI = KnoraApiV2PrefixExpansion + "result" val Error: IRI = KnoraApiV2PrefixExpansion + "error" val MayHaveMoreResults: IRI = KnoraApiV2PrefixExpansion + "mayHaveMoreResults" + val EventType: IRI = KnoraApiV2PrefixExpansion + "eventType" + val EventBody: IRI = KnoraApiV2PrefixExpansion + "eventBody" + val ResourceClassIri: IRI = KnoraApiV2PrefixExpansion + "resourceClassIri" + val ResourceIri: IRI = KnoraApiV2PrefixExpansion + "resourceIri" val IsShared: IRI = KnoraApiV2PrefixExpansion + "isShared" val IsBuiltIn: IRI = KnoraApiV2PrefixExpansion + "isBuiltIn" diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/ConstructResponseUtilV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/ConstructResponseUtilV2.scala index 8414b16df7..05c2d2f9e1 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/ConstructResponseUtilV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/ConstructResponseUtilV2.scala @@ -1419,20 +1419,24 @@ object ConstructResponseUtilV2 { timeout: Timeout, executionContext: ExecutionContext): Future[ReadResourceV2] = { def getDeletionInfo(rdfData: RdfData): Option[DeletionInfo] = { - val isDeleted: Boolean = rdfData.requireBooleanObject(OntologyConstants.KnoraBase.IsDeleted.toSmartIri) - - if (isDeleted) { - val deleteDate = rdfData.requireDateTimeObject(OntologyConstants.KnoraBase.DeleteDate.toSmartIri) - val maybeDeleteComment = rdfData.maybeStringObject(OntologyConstants.KnoraBase.DeleteComment.toSmartIri) - - Some( - DeletionInfo( - deleteDate = deleteDate, - maybeDeleteComment = maybeDeleteComment - ) - ) - } else { - None + val mayHaveDeletedStatements: Option[Boolean] = + rdfData.maybeBooleanObject(OntologyConstants.KnoraBase.IsDeleted.toSmartIri) + mayHaveDeletedStatements match { + case Some(isDeleted: Boolean) => + if (isDeleted) { + val deleteDate = rdfData.requireDateTimeObject(OntologyConstants.KnoraBase.DeleteDate.toSmartIri) + val maybeDeleteComment = rdfData.maybeStringObject(OntologyConstants.KnoraBase.DeleteComment.toSmartIri) + + Some( + DeletionInfo( + deleteDate = deleteDate, + maybeDeleteComment = maybeDeleteComment + ) + ) + } else { + None + } + case _ => None } } 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 dc42711694..4719fa91b2 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 @@ -66,6 +66,7 @@ sealed trait ResourcesResponderRequestV2 extends KnoraRequestV2 { * * @param resourceIris the IRIs of the resources to be queried. * @param propertyIri if defined, requests only the values of the specified explicit property. + * @param withDeleted if defined, returns a deleted resource or a deleted value. * @param valueUuid if defined, requests only the value with the specified UUID. * @param versionDate if defined, requests the state of the resources at the specified time in the past. * @param targetSchema the target API schema. @@ -77,6 +78,7 @@ case class ResourcesGetRequestV2(resourceIris: Seq[IRI], propertyIri: Option[SmartIri] = None, valueUuid: Option[UUID] = None, versionDate: Option[Instant] = None, + withDeleted: Boolean = false, targetSchema: ApiV2Schema, schemaOptions: Set[SchemaOption] = Set.empty, featureFactoryConfig: FeatureFactoryConfig, @@ -87,11 +89,13 @@ case class ResourcesGetRequestV2(resourceIris: Seq[IRI], * Requests a preview of one or more resources. A successful response will be a [[ReadResourcesSequenceV2]]. * * @param resourceIris the IRIs of the resources to obtain a preview for. + * @param withDeletedResource indicates if a preview of deleted resource should be returned. * @param targetSchema the schema of the response. * @param featureFactoryConfig the feature factory configuration. * @param requestingUser the user making the request. */ case class ResourcesPreviewGetRequestV2(resourceIris: Seq[IRI], + withDeletedResource: Boolean = false, targetSchema: ApiV2Schema, featureFactoryConfig: FeatureFactoryConfig, requestingUser: UserADM) @@ -101,18 +105,52 @@ case class ResourcesPreviewGetRequestV2(resourceIris: Seq[IRI], * Requests the version history of the values of a resource. * * @param resourceIri the IRI of the resource. + * @param withDeletedResource indicates if the version history of deleted resources should be returned or not. * @param startDate the start of the time period to return, inclusive. * @param endDate the end of the time period to return, exclusive. * @param featureFactoryConfig the feature factory configuration. * @param requestingUser the user making the request. */ case class ResourceVersionHistoryGetRequestV2(resourceIri: IRI, - startDate: Option[Instant], - endDate: Option[Instant], + withDeletedResource: Boolean = false, + startDate: Option[Instant] = None, + endDate: Option[Instant] = None, featureFactoryConfig: FeatureFactoryConfig, requestingUser: UserADM) extends ResourcesResponderRequestV2 +/** + * Requests the full version history of a resource and its values as events. + * + * @param resourceIri the IRI of the resource. + * @param resourceVersionHistory the version history of the resource and its values. + * @param featureFactoryConfig the feature factory configuration. + * @param requestingUser the user making the request. + */ +case class ResourceFullHistoryGetRequestV2(resourceIri: IRI, + resourceVersionHistory: Seq[ResourceHistoryEntry], + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM) + extends ResourcesResponderRequestV2 + +/** + * Requests the version history of all resources of a project. + * + * @param projectIri the IRI of the project. + * @param featureFactoryConfig the feature factory configuration. + * @param requestingUser the user making the request. + */ +case class ProjectResourcesWithHistoryGetRequestV2(projectIri: IRI, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM) + extends ResourcesResponderRequestV2 { + private val stringFormatter = StringFormatter.getInstanceForConstantOntologies + stringFormatter.validateAndEscapeIri(projectIri, throw BadRequestException(s"Invalid project IRI: $projectIri")) + if (!stringFormatter.isKnoraProjectIriStr(projectIri)) { + throw BadRequestException("Given IRI is not a project IRI.") + } +} + /** * Represents an item in the version history of a resource. * @@ -1265,3 +1303,248 @@ case class GraphDataGetResponseV2(nodes: Seq[GraphNodeV2], edges: Seq[GraphEdgeV ) } } + +/** + * Represents the version history of a resource or a values as events. + * + * @param eventType the type of the operation that is one of [[ResourceAndValueEventsUtil]] + * @param versionDate the version date of the event. + * @param author the user which had performed the operation. + * @param eventBody the request body in the form of [[ResourceOrValueEventBody]] needed for the operation indicated + * by eventType. + */ +case class ResourceAndValueHistoryV2(eventType: String, + versionDate: Instant, + author: IRI, + eventBody: ResourceOrValueEventBody) + +abstract class ResourceOrValueEventBody + +/** + * Represents a resource event (createResource) body with all the information required for the request body of this operation. + * @param resourceIri the IRI of the resource. + * @param resourceClassIri the class of the resource. + * @param label the label of the resource. + * @param values the values of the resource at creation time. + * @param permissions the permissions assigned to the new resource. + * @param creationDate the creation date of the resource. + * @param projectADM the project which the resource belongs to. + */ +case class ResourceEventBody(resourceIri: IRI, + resourceClassIri: SmartIri, + label: Option[String] = None, + values: Map[SmartIri, Seq[ValueContentV2]] = Map.empty[SmartIri, Seq[ValueContentV2]], + permissions: Option[String] = None, + lastModificationDate: Option[Instant] = None, + creationDate: Option[Instant] = None, + deletionInfo: Option[DeletionInfo] = None, + projectADM: ProjectADM) + extends ResourceOrValueEventBody { + + def toJsonLD(targetSchema: ApiV2Schema, + settings: KnoraSettingsImpl, + schemaOptions: Set[SchemaOption]): JsonLDObject = { + implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance + + val propertiesAndValuesAsJsonLD: Map[IRI, JsonLDArray] = values.map { + case (propIri: SmartIri, valueContents: Seq[ValueContentV2]) => + val valueContentsAsJsonLD: Seq[JsonLDValue] = valueContents.map { content => + content + .toOntologySchema(targetSchema) + .toJsonLDValue( + targetSchema = targetSchema, + projectADM = projectADM, + settings = settings, + schemaOptions = schemaOptions + ) + } + + propIri.toString -> JsonLDArray(valueContentsAsJsonLD) + } + + val resourceLabel: Option[(IRI, JsonLDString)] = label.map { resourceLabel => + OntologyConstants.Rdfs.Label -> JsonLDString(resourceLabel) + } + + val permissionAsJsonLD: Option[(IRI, JsonLDString)] = permissions.map { resourcePermission => + OntologyConstants.KnoraApiV2Complex.HasPermissions -> JsonLDString(resourcePermission) + } + + val creationDateAsJsonLD: Option[(IRI, JsonLDValue)] = creationDate.map { resourceCreationDate => + OntologyConstants.KnoraApiV2Complex.CreationDate -> JsonLDUtil.datatypeValueToJsonLDObject( + value = resourceCreationDate.toString, + datatype = OntologyConstants.Xsd.DateTimeStamp.toSmartIri + ) + } + val lastModificationDateAsJsonLD: Option[(IRI, JsonLDValue)] = lastModificationDate.map { lasModDate => + OntologyConstants.KnoraApiV2Complex.LastModificationDate -> JsonLDUtil.datatypeValueToJsonLDObject( + value = lasModDate.toString, + datatype = OntologyConstants.Xsd.DateTimeStamp.toSmartIri + ) + } + + val deletionInfoAsJsonLD: Map[IRI, JsonLDValue] = deletionInfo match { + case Some(definedDeletionInfo) => definedDeletionInfo.toJsonLDFields(ApiV2Complex) + case None => Map.empty[IRI, JsonLDValue] + } + JsonLDObject( + Map( + OntologyConstants.KnoraApiV2Complex.ResourceIri -> JsonLDString(resourceIri), + OntologyConstants.KnoraApiV2Complex.ResourceClassIri -> JsonLDString(resourceClassIri.toString), + OntologyConstants.KnoraApiV2Complex.AttachedToProject -> JsonLDUtil.iriToJsonLDObject(projectADM.id) + ) ++ resourceLabel ++ creationDateAsJsonLD ++ propertiesAndValuesAsJsonLD ++ lastModificationDateAsJsonLD + ++ deletionInfoAsJsonLD ++ permissionAsJsonLD + ) + } +} + +/** + * Represents a value event (create/update content/update permission/delete) body with all the information required for + * the request body of the operation. + * @param resourceIri the IRI of the resource. + * @param resourceClassIri the class of the resource. + * @param projectADM the project which the resource belongs to. + * @param propertyIri the IRI of the property. + * @param valueIri the IRI of the value. + * @param valueTypeIri the type of the value. + * @param valueContent the content of the value. + * @param valueUUID the UUID of the value. + * @param valueCreationDate the creation date of the value. + * @param previousValueIri in the case of update value/ delete value operation, this indicates the previous value IRI. + * @param permissions the permissions assigned to the value. + * @param valueComment the comment given for the value operation. + * @param deletionInfo in case of delete value operation, it contains the date of deletion and the given comment. + */ +case class ValueEventBody(resourceIri: IRI, + resourceClassIri: SmartIri, + projectADM: ProjectADM, + propertyIri: SmartIri, + valueIri: IRI, + valueTypeIri: SmartIri, + valueContent: Option[ValueContentV2] = None, + valueUUID: Option[UUID] = None, + valueCreationDate: Option[Instant] = None, + previousValueIri: Option[IRI] = None, + permissions: Option[String] = None, + valueComment: Option[String] = None, + deletionInfo: Option[DeletionInfo] = None) + extends ResourceOrValueEventBody { + + def toJsonLD(targetSchema: ApiV2Schema, + settings: KnoraSettingsImpl, + schemaOptions: Set[SchemaOption]): JsonLDObject = { + implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance + + val contentAsJsonLD: Option[(String, JsonLDValue)] = valueContent.map { content => + val contentJsonLD = content + .toOntologySchema(targetSchema) + .toJsonLDValue( + targetSchema = targetSchema, + projectADM = projectADM, + settings = settings, + schemaOptions = schemaOptions + ) + propertyIri.toString -> contentJsonLD + } + + val valueUUIDAsJsonLD: Option[(IRI, JsonLDValue)] = valueUUID.map { valueHasUUID => + OntologyConstants.KnoraApiV2Complex.ValueHasUUID -> JsonLDString(stringFormatter.base64EncodeUuid(valueHasUUID)) + } + + val valueCreationDateAsJsonLD: Option[(IRI, JsonLDValue)] = valueCreationDate.map { valueHasCreationDate => + OntologyConstants.KnoraApiV2Complex.ValueCreationDate -> JsonLDUtil.datatypeValueToJsonLDObject( + value = valueHasCreationDate.toString, + datatype = OntologyConstants.Xsd.DateTimeStamp.toSmartIri + ) + } + val valuePermissionsAsJSONLD: Option[(IRI, JsonLDValue)] = permissions.map { hasPermissions => + OntologyConstants.KnoraApiV2Complex.HasPermissions -> JsonLDString(hasPermissions) + } + + val deletionInfoAsJsonLD: Map[IRI, JsonLDValue] = deletionInfo match { + case Some(definedDeletionInfo) => definedDeletionInfo.toJsonLDFields(ApiV2Complex) + case None => Map.empty[IRI, JsonLDValue] + } + val valueHasCommentAsJsonLD: Option[(IRI, JsonLDValue)] = valueComment.map { definedComment => + OntologyConstants.KnoraApiV2Complex.ValueHasComment -> JsonLDString(definedComment) + } + + val previousValueAsJsonLD: Option[(IRI, JsonLDValue)] = previousValueIri.map { previousIri => + OntologyConstants.KnoraBase.PreviousValue -> JsonLDString(previousIri.toString) + } + JsonLDObject( + Map( + JsonLDKeywords.ID -> JsonLDString(valueIri), + JsonLDKeywords.TYPE -> JsonLDString(valueTypeIri.toString), + OntologyConstants.KnoraApiV2Complex.ResourceIri -> JsonLDString(resourceIri), + OntologyConstants.KnoraApiV2Complex.ResourceClassIri -> JsonLDString(resourceClassIri.toString), + OntologyConstants.Rdf.Property -> JsonLDString(propertyIri.toString), + ) ++ previousValueAsJsonLD ++ contentAsJsonLD ++ valueUUIDAsJsonLD ++ valueCreationDateAsJsonLD ++ valuePermissionsAsJSONLD + ++ deletionInfoAsJsonLD ++ valueHasCommentAsJsonLD + ) + } +} + +/** + * Represents the history of the project resources and values. + */ +case class ResourceAndValueVersionHistoryResponseV2(projectHistory: Seq[ResourceAndValueHistoryV2]) + extends KnoraJsonLDResponseV2 { + + /** + * Converts the response to a data structure that can be used to generate JSON-LD. + * + * @param targetSchema the Knora API schema to be used in the JSON-LD document. + * @return a [[JsonLDDocument]] representing the response. + */ + override def toJsonLDDocument(targetSchema: ApiV2Schema, + settings: KnoraSettingsImpl, + schemaOptions: Set[SchemaOption]): JsonLDDocument = { + implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance + + if (targetSchema != ApiV2Complex) { + throw AssertionException("Version history can be returned only in the complex schema") + } + + // Convert the history entries to an array of JSON-LD objects. + + val projectHistoryAsJsonLD: Seq[JsonLDObject] = projectHistory.map { historyEntry: ResourceAndValueHistoryV2 => + // convert event body to JsonLD object + val eventBodyAsJsonLD: JsonLDObject = historyEntry.eventBody match { + case valueEventBody: ValueEventBody => valueEventBody.toJsonLD(targetSchema, settings, schemaOptions) + case resourceEventBody: ResourceEventBody => resourceEventBody.toJsonLD(targetSchema, settings, schemaOptions) + case _ => throw NotFoundException(s"Event body is missing or has wrong type.") + } + + JsonLDObject( + Map( + OntologyConstants.KnoraApiV2Complex.EventType -> JsonLDString(historyEntry.eventType), + OntologyConstants.KnoraApiV2Complex.VersionDate -> JsonLDUtil.datatypeValueToJsonLDObject( + value = historyEntry.versionDate.toString, + datatype = OntologyConstants.Xsd.DateTimeStamp.toSmartIri + ), + OntologyConstants.KnoraApiV2Complex.Author -> JsonLDUtil.iriToJsonLDObject(historyEntry.author), + OntologyConstants.KnoraApiV2Complex.EventBody -> eventBodyAsJsonLD + ) + ) + } + + // Make the JSON-LD context. + + val context = JsonLDUtil.makeContext( + fixedPrefixes = Map( + "rdf" -> OntologyConstants.Rdf.RdfPrefixExpansion, + "rdfs" -> OntologyConstants.Rdfs.RdfsPrefixExpansion, + "xsd" -> OntologyConstants.Xsd.XsdPrefixExpansion, + OntologyConstants.KnoraApi.KnoraApiOntologyLabel -> OntologyConstants.KnoraApiV2Complex.KnoraApiV2PrefixExpansion, + OntologyConstants.KnoraBase.KnoraBaseOntologyLabel -> OntologyConstants.KnoraBase.KnoraBasePrefixExpansion + ) + ) + + // Make the JSON-LD document. + + val body = JsonLDObject(Map(JsonLDKeywords.GRAPH -> JsonLDArray(projectHistoryAsJsonLD))) + + JsonLDDocument(body = body, context = context) + } +} diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/resourceAndValueEventsUtil.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/resourceAndValueEventsUtil.scala new file mode 100644 index 0000000000..82913f6e23 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/resourceAndValueEventsUtil.scala @@ -0,0 +1,14 @@ +package org.knora.webapi.messages.v2.responder.resourcemessages + +/** + * Contains string constants for resource and value event types. + */ +object ResourceAndValueEventsUtil { + + val CREATE_RESOURCE_EVENT = "createResource" + val DELETE_RESOURCE_EVENT = "deleteResource" + val CREATE_VALUE_EVENT = "createValue" + val UPDATE_VALUE_CONTENT_EVENT = "updateValueContent" + val UPDATE_VALUE_PERMISSION_EVENT = "updateValuePermission" + val DELETE_VALUE_EVENT = "deleteValue" +} 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 b837d1fe4f..0cc8ebfbf7 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 @@ -34,7 +34,7 @@ import org.knora.webapi.messages.admin.responder.permissionsmessages.{ DefaultObjectAccessPermissionsStringResponseADM, ResourceCreateOperation } -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM +import org.knora.webapi.messages.admin.responder.projectsmessages._ import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.store.sipimessages.{SipiGetTextFileRequest, SipiGetTextFileResponse} import org.knora.webapi.messages.store.triplestoremessages._ @@ -95,6 +95,7 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt propertyIri, valueUuid, versionDate, + withDeleted, targetSchema, schemaOptions, featureFactoryConfig, @@ -103,12 +104,17 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt propertyIri, valueUuid, versionDate, + withDeleted, targetSchema, schemaOptions, featureFactoryConfig, requestingUser) - case ResourcesPreviewGetRequestV2(resIris, targetSchema, featureFactoryConfig, requestingUser) => - getResourcePreviewV2(resIris, targetSchema, featureFactoryConfig, requestingUser) + case ResourcesPreviewGetRequestV2(resIris, + withDeletedResource, + targetSchema, + featureFactoryConfig, + requestingUser) => + getResourcePreviewV2(resIris, withDeletedResource, targetSchema, featureFactoryConfig, requestingUser) case ResourceTEIGetRequestV2(resIri, textProperty, mappingIri, @@ -130,7 +136,10 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt deleteOrEraseResourceV2(deleteOrEraseResourceRequestV2) case graphDataGetRequest: GraphDataGetRequestV2 => getGraphDataResponseV2(graphDataGetRequest) case resourceHistoryRequest: ResourceVersionHistoryGetRequestV2 => getResourceHistoryV2(resourceHistoryRequest) - case other => handleUnexpectedMessage(other, log, this.getClass.getName) + case projectResourcesWithHistoryRequestV2: ProjectResourcesWithHistoryGetRequestV2 => + getProjectResourcesWithHistoryV2(projectResourcesWithHistoryRequestV2) + case resourceFullHistRequest: ResourceFullHistoryGetRequestV2 => getResourceHistoryEvents(resourceFullHistRequest) + case other => handleUnexpectedMessage(other, log, this.getClass.getName) } /** @@ -1329,6 +1338,7 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt * * @param resourceIris the Iris of the requested resources. * @param preview `true` if a preview of the resource is requested. + * @param withDeleted if defined, indicates if the deleted resources and values should be returned or not. * @param propertyIri if defined, requests only the values of the specified explicit property. * @param valueUuid if defined, requests only the value with the specified UUID. * @param versionDate if defined, requests the state of the resources at the specified time in the past. @@ -1340,9 +1350,10 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt private def getResourcesFromTriplestore( resourceIris: Seq[IRI], preview: Boolean, - propertyIri: Option[SmartIri], - valueUuid: Option[UUID], - versionDate: Option[Instant], + withDeleted: Boolean = false, + propertyIri: Option[SmartIri] = None, + valueUuid: Option[UUID] = None, + versionDate: Option[Instant] = None, queryStandoff: Boolean, featureFactoryConfig: FeatureFactoryConfig, requestingUser: UserADM): Future[ConstructResponseUtilV2.MainResourcesAndValueRdfData] = { @@ -1364,6 +1375,7 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt triplestore = settings.triplestoreType, resourceIris = resourceIrisDistinct, preview = preview, + withDeleted = withDeleted, maybePropertyIri = propertyIri, maybeValueUuid = valueUuid, maybeVersionDate = versionDate, @@ -1398,6 +1410,7 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt * @param propertyIri if defined, requests only the values of the specified explicit property. * @param valueUuid if defined, requests only the value with the specified UUID. * @param versionDate if defined, requests the state of the resources at the specified time in the past. + * @param withDeleted if defined, indicates if the deleted resource and values should be returned or not. * @param targetSchema the target API schema. * @param schemaOptions the schema options submitted with the request. * @param featureFactoryConfig the feature factory configuration. @@ -1408,6 +1421,7 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt propertyIri: Option[SmartIri] = None, valueUuid: Option[UUID] = None, versionDate: Option[Instant] = None, + withDeleted: Boolean = false, targetSchema: ApiV2Schema, schemaOptions: Set[SchemaOption], featureFactoryConfig: FeatureFactoryConfig, @@ -1425,6 +1439,7 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt mainResourcesAndValueRdfData: ConstructResponseUtilV2.MainResourcesAndValueRdfData <- getResourcesFromTriplestore( resourceIris = resourceIris, preview = false, + withDeleted = withDeleted, propertyIri = propertyIri, valueUuid = valueUuid, versionDate = versionDate, @@ -1482,11 +1497,13 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt * Get the preview of a resource. * * @param resourceIris the resource to query for. + * @param withDeleted indicates if the deleted resource should be returned or not. * @param featureFactoryConfig the feature factory configuration. * @param requestingUser the the user making the request. * @return a [[ReadResourcesSequenceV2]]. */ private def getResourcePreviewV2(resourceIris: Seq[IRI], + withDeleted: Boolean = false, targetSchema: ApiV2Schema, featureFactoryConfig: FeatureFactoryConfig, requestingUser: UserADM): Future[ReadResourcesSequenceV2] = { @@ -1498,9 +1515,7 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt mainResourcesAndValueRdfData: ConstructResponseUtilV2.MainResourcesAndValueRdfData <- getResourcesFromTriplestore( resourceIris = resourceIris, preview = true, - propertyIri = None, - valueUuid = None, - versionDate = None, + withDeleted = withDeleted, queryStandoff = false, // This has no effect, because we are not querying values. featureFactoryConfig = featureFactoryConfig, requestingUser = requestingUser @@ -2187,6 +2202,7 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt // its creation date. resourcePreviewResponse: ReadResourcesSequenceV2 <- getResourcePreviewV2( resourceIris = Seq(resourceHistoryRequest.resourceIri), + withDeleted = resourceHistoryRequest.withDeletedResource, targetSchema = ApiV2Complex, featureFactoryConfig = resourceHistoryRequest.featureFactoryConfig, requestingUser = resourceHistoryRequest.requestingUser @@ -2199,6 +2215,7 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt historyRequestSparql = org.knora.webapi.messages.twirl.queries.sparql.v2.txt .getResourceValueVersionHistory( triplestore = settings.triplestoreType, + withDeletedResource = resourceHistoryRequest.withDeletedResource, resourceIri = resourceHistoryRequest.resourceIri, maybeStartDate = resourceHistoryRequest.startDate, maybeEndDate = resourceHistoryRequest.endDate @@ -2246,4 +2263,351 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt historyEntriesWithResourceCreation ) } + + /** + * Returns the resources of a project with version history ordered by date. + * + * @param projectResourcesGetRequest the resources with version history request. + * @return the all resources of project with ordered version history. + */ + def getProjectResourcesWithHistoryV2(projectResourcesGetRequest: ProjectResourcesWithHistoryGetRequestV2) + : Future[ResourceAndValueVersionHistoryResponseV2] = + for { + // Get the project; checks if a project with given IRI exists. + projectInfoResponse: ProjectGetResponseADM <- (responderManager ? ProjectGetRequestADM( + identifier = ProjectIdentifierADM(maybeIri = Some(projectResourcesGetRequest.projectIri)), + featureFactoryConfig = projectResourcesGetRequest.featureFactoryConfig, + requestingUser = projectResourcesGetRequest.requestingUser + )).mapTo[ProjectGetResponseADM] + + // Do a SELECT prequery to get the IRIs of the resources that belong to the project. + prequery = org.knora.webapi.messages.twirl.queries.sparql.v2.txt + .getAllResourcesInProjectPrequery( + triplestore = settings.triplestoreType, + projectIri = projectInfoResponse.project.id + ) + .toString + + sparqlSelectResponse <- (storeManager ? SparqlSelectRequest(prequery)).mapTo[SparqlSelectResult] + mainResourceIris: Seq[IRI] = sparqlSelectResponse.results.bindings.map(_.rowMap("resource")) + // For each resource IRI return history events + historyOfResourcesAsSeqOfFutures: Seq[Future[Seq[ResourceAndValueHistoryV2]]] = mainResourceIris.map { + resourceIri => + for { + resourceHistory: ResourceVersionHistoryResponseV2 <- getResourceHistoryV2( + ResourceVersionHistoryGetRequestV2( + resourceIri = resourceIri, + withDeletedResource = true, + featureFactoryConfig = projectResourcesGetRequest.featureFactoryConfig, + requestingUser = projectResourcesGetRequest.requestingUser + )) + resourceFullHist: Seq[ResourceAndValueHistoryV2] <- getResourceHistoryEvents( + ResourceFullHistoryGetRequestV2( + resourceIri = resourceIri, + resourceVersionHistory = resourceHistory.history, + featureFactoryConfig = projectResourcesGetRequest.featureFactoryConfig, + requestingUser = projectResourcesGetRequest.requestingUser + )) + } yield resourceFullHist + } + + projectHistory: Seq[Seq[ResourceAndValueHistoryV2]] <- Future.sequence(historyOfResourcesAsSeqOfFutures) + sortedProjectHistory: Seq[ResourceAndValueHistoryV2] = projectHistory.flatten.sortBy(_.versionDate) + + } yield ResourceAndValueVersionHistoryResponseV2(projectHistory = sortedProjectHistory) + + /** + * Returns the full history of a resource as events. + * + * @param resourceFullHistRequest the version history of a resource. + * @return the full history of resource as sequence of [[ResourceAndValueHistoryV2]]. + */ + def getResourceHistoryEvents( + resourceFullHistRequest: ResourceFullHistoryGetRequestV2): Future[Seq[ResourceAndValueHistoryV2]] = { + + val resourceHist = resourceFullHistRequest.resourceVersionHistory.reverse + // Collect the full representations of the resource for each version date + val histories: Seq[Future[(ResourceHistoryEntry, ReadResourceV2)]] = resourceHist.map { hist => + for { + fullRepresentations <- getResourceAtGivenTime( + resourceIri = resourceFullHistRequest.resourceIri, + versionHist = hist, + featureFactoryConfig = resourceFullHistRequest.featureFactoryConfig, + requestingUser = resourceFullHistRequest.requestingUser + ) + } yield fullRepresentations + } + for { + fullReps: Seq[(ResourceHistoryEntry, ReadResourceV2)] <- Future.sequence(histories) + + // Create an event for the resource at creation time + (creationTimeHist, resourceAtCreation) = fullReps.head + resourceCreateEvent: ResourceAndValueHistoryV2 = getResourceAtCreationDate(resourceAtCreation, creationTimeHist) + resourceCreationEvent: Seq[ResourceAndValueHistoryV2] = Seq(resourceCreateEvent) + + // If there is a version history for deletion of the event, create a delete resource event for it. + (deletionRep, resourceAtValueChanges) = fullReps.tail.partition { + case (resHist, resource) => + resource + .asInstanceOf[ReadResourceV2] + .deletionInfo + .exists(deletionInfo => deletionInfo.deleteDate == resHist.versionDate) + } + resourceDeleteEvent = getResourceAtDeletionDates(deletionRep) + + // For each value version, form an event + valuesEvents: Seq[ResourceAndValueHistoryV2] = resourceAtValueChanges.flatMap { + case (versionHist, readResource) => getValueAtGivenVersionDate(readResource, versionHist, fullReps) + } + + } yield resourceCreationEvent ++ resourceDeleteEvent ++ valuesEvents + } + + /** + * Returns the full representation of a resource at a given date. + * + * @param resourceIri the IRI of the resource. + * @param versionHist the history info of the version; i.e. versionDate and author. + * @param featureFactoryConfig the feature factory configuration. + * @param requestingUser the user making the request. + * @return the full representation of the resource at the given version date. + */ + private def getResourceAtGivenTime(resourceIri: IRI, + versionHist: ResourceHistoryEntry, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM): Future[(ResourceHistoryEntry, ReadResourceV2)] = + for { + resourceFullRepAtCreationTime: ReadResourcesSequenceV2 <- getResourcesV2( + resourceIris = Seq(resourceIri), + versionDate = Some(versionHist.versionDate), + withDeleted = true, + targetSchema = ApiV2Complex, + schemaOptions = Set.empty[SchemaOption], + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser + ) + resourceAtCreationTime: ReadResourceV2 = resourceFullRepAtCreationTime.resources.head + } yield versionHist -> resourceAtCreationTime + + /** + * Returns a createResource event as [[ResourceAndValueHistoryV2]] with request body of the form [[ResourceEventBody]]. + * + * @param resourceAtTimeOfCreation the full representation of the resource at creation date. + * @param versionInfoAtCreation the history info of the version; i.e. versionDate and author. + * @return a createResource event. + */ + private def getResourceAtCreationDate(resourceAtTimeOfCreation: ReadResourceV2, + versionInfoAtCreation: ResourceHistoryEntry): ResourceAndValueHistoryV2 = { + + val requestBody: ResourceEventBody = ResourceEventBody( + resourceIri = resourceAtTimeOfCreation.resourceIri, + resourceClassIri = resourceAtTimeOfCreation.resourceClassIri, + label = Some(resourceAtTimeOfCreation.label), + values = + resourceAtTimeOfCreation.values.mapValues(readValues => readValues.map(readValue => readValue.valueContent)), + projectADM = resourceAtTimeOfCreation.projectADM, + permissions = Some(resourceAtTimeOfCreation.permissions), + creationDate = Some(resourceAtTimeOfCreation.creationDate) + ) + + ResourceAndValueHistoryV2( + eventType = ResourceAndValueEventsUtil.CREATE_RESOURCE_EVENT, + versionDate = versionInfoAtCreation.versionDate, + author = versionInfoAtCreation.author, + eventBody = requestBody + ) + } + + /** + * Returns resourceDeletion events as Seq[[ResourceAndValueHistoryV2]] with request body of the form [[ResourceEventBody]]. + * + * @param resourceDeletionInfo A sequence of resource deletion info containing version history of deletion and + * the full representation of resource at time of deletion. + * @return a seq of deleteResource events. + */ + private def getResourceAtDeletionDates( + resourceDeletionInfo: Seq[(ResourceHistoryEntry, ReadResourceV2)]): Seq[ResourceAndValueHistoryV2] = { + resourceDeletionInfo.map { + case (delHist, fullRepresentation) => + val requestBody: ResourceEventBody = ResourceEventBody( + resourceIri = fullRepresentation.resourceIri, + resourceClassIri = fullRepresentation.resourceClassIri, + projectADM = fullRepresentation.projectADM, + lastModificationDate = fullRepresentation.lastModificationDate, + deletionInfo = fullRepresentation.deletionInfo + ) + ResourceAndValueHistoryV2( + eventType = ResourceAndValueEventsUtil.DELETE_RESOURCE_EVENT, + versionDate = delHist.versionDate, + author = delHist.author, + eventBody = requestBody + ) + } + } + + /** + * Returns a value event as [[ResourceAndValueHistoryV2]] with body of the form [[ValueEventBody]]. + * + * @param resourceAtGivenTime the full representation of the resource at the given time. + * @param versionHist the history info of the version; i.e. versionDate and author. + * @param allResourceVersions all full representations of resource for each version date in its history. + * @return a create/update/delete value event. + */ + private def getValueAtGivenVersionDate( + resourceAtGivenTime: ReadResourceV2, + versionHist: ResourceHistoryEntry, + allResourceVersions: Seq[(ResourceHistoryEntry, ReadResourceV2)]): Seq[ResourceAndValueHistoryV2] = { + val resourceIri = resourceAtGivenTime.resourceIri + + /** returns the values of the resource which have the given version date. */ + def findValuesWithGivenVersionDate(values: Map[SmartIri, Seq[ReadValueV2]]): Map[SmartIri, ReadValueV2] = { + val valuesWithVersionDate: Map[SmartIri, ReadValueV2] = values.foldLeft(Map.empty[SmartIri, ReadValueV2]) { + case (acc, (propIri, readValue)) => + val valuesWithGivenVersion: Seq[ReadValueV2] = + readValue.filter(readValue => + readValue.valueCreationDate == versionHist.versionDate || readValue.deletionInfo.exists(deleteInfo => + deleteInfo.deleteDate == versionHist.versionDate)) + if (valuesWithGivenVersion.nonEmpty) { + acc + (propIri -> valuesWithGivenVersion.head) + } else { acc } + } + + valuesWithVersionDate + + } + + val valuesWithAskedVersionDate: Map[SmartIri, ReadValueV2] = findValuesWithGivenVersionDate( + resourceAtGivenTime.values) + val valueEvents: Seq[ResourceAndValueHistoryV2] = valuesWithAskedVersionDate.map { + case (propIri, readValue) => + val event = + //Is the given date a deletion date? + if (readValue.deletionInfo.exists(deletionInfo => deletionInfo.deleteDate == versionHist.versionDate)) { + // Yes. Return a deleteValue event + val deleteValueRequestBody = ValueEventBody( + resourceIri = resourceIri, + resourceClassIri = resourceAtGivenTime.resourceClassIri, + projectADM = resourceAtGivenTime.projectADM, + propertyIri = propIri, + valueIri = readValue.valueIri, + valueTypeIri = readValue.valueContent.valueType, + deletionInfo = readValue.deletionInfo, + previousValueIri = readValue.previousValueIri + ) + ResourceAndValueHistoryV2( + eventType = ResourceAndValueEventsUtil.DELETE_VALUE_EVENT, + versionDate = versionHist.versionDate, + author = versionHist.author, + eventBody = deleteValueRequestBody + ) + } else { + // No. Is the given date a creation date, i.e. value does not have a previous version? + if (readValue.previousValueIri.isEmpty) { + // Yes. return a createValue event with its request body + val createValueRequestBody = ValueEventBody( + resourceIri = resourceIri, + resourceClassIri = resourceAtGivenTime.resourceClassIri, + projectADM = resourceAtGivenTime.projectADM, + propertyIri = propIri, + valueIri = readValue.valueIri, + valueTypeIri = readValue.valueContent.valueType, + valueContent = Some(readValue.valueContent), + valueUUID = Some(readValue.valueHasUUID), + valueCreationDate = Some(readValue.valueCreationDate), + permissions = Some(readValue.permissions), + valueComment = readValue.valueContent.comment + ) + ResourceAndValueHistoryV2( + eventType = ResourceAndValueEventsUtil.CREATE_VALUE_EVENT, + versionDate = versionHist.versionDate, + author = versionHist.author, + eventBody = createValueRequestBody + ) + } else { + // No. return updateValue event + val (updateEventType: String, updateEventRequestBody: ValueEventBody) = + getUpdateEventType(propIri, readValue, allResourceVersions, resourceAtGivenTime) + ResourceAndValueHistoryV2( + eventType = updateEventType, + versionDate = versionHist.versionDate, + author = versionHist.author, + eventBody = updateEventRequestBody + ) + } + } + event + }.toSeq + + valueEvents + } + + /** + * Since update value operation can be used to update value content or value permissions, using the previous versions + * of the value, it determines the type of the update and returns eventType: updateValuePermission/updateValueContent + * together with the request body necessary to do the update. + * + * @param propertyIri the IRI of the property. + * @param currentVersionOfValue the current value version. + * @param allResourceVersions all versions of resource. + * @param resourceAtGivenTime the full representation of the resource at time of value update. + * @return (eventType, update event request body) + */ + private def getUpdateEventType(propertyIri: SmartIri, + currentVersionOfValue: ReadValueV2, + allResourceVersions: Seq[(ResourceHistoryEntry, ReadResourceV2)], + resourceAtGivenTime: ReadResourceV2): (String, ValueEventBody) = { + val previousValueIri: IRI = currentVersionOfValue.previousValueIri.getOrElse( + throw BadRequestException("No previous value IRI found for the value, Please report this as a bug.")) + + //find the version of resource which has a value with previousValueIri + val (previousVersionDate, previousVersionOfResource): (ResourceHistoryEntry, ReadResourceV2) = allResourceVersions + .find(resourceWithHist => + resourceWithHist._2.values.exists(item => + item._1 == propertyIri && item._2.exists(value => value.valueIri == previousValueIri))) + .getOrElse(throw NotFoundException(s"Could not find the previous value of ${currentVersionOfValue.valueIri}")) + + // check that the version date of the previousValue is before the version date of the current value. + if (previousVersionDate.versionDate.isAfter(currentVersionOfValue.valueCreationDate)) { + throw ForbiddenException( + s"Previous version of the value ${currentVersionOfValue.valueIri} that has previousValueIRI ${previousValueIri} " + + s"has a date after the current value.") + } + + // get the previous value + val previousValue: ReadValueV2 = + previousVersionOfResource.values(propertyIri).find(value => value.valueIri == previousValueIri).get + + // Is the content of previous version of value the same as content of the current version? + val event = if (previousValue.valueContent == currentVersionOfValue.valueContent) { + //Yes. Permission must have been updated; return a permission update event. + val updateValuePermissionsRequestBody = ValueEventBody( + resourceIri = resourceAtGivenTime.resourceIri, + resourceClassIri = resourceAtGivenTime.resourceClassIri, + projectADM = resourceAtGivenTime.projectADM, + propertyIri = propertyIri, + valueIri = currentVersionOfValue.valueIri, + valueTypeIri = currentVersionOfValue.valueContent.valueType, + permissions = Some(currentVersionOfValue.permissions), + valueComment = currentVersionOfValue.valueContent.comment + ) + (ResourceAndValueEventsUtil.UPDATE_VALUE_PERMISSION_EVENT, updateValuePermissionsRequestBody) + } else { + // No. Content must have been updated; return a content update event. + val updateValueContentRequestBody = ValueEventBody( + resourceIri = resourceAtGivenTime.resourceIri, + resourceClassIri = resourceAtGivenTime.resourceClassIri, + projectADM = resourceAtGivenTime.projectADM, + propertyIri = propertyIri, + valueIri = currentVersionOfValue.valueIri, + valueTypeIri = currentVersionOfValue.valueContent.valueType, + valueContent = Some(currentVersionOfValue.valueContent), + valueUUID = Some(currentVersionOfValue.valueHasUUID), + valueCreationDate = Some(currentVersionOfValue.valueCreationDate), + valueComment = currentVersionOfValue.valueContent.comment, + previousValueIri = currentVersionOfValue.previousValueIri + ) + (ResourceAndValueEventsUtil.UPDATE_VALUE_CONTENT_EVENT, updateValueContentRequestBody) + } + event + } } 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 7159124955..12f5e3c782 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 @@ -759,7 +759,7 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand // Do a SELECT prequery to get the IRIs of the requested page of resources. prequery = org.knora.webapi.messages.twirl.queries.sparql.v2.txt - .getResourcesInProjectPrequery( + .getResourcesByClassInProjectPrequery( triplestore = settings.triplestoreType, projectIri = resourcesInProjectGetRequestV2.projectIri.toString, resourceClassIri = internalClassIri, @@ -798,6 +798,7 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand triplestore = settings.triplestoreType, resourceIris = mainResourceIris, preview = false, + withDeleted = false, queryAllNonStandoff = true, maybePropertyIri = None, maybeVersionDate = None, 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 dd6db54df7..353c791257 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 @@ -107,6 +107,7 @@ class StandoffResponderV2(responderData: ResponderData) extends Responder(respon triplestore = settings.triplestoreType, resourceIris = Seq(getStandoffRequestV2.resourceIri), preview = false, + withDeleted = false, maybePropertyIri = None, maybeVersionDate = None, queryAllNonStandoff = false, diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala index b41bd8c810..318109c58c 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala @@ -66,6 +66,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) updateResourceMetadata(featureFactoryConfig) ~ getResourcesInProject(featureFactoryConfig) ~ getResourceHistory(featureFactoryConfig) ~ + getProjectResourceAndValueHistory(featureFactoryConfig) ~ getResources(featureFactoryConfig) ~ getResourcesPreview(featureFactoryConfig) ~ getResourcesTei(featureFactoryConfig) ~ @@ -266,6 +267,36 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } + private def getProjectResourceAndValueHistory(featureFactoryConfig: FeatureFactoryConfig): Route = + path(ResourcesBasePath / "projectHistory" / Segment) { projectIri: IRI => + get { requestContext => + { + val requestMessageFuture: Future[ProjectResourcesWithHistoryGetRequestV2] = for { + requestingUser <- getUserADM( + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig + ) + } yield + ProjectResourcesWithHistoryGetRequestV2( + projectIri = projectIri, + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser + ) + + RouteUtilV2.runRdfRouteWithFuture( + requestMessageF = requestMessageFuture, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log, + targetSchema = ApiV2Complex, + schemaOptions = RouteUtilV2.getSchemaOptions(requestContext) + ) + } + } + } + private def getResources(featureFactoryConfig: FeatureFactoryConfig): Route = path(ResourcesBasePath / Segments) { resIris: Seq[String] => get { requestContext => diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getAllResourcesInProjectPrequery.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getAllResourcesInProjectPrequery.scala.txt new file mode 100644 index 0000000000..2bb2575525 --- /dev/null +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getAllResourcesInProjectPrequery.scala.txt @@ -0,0 +1,40 @@ +@* + * Copyright © 2015-2021 the contributors (see Contributors.md). + * + * This file is part of Knora. + * + * Knora is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Knora is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with Knora. If not, see . + *@ + +@import org.knora.webapi._ +@import org.knora.webapi.messages.SmartIri + +@* + * Constructs a prequery that gets the IRIs of all resources of a specified project. + * + * @param triplestore the name of the triplestore being used. + * @param projectIri the IRI of the project. + *@ +@(triplestore: String, + projectIri: IRI) + +@if(triplestore.startsWith("graphdb")) { + @{ + org.knora.webapi.messages.twirl.queries.sparql.v2.txt.getAllResourcesInProjectPrequeryGraphDB(projectIri = projectIri) + } +} else { + @{ + org.knora.webapi.messages.twirl.queries.sparql.v2.txt.getAllResourcesInProjectPrequeryStandard(projectIri = projectIri) + } +} diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getAllResourcesInProjectPrequeryGraphDB.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getAllResourcesInProjectPrequeryGraphDB.scala.txt new file mode 100644 index 0000000000..329feee078 --- /dev/null +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getAllResourcesInProjectPrequeryGraphDB.scala.txt @@ -0,0 +1,44 @@ +@* + * Copyright © 2015-2021 the contributors (see Contributors.md). + * + * This file is part of Knora. + * + * Knora is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Knora is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with Knora. If not, see . + *@ + +@import org.knora.webapi._ +@import org.knora.webapi.messages.SmartIri + +@* + * Constructs a prequery that gets the IRIs of resources of a specified project, using GraphDB. + * This template is used only by getAllResourcesInProjectPrequery.sparql.txt. + * + * @param projectIri the IRI of the project. + *@ +@(projectIri: IRI) + +PREFIX rdf: +PREFIX rdfs: +PREFIX knora-base: + +SELECT DISTINCT ?resource +FROM +WHERE { + ?resource knora-base:attachedToProject <@projectIri> . + ?resourceType rdfs:subClassOf* knora-base:Resource . + ?resource rdf:type ?resourceType . + ?resource knora-base:creationDate ?creationDate. + +} +ORDER BY DESC(?creationDate) diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getAllResourcesInProjectPrequeryStandard.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getAllResourcesInProjectPrequeryStandard.scala.txt new file mode 100644 index 0000000000..fbae7eebb8 --- /dev/null +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getAllResourcesInProjectPrequeryStandard.scala.txt @@ -0,0 +1,42 @@ +@* + * Copyright © 2015-2021 the contributors (see Contributors.md). + * + * This file is part of Knora. + * + * Knora is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Knora is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with Knora. If not, see . + *@ + +@import org.knora.webapi._ +@import org.knora.webapi.messages.SmartIri + +@* + * Constructs a prequery that gets all resources from the specified project using standard SPARQL. + * This template is used only by getAllResourcesInProjectPrequery.sparql.txt. + * + * @param projectIri the IRI of the project. + *@ +@(projectIri: IRI) + +PREFIX rdf: +PREFIX rdfs: +PREFIX knora-base: + +SELECT DISTINCT ?resource +WHERE { + ?resource knora-base:attachedToProject <@projectIri> . + ?resourceType rdfs:subClassOf* knora-base:Resource . + ?resource rdf:type ?resourceType . + ?resource knora-base:creationDate ?creationDate. +} +ORDER BY DESC(?creationDate) diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcePropertiesAndValues.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcePropertiesAndValues.scala.txt index 69de25d7aa..cd485b5d40 100644 --- a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcePropertiesAndValues.scala.txt +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcePropertiesAndValues.scala.txt @@ -32,6 +32,7 @@ * @param triplestore the name of the triplestore being used. * @param resourceIris the IRIs of the requested resources. * @param preview indicates if the request should only return preview information. + * @param withDeleted indicates if the deleted resources and values should be returned or not. * @param maybePropertyIri if defined, only returns values of the specified explicit property. * Cannot be used in conjunction with preview. * @param maybeValueUuid if defined, requests only the value with the specified UUID. @@ -50,6 +51,7 @@ @(triplestore: String, resourceIris: Seq[IRI], preview: Boolean, + withDeleted: Boolean, queryAllNonStandoff: Boolean, maybePropertyIri: Option[SmartIri] = None, maybeValueUuid: Option[UUID] = None, @@ -67,6 +69,7 @@ @{ org.knora.webapi.messages.twirl.queries.sparql.v2.txt.getResourcePropertiesAndValuesGraphDB(resourceIris = resourceIris, preview = preview, + withDeleted= withDeleted, maybePropertyIri = maybePropertyIri, maybeValueUuid = maybeValueUuid, maybeVersionDate = maybeVersionDate, @@ -81,6 +84,7 @@ org.knora.webapi.messages.twirl.queries.sparql.v2.txt.getResourcePropertiesAndValuesStandard(triplestore = triplestore, resourceIris = resourceIris, preview = preview, + withDeleted= withDeleted, maybePropertyIri = maybePropertyIri, maybeValueUuid = maybeValueUuid, maybeVersionDate = maybeVersionDate, diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcePropertiesAndValuesGraphDB.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcePropertiesAndValuesGraphDB.scala.txt index dafa6249c9..4c0b3d14ff 100644 --- a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcePropertiesAndValuesGraphDB.scala.txt +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcePropertiesAndValuesGraphDB.scala.txt @@ -30,6 +30,7 @@ * * @param resourceIris the IRIs of the requested resources. * @param preview indicates if the request should only return preview information. + * @param withDeleted indicates if the deleted resources and values should be returned or not. * @param maybePropertyIri if defined, only returns values of the specified explicit property. * Cannot be used in conjunction with preview. * @param maybeValueUuid if defined, requests only the value with the specified UUID. @@ -47,6 +48,7 @@ *@ @(resourceIris: Seq[IRI], preview: Boolean, + withDeleted: Boolean, maybePropertyIri: Option[SmartIri], maybeValueUuid: Option[UUID], maybeVersionDate: Option[Instant], @@ -63,7 +65,6 @@ PREFIX rdfs: CONSTRUCT { @* include this inferred information in the results, needed to identify resources *@ ?resource a knora-base:Resource ; - knora-base:isDeleted false ; knora-base:isMainResource true ; knora-base:attachedToProject ?resourceProject ; rdfs:label ?label ; @@ -73,6 +74,14 @@ CONSTRUCT { knora-base:creationDate ?creationDate ; knora-base:lastModificationDate ?lastModificationDate . + @*If deleted resources are wanted return deletion info, otherwise just indicated that resource is not deleted. *@ + @if(!withDeleted) { + ?resource knora-base:isDeleted false . + } else { + ?resource knora-base:isDeleted ?isDeleted ; + knora-base:deleteDate ?deletionDate ; + knora-base:deleteComment ?deleteComment . + } @* include this inferred information in the results, needed to identify value properties *@ ?resource knora-base:hasValue ?valueObject ; ?resourceValueProperty ?valueObject . @@ -100,13 +109,25 @@ CONSTRUCT { VALUES ?resource { @for(resIri <- resourceIris) { <@resIri> } } ?resource rdf:type knora-base:Resource ; - knora-base:isDeleted false ; knora-base:attachedToProject ?resourceProject ; knora-base:attachedToUser ?resourceCreator ; knora-base:hasPermissions ?resourcePermissions ; knora-base:creationDate ?creationDate ; rdfs:label ?label . + @* If deleted resource is not wanted, ignore deleted resources. *@ + @if(!withDeleted) { + ?resource knora-base:isDeleted false . + } else { + OPTIONAL { + ?resource knora-base:isDeleted ?isDeleted ; + knora-base:deleteDate ?deletionDate . + } + OPTIONAL { + ?resource knora-base:deleteComment ?deleteComment . + } + } + @* Is this a version history request? *@ @maybeVersionDate match { case Some(versionDate) => { @@ -149,10 +170,12 @@ CONSTRUCT { } } - @* If the value was deleted on or before the target date, ignore all its versions. *@ - FILTER NOT EXISTS { - ?currentValue knora-base:deleteDate ?currentValueDeleteDate . - FILTER(?currentValueDeleteDate <= "@versionDate"^^xsd:dateTime) + @* If deleted values are not required and the value was deleted on or before the target date, ignore all its versions. *@ + @if(!withDeleted) { + FILTER NOT EXISTS { + ?currentValue knora-base:deleteDate ?currentValueDeleteDate . + FILTER(?currentValueDeleteDate <= "@versionDate"^^xsd:dateTime) + } } @* Get the current version's UUID, because we're going to return it with the requested version. *@ diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcePropertiesAndValuesStandard.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcePropertiesAndValuesStandard.scala.txt index 68f9931bbf..3d72f60727 100644 --- a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcePropertiesAndValuesStandard.scala.txt +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcePropertiesAndValuesStandard.scala.txt @@ -31,6 +31,7 @@ * @param triplestore the name of the triplestore being used. * @param resourceIris the IRIs of the requested resources. * @param preview indicates if the request should only return preview information. + * @param withDeleted indicates if the deleted resources and values should be returned or not. * @param maybePropertyIri if defined, only returns values of the specified explicit property. * Cannot be used in conjunction with preview. * @param maybeValueUuid if defined, requests only the value with the specified UUID. @@ -49,6 +50,7 @@ @(triplestore: String, resourceIris: Seq[IRI], preview: Boolean, + withDeleted: Boolean, maybePropertyIri: Option[SmartIri], maybeValueUuid: Option[UUID], maybeVersionDate: Option[Instant], @@ -66,7 +68,6 @@ PREFIX knora-base: CONSTRUCT { @* include this inferred information in the results, needed to identify resources *@ ?resource a knora-base:Resource ; - knora-base:isDeleted false ; knora-base:isMainResource true ; knora-base:attachedToProject ?resourceProject ; rdfs:label ?label ; @@ -76,6 +77,15 @@ CONSTRUCT { knora-base:creationDate ?creationDate ; knora-base:lastModificationDate ?lastModificationDate . + @*If deleted resources are wanted return deletion info, otherwise just indicated that resource is not deleted. *@ + @if(!withDeleted) { + ?resource knora-base:isDeleted false . + } else { + ?resource knora-base:isDeleted ?isDeleted ; + knora-base:deleteDate ?deletionDate ; + knora-base:deleteComment ?deleteComment . + } + @* include this inferred information in the results, needed to identify value properties *@ ?resource knora-base:hasValue ?valueObject ; ?resourceValueProperty ?valueObject . @@ -109,8 +119,20 @@ CONSTRUCT { knora-base:attachedToUser ?resourceCreator ; knora-base:hasPermissions ?resourcePermissions ; knora-base:creationDate ?creationDate ; - rdfs:label ?label ; - knora-base:isDeleted false . + rdfs:label ?label . + + @* If deleted resource is not wanted, ignore deleted resources. *@ + @if(!withDeleted) { + ?resource knora-base:isDeleted false . + } else { + OPTIONAL { + ?resource knora-base:isDeleted ?isDeleted ; + knora-base:deleteDate ?deletionDate . + } + OPTIONAL { + ?resource knora-base:deleteComment ?deleteComment . + } + } @* Is this a version history request? *@ @maybeVersionDate match { @@ -149,10 +171,12 @@ CONSTRUCT { case None => {} } - @* If the value was deleted on or before the target date, ignore all its versions. *@ - FILTER NOT EXISTS { - ?currentValue knora-base:deleteDate ?currentValueDeleteDate . - FILTER(?currentValueDeleteDate <= "@versionDate"^^xsd:dateTime) + @* If deleted values are not required and the value was deleted on or before the target date, ignore all its versions. *@ + @if(!withDeleted) { + FILTER NOT EXISTS { + ?currentValue knora-base:deleteDate ?currentValueDeleteDate . + FILTER(?currentValueDeleteDate <= "@versionDate"^^xsd:dateTime) + } } @* Get the current version's UUID, because we're going to return it with the requested version. *@ diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourceValueVersionHistory.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourceValueVersionHistory.scala.txt index 00ea85359b..9d24fb7dd0 100644 --- a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourceValueVersionHistory.scala.txt +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourceValueVersionHistory.scala.txt @@ -26,25 +26,29 @@ * If the triplestore type is GraphDB, this template delegates to getResourceVersionHistoryGraphDB.sparql.txt, * which is optimised for GraphDB. Otherwise, it delegates to getResourceVersionHistoryStandard.sparql.txt. * - * @param triplestore the name of the triplestore being used. - * @param resourceIri the IRI of the resource. - * @param maybeStartDate the start of the time period to return, inclusive. - * @param maybeEndDate the end of the time period to return, exclusive. + * @param triplestore the name of the triplestore being used. + * @param resourceIri the IRI of the resource. + * @param withDeletedResource indicates if the version history of a deleted resource should be returned or not. + * @param maybeStartDate the start of the time period to return, inclusive. + * @param maybeEndDate the end of the time period to return, exclusive. *@ @(triplestore: String, resourceIri: IRI, + withDeletedResource: Boolean = false, maybeStartDate: Option[Instant] = None, maybeEndDate: Option[Instant] = None) @if(triplestore.startsWith("graphdb")) { @{ org.knora.webapi.messages.twirl.queries.sparql.v2.txt.getResourceValueVersionHistoryGraphDB(resourceIri = resourceIri, + withDeletedResource = withDeletedResource, maybeStartDate = maybeStartDate, maybeEndDate = maybeEndDate) } } else { @{ org.knora.webapi.messages.twirl.queries.sparql.v2.txt.getResourceValueVersionHistoryStandard(resourceIri = resourceIri, + withDeletedResource = withDeletedResource, maybeStartDate = maybeStartDate, maybeEndDate = maybeEndDate) } diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourceValueVersionHistoryGraphDB.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourceValueVersionHistoryGraphDB.scala.txt index 080c9fba1e..06ed0df55e 100644 --- a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourceValueVersionHistoryGraphDB.scala.txt +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourceValueVersionHistoryGraphDB.scala.txt @@ -23,11 +23,13 @@ @** * Gets the the version history of the values of a resource, using GraphDB. * - * @param resourceIri the IRI of the resource. - * @param maybeStartDate the start of the time period to return, inclusive. - * @param maybeEndDate the end of the time period to return, exclusive. + * @param resourceIri the IRI of the resource. + * @param withDeletedResource indicates if the version history of a deleted resource should be returned or not. + * @param maybeStartDate the start of the time period to return, inclusive. + * @param maybeEndDate the end of the time period to return, exclusive. *@ @(resourceIri: IRI, + withDeletedResource: Boolean = false, maybeStartDate: Option[Instant] = None, maybeEndDate: Option[Instant] = None) @@ -36,8 +38,10 @@ PREFIX rdfs: PREFIX knora-base: SELECT DISTINCT ?versionDate ?author WHERE { - <@resourceIri> knora-base:isDeleted false ; - knora-base:hasValue ?currentValue . + <@resourceIri> knora-base:hasValue ?currentValue . + @if(!withDeletedResource) { + <@resourceIri> knora-base:isDeleted false . + } ?currentValue knora-base:previousValue* ?valueObject . @@ -48,7 +52,12 @@ SELECT DISTINCT ?versionDate ?author WHERE { ?valueObject knora-base:deleteDate ?versionDate . ?valueObject knora-base:deletedBy ?author . } - + @if(withDeletedResource) { + UNION { + <@resourceIri> knora-base:deleteDate ?versionDate . + <@resourceIri> knora-base:attachedToUser ?author . + } + } @maybeStartDate match { case Some(startDate) => { FILTER(?versionDate >= "@startDate"^^xsd:dateTime) diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourceValueVersionHistoryStandard.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourceValueVersionHistoryStandard.scala.txt index 8f973ac6e4..60ea242d97 100644 --- a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourceValueVersionHistoryStandard.scala.txt +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourceValueVersionHistoryStandard.scala.txt @@ -23,11 +23,13 @@ @** * Gets the the version history of the values of a resource, using standard SPARQL. * - * @param resourceIri the IRI of the resource. - * @param maybeStartDate the start of the time period to return, inclusive. - * @param maybeEndDate the end of the time period to return, exclusive. + * @param resourceIri the IRI of the resource. + * @param withDeletedResource indicates if the version history of a deleted resource should be returned or not. + * @param maybeStartDate the start of the time period to return, inclusive. + * @param maybeEndDate the end of the time period to return, exclusive. *@ @(resourceIri: IRI, + withDeletedResource: Boolean = false, maybeStartDate: Option[Instant] = None, maybeEndDate: Option[Instant] = None) @@ -36,9 +38,10 @@ PREFIX rdfs: PREFIX knora-base: SELECT DISTINCT ?versionDate ?author WHERE { - <@resourceIri> knora-base:isDeleted false ; - ?property ?currentValue . - + <@resourceIri> ?property ?currentValue . + @if(!withDeletedResource) { + <@resourceIri> knora-base:isDeleted false . + } ?property rdfs:subPropertyOf* knora-base:hasValue . ?currentValue knora-base:previousValue* ?valueObject . @@ -50,6 +53,12 @@ SELECT DISTINCT ?versionDate ?author WHERE { ?valueObject knora-base:deleteDate ?versionDate . ?valueObject knora-base:deletedBy ?author . } + @if(withDeletedResource) { + UNION { + <@resourceIri> knora-base:deleteDate ?versionDate . + <@resourceIri> knora-base:attachedToUser ?author . + } + } @maybeStartDate match { case Some(startDate) => { diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcesInProjectPrequery.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcesByClassInProjectPrequery.scala.txt similarity index 79% rename from webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcesInProjectPrequery.scala.txt rename to webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcesByClassInProjectPrequery.scala.txt index 229a4da15a..cc06ad4875 100644 --- a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcesInProjectPrequery.scala.txt +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcesByClassInProjectPrequery.scala.txt @@ -23,13 +23,13 @@ @* * Constructs a prequery that gets the IRIs of resources from the specified project. * - * @param the name of the triplestore being used. - * @param projectIri the IRI of the project. - * @param resourceClassIri the IRI of the resource class. - * @param maybeOrderByProperty the IRI of the property to order by. - * @param maybeOrderByValuePredicate a predicate of a Knora value class, whose object will be used in ORDER BY. - * @param offset the OFFSET to be used. - * @param offset the LIMIT to be used. + * @param triplestore the name of the triplestore being used. + * @param projectIri the IRI of the project. + * @param resourceClassIri the IRI of the resource class. + * @param maybeOrderByProperty the IRI of the property to order by. + * @param maybeOrderByValuePredicate the predicate of a Knora value class whose object will be used in ORDER BY. + * @param offset the OFFSET to be used. + * @param limit the LIMIT to be used. *@ @(triplestore: String, projectIri: IRI, @@ -41,7 +41,7 @@ @if(triplestore.startsWith("graphdb")) { @{ - org.knora.webapi.messages.twirl.queries.sparql.v2.txt.getResourcesInProjectPrequeryGraphDB(projectIri = projectIri, + org.knora.webapi.messages.twirl.queries.sparql.v2.txt.getResourcesByClassInProjectPrequeryGraphDB(projectIri = projectIri, resourceClassIri = resourceClassIri, maybeOrderByProperty = maybeOrderByProperty, maybeOrderByValuePredicate = maybeOrderByValuePredicate, @@ -50,7 +50,7 @@ } } else { @{ - org.knora.webapi.messages.twirl.queries.sparql.v2.txt.getResourcesInProjectPrequeryStandard(triplestore = triplestore, + org.knora.webapi.messages.twirl.queries.sparql.v2.txt.getResourcesByClassInProjectPrequeryStandard(triplestore = triplestore, projectIri = projectIri, resourceClassIri = resourceClassIri, maybeOrderByProperty = maybeOrderByProperty, diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcesInProjectPrequeryGraphDB.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcesByClassInProjectPrequeryGraphDB.scala.txt similarity index 84% rename from webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcesInProjectPrequeryGraphDB.scala.txt rename to webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcesByClassInProjectPrequeryGraphDB.scala.txt index 41d232269f..33a2094a32 100644 --- a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcesInProjectPrequeryGraphDB.scala.txt +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcesByClassInProjectPrequeryGraphDB.scala.txt @@ -24,12 +24,12 @@ * Constructs a prequery that gets the IRIs of resources from the specified project, using GraphDB. * This template is used only by getResourcesInProjectPrequery.sparql.txt. * - * @param projectIri the IRI of the project. - * @param resourceClassIri the IRI of the resource class. - * @param maybeOrderByProperty the IRI of the property to order by. - * @param maybeOrderByValuePredicate a predicate of a Knora value class, whose object will be used in ORDER BY. - * @param offset the OFFSET to be used. - * @param offset the LIMIT to be used. + * @param projectIri the IRI of the project. + * @param resourceClassIri the IRI of the resource class. + * @param maybeOrderByProperty the IRI of the property to order by. + * @param maybeOrderByValuePredicate the predicate of a Knora value class whose object will be used in ORDER BY. + * @param offset the OFFSET to be used. + * @param limit the LIMIT to be used. *@ @(projectIri: IRI, resourceClassIri: SmartIri, diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcesInProjectPrequeryStandard.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcesByClassInProjectPrequeryStandard.scala.txt similarity index 83% rename from webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcesInProjectPrequeryStandard.scala.txt rename to webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcesByClassInProjectPrequeryStandard.scala.txt index 3bfac7e888..a8baff3afa 100644 --- a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcesInProjectPrequeryStandard.scala.txt +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/getResourcesByClassInProjectPrequeryStandard.scala.txt @@ -24,13 +24,13 @@ * Constructs a prequery that gets resources from the specified project, using standard SPARQL. * This template is used only by getResourcesInProjectPrequery.sparql.txt. * - * @param the name of the triplestore being used. - * @param projectIri the IRI of the project. - * @param resourceClassIri the IRI of the resource class. - * @param orderByProperty the IRI of the property to order by. - * @param orderByValuePredicate a predicate of a Knora value class, whose object will be used in ORDER BY. - * @param offset the OFFSET to be used. - * @param offset the LIMIT to be used. + * @param triplestore the name of the triplestore being used. + * @param projectIri the IRI of the project. + * @param resourceClassIri the IRI of the resource class. + * @param orderByProperty the IRI of the property to order by. + * @param orderByValuePredicate the predicate of a Knora value class whose object will be used in ORDER BY. + * @param offset the OFFSET to be used. + * @param limit the LIMIT to be used. *@ @(triplestore: String, projectIri: IRI, diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/BUILD.bazel b/webapi/src/test/scala/org/knora/webapi/e2e/v2/BUILD.bazel index a41f063ec8..468b9b4701 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/BUILD.bazel +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/BUILD.bazel @@ -107,6 +107,7 @@ scala_test( "//webapi:test_library", "@maven//:org_eclipse_rdf4j_rdf4j_client", "@maven//:org_xmlunit_xmlunit_core", + "@maven//:javax_json_javax_json_api", ] + BASE_TEST_DEPENDENCIES_WITH_JSON, ) 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 908c689783..e7bfe1ccc0 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 @@ -611,6 +611,15 @@ class ResourcesRouteV2E2ESpec extends E2ESpec(ResourcesRouteV2E2ESpec.config) { } } + "return entire resource and value history events for a given project" in { + val projectIri = URLEncoder.encode("http://rdfh.ch/projects/0001", "UTF-8") + val projectHistoryRequest = Get(s"$baseApiUrl/v2/resources/projectHistory/$projectIri") + .addCredentials(BasicHttpCredentials(SharedTestDataADM.anythingAdminUser.email, password)) + val projectHistoryResponse: HttpResponse = singleAwaitingRequest(projectHistoryRequest) + val historyResponseAsString = responseToString(projectHistoryResponse) + assert(projectHistoryResponse.status == StatusCodes.OK, historyResponseAsString) + } + "return a graph of resources reachable via links from/to a given resource" in { val request = Get(s"$baseApiUrl/v2/graph/${URLEncoder.encode("http://rdfh.ch/0001/start", "UTF-8")}?direction=both") diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ValuesRouteV2E2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ValuesRouteV2E2ESpec.scala index db6f8784d7..1786aa2674 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ValuesRouteV2E2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ValuesRouteV2E2ESpec.scala @@ -3437,7 +3437,7 @@ class ValuesRouteV2E2ESpec extends E2ESpec { val request = Put(baseApiUrl + "/v2/values", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLDEntity)) ~> addCredentials( BasicHttpCredentials(anythingUserEmail, password)) val response: HttpResponse = singleAwaitingRequest(request) - println(responseToString(response)) + assert(response.status == StatusCodes.OK, response.toString) val responseJsonDoc: JsonLDDocument = responseToJsonLDDocument(response) val valueIri: IRI = diff --git a/webapi/src/test/scala/org/knora/webapi/messages/v2/responder/resourcesmessages/BUILD.bazel b/webapi/src/test/scala/org/knora/webapi/messages/v2/responder/resourcesmessages/BUILD.bazel new file mode 100644 index 0000000000..57866c6fe7 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/messages/v2/responder/resourcesmessages/BUILD.bazel @@ -0,0 +1,22 @@ +package(default_visibility = ["//visibility:public"]) + +load("@io_bazel_rules_scala//scala:scala.bzl", "scala_test") +load("//third_party:dependencies.bzl", "ALL_WEBAPI_MAIN_DEPENDENCIES", "BASE_TEST_DEPENDENCIES") + +scala_test( + name = "ResourcesMessagesV2Spec", + size = "small", # 60s + srcs = [ + "ResourcesMessagesV2Spec.scala", + ], + data = [ + "//knora-ontologies", + "//test_data", + ], + jvm_flags = ["-Dconfig.resource=fuseki.conf"], + # unused_dependency_checker_mode = "warn", + deps = ALL_WEBAPI_MAIN_DEPENDENCIES + [ + "//webapi:main_library", + "//webapi:test_library", + ] + BASE_TEST_DEPENDENCIES, +) diff --git a/webapi/src/test/scala/org/knora/webapi/messages/v2/responder/resourcesmessages/ResourcesMessagesV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/messages/v2/responder/resourcesmessages/ResourcesMessagesV2Spec.scala new file mode 100644 index 0000000000..ef84a01efa --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/messages/v2/responder/resourcesmessages/ResourcesMessagesV2Spec.scala @@ -0,0 +1,36 @@ +package org.knora.webapi.messages.v2.responder.resourcesmessages + +import org.knora.webapi.CoreSpec +import org.knora.webapi.exceptions.BadRequestException +import org.knora.webapi.messages.v2.responder.resourcemessages._ +import org.knora.webapi.sharedtestdata._ + +/** + * Tests [[ResourceMessagesV2]]. + */ +class ResourcesMessagesV2Spec extends CoreSpec() { + "All Resources of a Project With History Get Requests" should { + "fail if given project IRI is not valid" in { + val projectIri = "invalid-project-IRI" + val caught = intercept[BadRequestException]( + ProjectResourcesWithHistoryGetRequestV2( + projectIri = projectIri, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = SharedTestDataADM.imagesUser01 + ) + ) + assert(caught.getMessage === s"Invalid project IRI: $projectIri") + } + + "fail if given IRI is not a project Iri" in { + val caught = intercept[BadRequestException]( + ProjectResourcesWithHistoryGetRequestV2( + projectIri = "http://rdfh.ch/0001/thing-with-history", // resource IRI instead of project IRI + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = SharedTestDataADM.imagesUser01 + ) + ) + assert(caught.getMessage === "Given IRI is not a project IRI.") + } + } +} diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/BUILD.bazel b/webapi/src/test/scala/org/knora/webapi/responders/v2/BUILD.bazel index ab3a8961dd..5df4ce0670 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/BUILD.bazel +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/BUILD.bazel @@ -49,7 +49,7 @@ scala_test( scala_test( name = "ResourcesResponderV2Spec", - size = "medium", + size = "large", srcs = [ "ResourcesResponderV2Spec.scala", "ResourcesResponderV2SpecFullData.scala", 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 e23da944b2..10a99d71b5 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 @@ -605,10 +605,10 @@ class ResourcesResponderV2Spec extends CoreSpec() with ImplicitSender { "return a preview descriptions of the book 'Zeitglöcklein des Lebens und Leidens Christi' in the Incunabula test data" in { responderManager ! ResourcesPreviewGetRequestV2( - Seq("http://rdfh.ch/0803/c5058f3a"), - ApiV2Complex, + resourceIris = Seq("http://rdfh.ch/0803/c5058f3a"), + targetSchema = ApiV2Complex, featureFactoryConfig = defaultFeatureFactoryConfig, - incunabulaUserProfile + requestingUser = incunabulaUserProfile ) expectMsgPF(timeout) { @@ -2384,5 +2384,249 @@ class ResourcesResponderV2Spec extends CoreSpec() with ImplicitSender { assert(entityUsedResponse.results.bindings.isEmpty, s"Link value was not erased") } } + + "not return resources of a project which does not exist" in { + + responderManager ! ProjectResourcesWithHistoryGetRequestV2( + projectIri = "http://rdfh.ch/projects/1111", + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = SharedTestDataADM.anythingAdminUser + ) + expectMsgPF(timeout) { + case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[NotFoundException] should ===(true) + } + } + + "return full history of a-thing-picture resource" in { + val resourceIri = "http://rdfh.ch/0001/a-thing-picture" + responderManager ! ResourceVersionHistoryGetRequestV2( + resourceIri = resourceIri, + withDeletedResource = true, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = anythingUserProfile + ) + val response: ResourceVersionHistoryResponseV2 = expectMsgType[ResourceVersionHistoryResponseV2](timeout) + + responderManager ! ResourceFullHistoryGetRequestV2( + resourceIri = resourceIri, + resourceVersionHistory = response.history, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = anythingUserProfile + ) + val events: Seq[ResourceAndValueHistoryV2] = expectMsgType[Seq[ResourceAndValueHistoryV2]](timeout) + events.size shouldEqual (3) + val createResourceEvents = + events.filter(historyEvent => historyEvent.eventType == ResourceAndValueEventsUtil.CREATE_RESOURCE_EVENT) + createResourceEvents.size should be(1) + val createValueEvents = + events.filter(historyEvent => historyEvent.eventType == ResourceAndValueEventsUtil.CREATE_VALUE_EVENT) + createValueEvents.size should be(1) + val updateValueEvents = + events.filter(historyEvent => historyEvent.eventType == ResourceAndValueEventsUtil.UPDATE_VALUE_CONTENT_EVENT) + updateValueEvents.size should be(1) + } + + "return full history of a resource as events" in { + val resourceIri = "http://rdfh.ch/0001/thing-with-history" + + responderManager ! ResourceVersionHistoryGetRequestV2( + resourceIri = resourceIri, + withDeletedResource = true, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = anythingUserProfile + ) + val response: ResourceVersionHistoryResponseV2 = expectMsgType[ResourceVersionHistoryResponseV2](timeout) + + responderManager ! ResourceFullHistoryGetRequestV2( + resourceIri = resourceIri, + resourceVersionHistory = response.history, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = anythingUserProfile + ) + val events: Seq[ResourceAndValueHistoryV2] = expectMsgType[Seq[ResourceAndValueHistoryV2]](timeout) + events.size should be(9) + } + + "update value permission to test update permission event" in { + val resourceIri = "http://rdfh.ch/0001/thing-with-history" + // Update the value permission. + + responderManager ! UpdateValueRequestV2( + UpdateValuePermissionsV2( + resourceIri = resourceIri, + resourceClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, + propertyIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasInteger".toSmartIri, + valueIri = "http://rdfh.ch/0001/thing-with-history/values/1c", + valueType = OntologyConstants.KnoraApiV2Complex.IntValue.toSmartIri, + permissions = "CR knora-admin:Creator|V knora-admin:KnownUser" + ), + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = anythingUserProfile, + apiRequestID = UUID.randomUUID + ) + + val updateValuePermissionResponse = expectMsgType[UpdateValueResponseV2](timeout) + + responderManager ! ResourceVersionHistoryGetRequestV2( + resourceIri = resourceIri, + withDeletedResource = true, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = anythingUserProfile + ) + val response: ResourceVersionHistoryResponseV2 = expectMsgType[ResourceVersionHistoryResponseV2](timeout) + + responderManager ! ResourceFullHistoryGetRequestV2( + resourceIri = resourceIri, + resourceVersionHistory = response.history, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = anythingUserProfile + ) + val events: Seq[ResourceAndValueHistoryV2] = expectMsgType[Seq[ResourceAndValueHistoryV2]](timeout) + events.size should be(10) + val updatePermissionEvent: Option[ResourceAndValueHistoryV2] = + events.find(event => event.eventType == ResourceAndValueEventsUtil.UPDATE_VALUE_PERMISSION_EVENT) + assert(updatePermissionEvent.isDefined) + val updatePermissionPayload = updatePermissionEvent.get.eventBody + .asInstanceOf[ValueEventBody] + updatePermissionPayload.valueIri shouldEqual (updateValuePermissionResponse.valueIri) + } + + "create a new value to test create value history event" in { + val resourceIri = "http://rdfh.ch/0001/thing-with-history" + val newValueIri = "http://rdfh.ch/0001/thing-with-history/values/newText" + val testValue = "a test value" + // create new value. + + responderManager ! CreateValueRequestV2( + CreateValueV2( + resourceIri = resourceIri, + resourceClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, + propertyIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasText".toSmartIri, + valueContent = TextValueContentV2( + ontologySchema = ApiV2Complex, + maybeValueHasString = Some(testValue) + ), + valueIri = Some(newValueIri.toSmartIri), + permissions = Some("CR knora-admin:Creator|V knora-admin:KnownUser") + ), + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = anythingUserProfile, + apiRequestID = UUID.randomUUID + ) + + expectMsgType[CreateValueResponseV2](timeout) + + responderManager ! ResourceVersionHistoryGetRequestV2( + resourceIri = resourceIri, + withDeletedResource = true, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = anythingUserProfile + ) + val response: ResourceVersionHistoryResponseV2 = expectMsgType[ResourceVersionHistoryResponseV2](timeout) + + responderManager ! ResourceFullHistoryGetRequestV2( + resourceIri = resourceIri, + resourceVersionHistory = response.history, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = anythingUserProfile + ) + val events: Seq[ResourceAndValueHistoryV2] = expectMsgType[Seq[ResourceAndValueHistoryV2]](timeout) + events.size should be(11) + val createValueEvent: Option[ResourceAndValueHistoryV2] = + events.find( + event => + event.eventType == ResourceAndValueEventsUtil.CREATE_VALUE_EVENT && event.eventBody + .asInstanceOf[ValueEventBody] + .valueIri == newValueIri) + assert(createValueEvent.isDefined) + val createValuePayloadContent = createValueEvent.get.eventBody + .asInstanceOf[ValueEventBody] + .valueContent + assert(createValuePayloadContent.isDefined) + val payloadContent = createValuePayloadContent.get + assert(payloadContent.valueType == OntologyConstants.KnoraBase.TextValue.toSmartIri) + assert(payloadContent.valueHasString == testValue) + } + + "delete the newly created value to check the delete value event of resource history" in { + val resourceIri = "http://rdfh.ch/0001/thing-with-history" + val valueToDelete = "http://rdfh.ch/0001/thing-with-history/values/newText" + val deleteComment = "delete value test" + // delete the new value. + + responderManager ! DeleteValueRequestV2( + resourceIri = resourceIri, + resourceClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, + propertyIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasText".toSmartIri, + valueIri = valueToDelete, + valueTypeIri = OntologyConstants.KnoraApiV2Complex.TextValue.toSmartIri, + deleteComment = Some(deleteComment), + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = anythingUserProfile, + apiRequestID = UUID.randomUUID + ) + expectMsgType[SuccessResponseV2](timeout) + responderManager ! ResourceVersionHistoryGetRequestV2( + resourceIri = resourceIri, + withDeletedResource = true, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = anythingUserProfile + ) + val response: ResourceVersionHistoryResponseV2 = expectMsgType[ResourceVersionHistoryResponseV2](timeout) + + responderManager ! ResourceFullHistoryGetRequestV2( + resourceIri = resourceIri, + resourceVersionHistory = response.history, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = anythingUserProfile + ) + val events: Seq[ResourceAndValueHistoryV2] = expectMsgType[Seq[ResourceAndValueHistoryV2]](timeout) + events.size should be(12) + val deleteValueEvent: Option[ResourceAndValueHistoryV2] = + events.find( + event => + event.eventType == ResourceAndValueEventsUtil.DELETE_VALUE_EVENT && event.eventBody + .asInstanceOf[ValueEventBody] + .valueIri == valueToDelete) + assert(deleteValueEvent.isDefined) + } + + "return full history of a deleted resource" in { + val resourceIri = "http://rdfh.ch/0001/PHbbrEsVR32q5D_ioKt6pA" + responderManager ! ResourceVersionHistoryGetRequestV2( + resourceIri = resourceIri, + withDeletedResource = true, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = anythingUserProfile + ) + val response: ResourceVersionHistoryResponseV2 = expectMsgType[ResourceVersionHistoryResponseV2](timeout) + + responderManager ! ResourceFullHistoryGetRequestV2( + resourceIri = resourceIri, + resourceVersionHistory = response.history, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = anythingUserProfile + ) + + val events: Seq[ResourceAndValueHistoryV2] = expectMsgType[Seq[ResourceAndValueHistoryV2]](timeout) + events.size should be(2) + val deleteResourceEvent: Option[ResourceAndValueHistoryV2] = + events.find(event => event.eventType == ResourceAndValueEventsUtil.DELETE_RESOURCE_EVENT) + assert(deleteResourceEvent.isDefined) + val deletionInfo = deleteResourceEvent.get.eventBody.asInstanceOf[ResourceEventBody].deletionInfo.get + deletionInfo.maybeDeleteComment should be(Some("a comment for the deleted thing.")) + } + + "return seq of full history events for each resource of a project" in { + responderManager ! ProjectResourcesWithHistoryGetRequestV2( + projectIri = "http://rdfh.ch/projects/0001", + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = SharedTestDataADM.anythingAdminUser + ) + val response: ResourceAndValueVersionHistoryResponseV2 = + expectMsgType[ResourceAndValueVersionHistoryResponseV2](timeout) + response.projectHistory.size should be > 1 + + } } }