Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
fix(api-v2): Fix post-update check for resource with standoff link (D…
…SP-841) (#1728)
  • Loading branch information
Benjamin Geer committed Oct 13, 2020
1 parent 6c2e903 commit 35d449f
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 55 deletions.
6 changes: 6 additions & 0 deletions docs/05-internals/development/generating-client-test-data.md
Expand Up @@ -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:

```
Expand Down
Expand Up @@ -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.
Expand Down
Expand Up @@ -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.
Expand Down Expand Up @@ -701,7 +703,8 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt
resourceLabel = internalCreateResource.label,
resourceCreationDate = creationDate
),
values = sparqlForValuesResponse.unverifiedValues
values = sparqlForValuesResponse.unverifiedValues,
hasStandoffLink = sparqlForValuesResponse.hasStandoffLink
)
}

Expand Down Expand Up @@ -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)

Expand Down
Expand Up @@ -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 {
Expand All @@ -582,15 +581,16 @@ 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 {
case (propertyIri, unverifiedValuesWithSparql) => propertyIri -> unverifiedValuesWithSparql.map(_.unverifiedValue)
}
} yield GenerateSparqlToCreateMultipleValuesResponseV2(
insertSparql = allInsertSparql,
unverifiedValues = unverifiedValues
unverifiedValues = unverifiedValues,
hasStandoffLink = sparqlForStandoffLinks.isDefined
)
}

Expand Down Expand Up @@ -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)
}
}

/**
Expand Down
Expand Up @@ -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": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<text>\n This text links to another <a class=\"salsah-link\" href=\"http://rdfh.ch/0001/another-thing\">resource</a>.\n</text>",
| "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))
}
}
}

Expand Down

0 comments on commit 35d449f

Please sign in to comment.