Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(api-v2): Fix post-update check for resource with standoff link (DSP-841) #1728

Merged
merged 2 commits into from Oct 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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