diff --git a/docs/05-internals/development/generating-client-test-data.md b/docs/05-internals/development/generating-client-test-data.md index 207604cba5..7d16918a4c 100644 --- a/docs/05-internals/development/generating-client-test-data.md +++ b/docs/05-internals/development/generating-client-test-data.md @@ -36,6 +36,12 @@ with the list in `webapi/scripts/expected-client-test-data.txt`. ## Usage +On macOS, you will need to install Redis in order to have the `redis-cli` command-line tool: + +``` +brew install redis +``` + To generate client test data, type: ``` diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala index 19f9978aa1..cdcd2dce9c 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala @@ -528,9 +528,11 @@ case class GenerateSparqlForValueInNewResourceV2(valueContent: ValueContentV2, * update that will create the values. * @param unverifiedValues a map of property IRIs to [[UnverifiedValueV2]] objects describing * the values that should have been created. + * @param hasStandoffLink `true` if the property `knora-base:hasStandoffLinkToValue` was automatically added. */ case class GenerateSparqlToCreateMultipleValuesResponseV2(insertSparql: String, - unverifiedValues: Map[SmartIri, Seq[UnverifiedValueV2]]) + unverifiedValues: Map[SmartIri, Seq[UnverifiedValueV2]], + hasStandoffLink: Boolean) /** * The value of a Knora property in the context of some particular input or output operation. 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 47c9e0b3e6..e7dc8e12ea 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 @@ -65,9 +65,11 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt * @param sparqlTemplateResourceToCreate a [[SparqlTemplateResourceToCreate]] describing SPARQL for creating * the resource. * @param values the resource's values for verification. + * @param hasStandoffLink `true` if the property `knora-base:hasStandoffLinkToValue` was automatically added. */ private case class ResourceReadyToCreate(sparqlTemplateResourceToCreate: SparqlTemplateResourceToCreate, - values: Map[SmartIri, Seq[UnverifiedValueV2]]) + values: Map[SmartIri, Seq[UnverifiedValueV2]], + hasStandoffLink: Boolean) /** * Receives a message of type [[ResourcesResponderRequestV2]], and returns an appropriate response message. @@ -701,7 +703,8 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt resourceLabel = internalCreateResource.label, resourceCreationDate = creationDate ), - values = sparqlForValuesResponse.unverifiedValues + values = sparqlForValuesResponse.unverifiedValues, + hasStandoffLink = sparqlForValuesResponse.hasStandoffLink ) } @@ -1066,11 +1069,21 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt throw AssertionException(s"Resource <$resourceIri> was saved, but it has the wrong label") } - _ = if (resource.values.keySet != resourceReadyToCreate.values.keySet) { - throw AssertionException(s"Resource <$resourceIri> was saved, but it has the wrong properties") + savedPropertyIris: Set[SmartIri] = resource.values.keySet + + // Check that the property knora-base:hasStandoffLinkToValue was automatically added if necessary. + expectedPropertyIris: Set[SmartIri] = resourceReadyToCreate.values.keySet ++ (if (resourceReadyToCreate.hasStandoffLink) { + Some(OntologyConstants.KnoraBase.HasStandoffLinkToValue.toSmartIri) + } else { + None + }) + + _ = if (savedPropertyIris != expectedPropertyIris) { + throw AssertionException(s"Resource <$resourceIri> was saved, but it has the wrong properties: expected (${expectedPropertyIris.map(_.toSparql).mkString(", ")}), but saved (${savedPropertyIris.map(_.toSparql).mkString(", ")})") } - _ = resource.values.foreach { + // Ignore knora-base:hasStandoffLinkToValue when checking the expected values. + _ = (resource.values - OntologyConstants.KnoraBase.HasStandoffLinkToValue.toSmartIri).foreach { case (propertyIri: SmartIri, savedValues: Seq[ReadValueV2]) => val expectedValues: Seq[UnverifiedValueV2] = resourceReadyToCreate.values(propertyIri) diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala index 63b95943f3..bc419cced5 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala @@ -560,8 +560,7 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde private def generateSparqlToCreateMultipleValuesV2(createMultipleValuesRequest: GenerateSparqlToCreateMultipleValuesRequestV2): Future[GenerateSparqlToCreateMultipleValuesResponseV2] = { for { // Generate SPARQL to create links and LinkValues for standoff links in text values. - - sparqlForStandoffLinks: String <- generateInsertSparqlForStandoffLinksInMultipleValues(createMultipleValuesRequest) + sparqlForStandoffLinks: Option[String] <- generateInsertSparqlForStandoffLinksInMultipleValues(createMultipleValuesRequest) // Generate SPARQL for each value. sparqlForPropertyValueFutures: Map[SmartIri, Seq[Future[InsertSparqlWithUnverifiedValue]]] = createMultipleValuesRequest.values.map { @@ -582,7 +581,7 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde sparqlForPropertyValues: Map[SmartIri, Seq[InsertSparqlWithUnverifiedValue]] <- ActorUtil.sequenceSeqFuturesInMap(sparqlForPropertyValueFutures) // Concatenate all the generated SPARQL. - allInsertSparql: String = sparqlForPropertyValues.values.flatten.map(_.insertSparql).mkString("\n\n") + "\n\n" + sparqlForStandoffLinks + allInsertSparql: String = sparqlForPropertyValues.values.flatten.map(_.insertSparql).mkString("\n\n") + "\n\n" + sparqlForStandoffLinks.getOrElse("") // Collect all the unverified values. unverifiedValues: Map[SmartIri, Seq[UnverifiedValueV2]] = sparqlForPropertyValues.map { @@ -590,7 +589,8 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } } yield GenerateSparqlToCreateMultipleValuesResponseV2( insertSparql = allInsertSparql, - unverifiedValues = unverifiedValues + unverifiedValues = unverifiedValues, + hasStandoffLink = sparqlForStandoffLinks.isDefined ) } @@ -694,66 +694,72 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde * @param createMultipleValuesRequest the request to create multiple values. * @return SPARQL INSERT statements. */ - private def generateInsertSparqlForStandoffLinksInMultipleValues(createMultipleValuesRequest: GenerateSparqlToCreateMultipleValuesRequestV2): Future[String] = { + private def generateInsertSparqlForStandoffLinksInMultipleValues(createMultipleValuesRequest: GenerateSparqlToCreateMultipleValuesRequestV2): Future[Option[String]] = { // To create LinkValues for the standoff links in the values to be created, we need to compute // the initial reference count of each LinkValue. This is equal to the number of TextValues in the resource // that have standoff links to a particular target resource. // First, get the standoff link targets from all the text values to be created. val standoffLinkTargetsPerTextValue: Vector[Set[IRI]] = createMultipleValuesRequest.flatValues.foldLeft(Vector.empty[Set[IRI]]) { - case (acc: Vector[Set[IRI]], createValueV2: GenerateSparqlForValueInNewResourceV2) => + case (standoffLinkTargetsAcc: Vector[Set[IRI]], createValueV2: GenerateSparqlForValueInNewResourceV2) => createValueV2.valueContent match { - case textValueContentV2: TextValueContentV2 => acc :+ textValueContentV2.standoffLinkTagTargetResourceIris - case _ => acc + case textValueContentV2: TextValueContentV2 if textValueContentV2.standoffLinkTagTargetResourceIris.nonEmpty => + standoffLinkTargetsAcc :+ textValueContentV2.standoffLinkTagTargetResourceIris + + case _ => standoffLinkTargetsAcc } } - // Combine those resource references into a single list, so if there are n text values with a link to - // some IRI, the list will contain that IRI n times. - val allStandoffLinkTargets: Vector[IRI] = standoffLinkTargetsPerTextValue.flatten + if (standoffLinkTargetsPerTextValue.nonEmpty) { + // Combine those resource references into a single list, so if there are n text values with a link to + // some IRI, the list will contain that IRI n times. + val allStandoffLinkTargets: Vector[IRI] = standoffLinkTargetsPerTextValue.flatten - // Now we need to count the number of times each IRI occurs in allStandoffLinkTargets. To do this, first - // use groupBy(identity). The groupBy method takes a function that returns a key for each item in the - // collection, and makes a Map in which items with the same key are grouped together. The identity - // function just returns its argument. So groupBy(identity) makes a Map[IRI, Vector[IRI]] in which each - // IRI points to a sequence of the same IRI repeated as many times as it occurred in allStandoffLinkTargets. - val allStandoffLinkTargetsGrouped: Map[IRI, Vector[IRI]] = allStandoffLinkTargets.groupBy(identity) + // Now we need to count the number of times each IRI occurs in allStandoffLinkTargets. To do this, first + // use groupBy(identity). The groupBy method takes a function that returns a key for each item in the + // collection, and makes a Map in which items with the same key are grouped together. The identity + // function just returns its argument. So groupBy(identity) makes a Map[IRI, Vector[IRI]] in which each + // IRI points to a sequence of the same IRI repeated as many times as it occurred in allStandoffLinkTargets. + val allStandoffLinkTargetsGrouped: Map[IRI, Vector[IRI]] = allStandoffLinkTargets.groupBy(identity) - // Replace each Vector[IRI] with its size. That's the number of text values containing - // standoff links to that IRI. - val initialReferenceCounts: Map[IRI, Int] = allStandoffLinkTargetsGrouped.mapValues(_.size) + // Replace each Vector[IRI] with its size. That's the number of text values containing + // standoff links to that IRI. + val initialReferenceCounts: Map[IRI, Int] = allStandoffLinkTargetsGrouped.mapValues(_.size) - for { - newValueIri: IRI <- makeUnusedValueIri(createMultipleValuesRequest.resourceIri) + for { + newValueIri: IRI <- makeUnusedValueIri(createMultipleValuesRequest.resourceIri) - // For each standoff link target IRI, construct a SparqlTemplateLinkUpdate to create a hasStandoffLinkTo property - // and one LinkValue with its initial reference count. - standoffLinkUpdates: Seq[SparqlTemplateLinkUpdate] = initialReferenceCounts.toSeq.map { - case (targetIri, initialReferenceCount) => - SparqlTemplateLinkUpdate( - linkPropertyIri = OntologyConstants.KnoraBase.HasStandoffLinkTo.toSmartIri, - directLinkExists = false, - insertDirectLink = true, - deleteDirectLink = false, - linkValueExists = false, - linkTargetExists = true, // doesn't matter, the generateInsertStatementsForStandoffLinks template doesn't use it - newLinkValueIri = newValueIri, - linkTargetIri = targetIri, - currentReferenceCount = 0, - newReferenceCount = initialReferenceCount, - newLinkValueCreator = OntologyConstants.KnoraAdmin.SystemUser, - newLinkValuePermissions = standoffLinkValuePermissions - ) - } + // For each standoff link target IRI, construct a SparqlTemplateLinkUpdate to create a hasStandoffLinkTo property + // and one LinkValue with its initial reference count. + standoffLinkUpdates: Seq[SparqlTemplateLinkUpdate] = initialReferenceCounts.toSeq.map { + case (targetIri, initialReferenceCount) => + SparqlTemplateLinkUpdate( + linkPropertyIri = OntologyConstants.KnoraBase.HasStandoffLinkTo.toSmartIri, + directLinkExists = false, + insertDirectLink = true, + deleteDirectLink = false, + linkValueExists = false, + linkTargetExists = true, // doesn't matter, the generateInsertStatementsForStandoffLinks template doesn't use it + newLinkValueIri = newValueIri, + linkTargetIri = targetIri, + currentReferenceCount = 0, + newReferenceCount = initialReferenceCount, + newLinkValueCreator = OntologyConstants.KnoraAdmin.SystemUser, + newLinkValuePermissions = standoffLinkValuePermissions + ) + } - // Generate SPARQL INSERT statements based on those SparqlTemplateLinkUpdates. - sparqlInsert = org.knora.webapi.messages.twirl.queries.sparql.v2.txt.generateInsertStatementsForStandoffLinks( - resourceIri = createMultipleValuesRequest.resourceIri, - linkUpdates = standoffLinkUpdates, - creationDate = createMultipleValuesRequest.creationDate, - stringFormatter = stringFormatter - ).toString() - } yield sparqlInsert + // Generate SPARQL INSERT statements based on those SparqlTemplateLinkUpdates. + sparqlInsert = org.knora.webapi.messages.twirl.queries.sparql.v2.txt.generateInsertStatementsForStandoffLinks( + resourceIri = createMultipleValuesRequest.resourceIri, + linkUpdates = standoffLinkUpdates, + creationDate = createMultipleValuesRequest.creationDate, + stringFormatter = stringFormatter + ).toString() + } yield Some(sparqlInsert) + } else { + FastFuture.successful(None) + } } /** 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 e5286c1c1d..49664c9c03 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 @@ -1687,6 +1687,52 @@ class ResourcesRouteV2E2ESpec extends E2ESpec(ResourcesRouteV2E2ESpec.config) { val previewResponseAsString = responseToString(previewResponse) assert(previewResponse.status == StatusCodes.NotFound, previewResponseAsString) } + + "create a resource containing a text value with a standoff link" in { + val jsonLDEntity = + """{ + | "@type": "anything:Thing", + | "anything:hasText": { + | "@type": "knora-api:TextValue", + | "knora-api:textValueAsXml": "\n\n This text links to another resource.\n", + | "knora-api:textValueHasMapping": { + | "@id": "http://rdfh.ch/standoff/mappings/StandardMapping" + | } + | }, + | "knora-api:attachedToProject": { + | "@id": "http://rdfh.ch/projects/0001" + | }, + | "rdfs:label": "obj_inst1", + | "@context": { + | "anything": "http://0.0.0.0:3333/ontology/0001/anything/v2#", + | "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + | "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + | "knora-api": "http://api.knora.org/ontology/knora-api/v2#" + | } + |}""".stripMargin + + val request = Post(s"$baseApiUrl/v2/resources", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLDEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val response: HttpResponse = singleAwaitingRequest(request) + assert(response.status == StatusCodes.OK, response.toString) + val responseJsonDoc: JsonLDDocument = responseToJsonLDDocument(response) + val resourceIri: IRI = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.ID, stringFormatter.validateAndEscapeIri) + assert(resourceIri.toSmartIri.isKnoraDataIri) + + // Request the newly created resource in the complex schema, and check that it matches the ontology. + val resourceComplexGetRequest = Get(s"$baseApiUrl/v2/resources/${URLEncoder.encode(resourceIri, "UTF-8")}") ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val resourceComplexGetResponse: HttpResponse = singleAwaitingRequest(resourceComplexGetRequest) + val resourceComplexGetResponseAsString = responseToString(resourceComplexGetResponse) + + instanceChecker.check( + instanceResponse = resourceComplexGetResponseAsString, + expectedClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, + knoraRouteGet = doGetRequest + ) + + // Check that it has the property knora-api:hasStandoffLinkToValue. + val resourceJsonLDDoc = JsonLDUtil.parseJsonLD(resourceComplexGetResponseAsString) + assert(resourceJsonLDDoc.body.value.contains(OntologyConstants.KnoraApiV2Complex.HasStandoffLinkToValue)) + } } }