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

feat(api-v2): Change GUI element and attribute of a property (DSP-1600) #1855

Merged
merged 10 commits into from May 17, 2021
37 changes: 37 additions & 0 deletions docs/03-apis/api-v2/ontology-information.md
Expand Up @@ -1410,6 +1410,43 @@ HTTP PUT to http://host/v2/ontologies/properties
Values for `rdfs:comment` must be submitted in at least one language,
either as an object or as an array of objects.

### Changing the GUI Element and GUI Attributes of a Property

This operation is permitted even if the property is used in data.

```
HTTP PUT to http://host/v2/ontologies/properties/guielement
```

```jsonld
{
"@id" : "ONTOLOGY_IRI",
"@type" : "owl:Ontology",
"knora-api:lastModificationDate" : {
"@type" : "xsd:dateTimeStamp",
"@value" : "ONTOLOGY_LAST_MODIFICATION_DATE"
},
"@graph" : [ {
"@id" : "PROPERTY_IRI",
"@type" : "owl:ObjectProperty",
"salsah-gui:guiElement" : {
"@id" : "salsah-gui:Textarea"
},
"salsah-gui:guiAttribute" : [ "cols=80", "rows=24" ]
} ],
"@context" : {
"rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
"knora-api" : "http://api.knora.org/ontology/knora-api/v2#",
"salsah-gui" : "http://api.knora.org/ontology/salsah-gui/v2#",
"owl" : "http://www.w3.org/2002/07/owl#",
"rdfs" : "http://www.w3.org/2000/01/rdf-schema#",
"xsd" : "http://www.w3.org/2001/XMLSchema#"
}
```

To remove the values of `salsah-gui:guiElement` and `salsah-gui:guiAttribute` from
the property definition, submit the request without those predicates.

### Adding Cardinalities to a Class

This operation is not permitted if the class is used in data, or if it
Expand Down
2 changes: 2 additions & 0 deletions webapi/scripts/expected-client-test-data.txt
Expand Up @@ -184,6 +184,7 @@ test-data/v2/ontologies/change-class-comment-request.json
test-data/v2/ontologies/change-class-label-request.json
test-data/v2/ontologies/change-gui-order-request.json
test-data/v2/ontologies/change-property-comment-request.json
test-data/v2/ontologies/change-property-guielement-request.json
test-data/v2/ontologies/change-property-label-request.json
test-data/v2/ontologies/create-class-with-cardinalities-request.json
test-data/v2/ontologies/create-class-without-cardinalities-request.json
Expand Down Expand Up @@ -213,6 +214,7 @@ test-data/v2/ontologies/knora-api-ontology.json
test-data/v2/ontologies/minimal-ontology.json
test-data/v2/ontologies/remove-class-cardinalities-request.json
test-data/v2/ontologies/remove-property-cardinality-request.json
test-data/v2/ontologies/remove-property-guielement-request.json
test-data/v2/ontologies/replace-class-cardinalities-request.json
test-data/v2/ontologies/update-ontology-metadata-request.json
test-data/v2/resources/
Expand Down
Expand Up @@ -748,6 +748,109 @@ sealed trait ChangeLabelsOrCommentsRequest {
val newObjects: Seq[StringLiteralV2]
}

/**
* Requests that the `salsah-gui:guiElement` and `salsah-gui:guiAttribute` of a property are changed.
*
* @param propertyIri the IRI of the property to be changed.
* @param newGuiElement the new GUI element to be used with the property, or `None` if no GUI element should be specified.
* @param newGuiAttributes the new GUI attributes to be used with the property, or `None` if no GUI element should be specified.
* @param lastModificationDate the ontology's last modification date.
* @param apiRequestID the ID of the API request.
* @param featureFactoryConfig the feature factory configuration.
* @param requestingUser the user making the request.
*/
case class ChangePropertyGuiElementRequest(propertyIri: SmartIri,
newGuiElement: Option[SmartIri],
newGuiAttributes: Set[String],
lastModificationDate: Instant,
apiRequestID: UUID,
featureFactoryConfig: FeatureFactoryConfig,
requestingUser: UserADM)
extends OntologiesResponderRequestV2

/**
* Constructs instances of [[ChangePropertyGuiElementRequest]] based on JSON-LD input.
*/
object ChangePropertyGuiElementRequest extends KnoraJsonLDRequestReaderV2[ChangePropertyGuiElementRequest] {

/**
* Converts a JSON-LD request to a [[ChangePropertyGuiElementRequest]].
*
* @param jsonLDDocument the JSON-LD input.
* @param apiRequestID the UUID of the API request.
* @param requestingUser the user making the request.
* @param responderManager a reference to the responder manager.
* @param storeManager a reference to the store manager.
* @param featureFactoryConfig the feature factory configuration.
* @param settings the application settings.
* @param log a logging adapter.
* @return a [[ChangePropertyLabelsOrCommentsRequestV2]] representing the input.
*/
override def fromJsonLD(jsonLDDocument: JsonLDDocument,
apiRequestID: UUID,
requestingUser: UserADM,
responderManager: ActorRef,
storeManager: ActorRef,
featureFactoryConfig: FeatureFactoryConfig,
settings: KnoraSettingsImpl,
log: LoggingAdapter)(
implicit timeout: Timeout,
executionContext: ExecutionContext): Future[ChangePropertyGuiElementRequest] = {
Future {
fromJsonLDSync(
jsonLDDocument = jsonLDDocument,
apiRequestID = apiRequestID,
featureFactoryConfig = featureFactoryConfig,
requestingUser = requestingUser
)
}
}

private def fromJsonLDSync(jsonLDDocument: JsonLDDocument,
apiRequestID: UUID,
featureFactoryConfig: FeatureFactoryConfig,
requestingUser: UserADM): ChangePropertyGuiElementRequest = {
implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance

val inputOntologiesV2 = InputOntologyV2.fromJsonLD(jsonLDDocument)
val propertyUpdateInfo = OntologyUpdateHelper.getPropertyDef(inputOntologiesV2)
val propertyInfoContent = propertyUpdateInfo.propertyInfoContent
val lastModificationDate = propertyUpdateInfo.lastModificationDate

val newGuiElement: Option[SmartIri] =
propertyInfoContent.predicates
.get(OntologyConstants.SalsahGuiApiV2WithValueObjects.GuiElementProp.toSmartIri)
.map { predicateInfoV2: PredicateInfoV2 =>
predicateInfoV2.objects.head match {
case iriLiteralV2: SmartIriLiteralV2 => iriLiteralV2.value
case other =>
throw BadRequestException(s"Unexpected object for salsah-gui:guiElement: $other")
}
}

val newGuiAttributes: Set[String] =
propertyInfoContent.predicates
.get(OntologyConstants.SalsahGuiApiV2WithValueObjects.GuiAttribute.toSmartIri)
.map { predicateInfoV2: PredicateInfoV2 =>
predicateInfoV2.objects.map {
case stringLiteralV2: StringLiteralV2 => stringLiteralV2.value
case other => throw BadRequestException(s"Unexpected object for salsah-gui:guiAttribute: $other")
}.toSet
}
.getOrElse(Set.empty[String])

ChangePropertyGuiElementRequest(
propertyIri = propertyInfoContent.propertyIri,
newGuiElement = newGuiElement,
newGuiAttributes = newGuiAttributes,
lastModificationDate = lastModificationDate,
apiRequestID = apiRequestID,
featureFactoryConfig = featureFactoryConfig,
requestingUser = requestingUser
)
}
}

/**
* Requests that a property's labels or comments are changed. A successful response will be a [[ReadOntologyV2]].
*
Expand Down
Expand Up @@ -150,6 +150,8 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon
case createPropertyRequest: CreatePropertyRequestV2 => createProperty(createPropertyRequest)
case changePropertyLabelsOrCommentsRequest: ChangePropertyLabelsOrCommentsRequestV2 =>
changePropertyLabelsOrComments(changePropertyLabelsOrCommentsRequest)
case changePropertyGuiElementRequest: ChangePropertyGuiElementRequest =>
changePropertyGuiElement(changePropertyGuiElementRequest)
case deletePropertyRequest: DeletePropertyRequestV2 => deleteProperty(deletePropertyRequest)
case deleteOntologyRequest: DeleteOntologyRequestV2 => deleteOntology(deleteOntologyRequest)
case other => handleUnexpectedMessage(other, log, this.getClass.getName)
Expand Down Expand Up @@ -3978,6 +3980,208 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon
} yield taskResult
}

/**
* Changes the values of `salsah-gui:guiElement` and `salsah-gui:guiAttribute` in a property definition.
*
* @param changePropertyGuiElementRequest the request to change the property's GUI element and GUI attribute.
* @return a [[ReadOntologyV2]] containing the modified property definition.
*/
private def changePropertyGuiElement(
changePropertyGuiElementRequest: ChangePropertyGuiElementRequest): Future[ReadOntologyV2] = {
def makeTaskFuture(internalPropertyIri: SmartIri, internalOntologyIri: SmartIri): Future[ReadOntologyV2] = {
for {
cacheData <- getCacheData

ontology = cacheData.ontologies(internalOntologyIri)

currentReadPropertyInfo: ReadPropertyInfoV2 = ontology.properties.getOrElse(
internalPropertyIri,
throw NotFoundException(s"Property ${changePropertyGuiElementRequest.propertyIri} not found"))

// Check that the ontology exists and has not been updated by another user since the client last read it.
_ <- checkOntologyLastModificationDateBeforeUpdate(
internalOntologyIri = internalOntologyIri,
expectedLastModificationDate = changePropertyGuiElementRequest.lastModificationDate,
featureFactoryConfig = changePropertyGuiElementRequest.featureFactoryConfig
)

// If this is a link property, also change the GUI element and attribute of the corresponding link value property.

maybeCurrentLinkValueReadPropertyInfo: Option[ReadPropertyInfoV2] = if (currentReadPropertyInfo.isLinkProp) {
val linkValuePropertyIri = internalPropertyIri.fromLinkPropToLinkValueProp
Some(
ontology.properties.getOrElse(
linkValuePropertyIri,
throw InconsistentRepositoryDataException(s"Link value property $linkValuePropertyIri not found")))
} else {
None
}

// Do the update.

currentTime: Instant = Instant.now

updateSparql = org.knora.webapi.messages.twirl.queries.sparql.v2.txt
.changePropertyGuiElement(
triplestore = settings.triplestoreType,
ontologyNamedGraphIri = internalOntologyIri,
ontologyIri = internalOntologyIri,
propertyIri = internalPropertyIri,
maybeLinkValuePropertyIri = maybeCurrentLinkValueReadPropertyInfo.map(_.entityInfoContent.propertyIri),
maybeNewGuiElement = changePropertyGuiElementRequest.newGuiElement,
newGuiAttributes = changePropertyGuiElementRequest.newGuiAttributes,
lastModificationDate = changePropertyGuiElementRequest.lastModificationDate,
currentTime = currentTime
)
.toString()

_ <- (storeManager ? SparqlUpdateRequest(updateSparql)).mapTo[SparqlUpdateResponse]

// Check that the ontology's last modification date was updated.

_ <- checkOntologyLastModificationDateAfterUpdate(
internalOntologyIri = internalOntologyIri,
expectedLastModificationDate = currentTime,
featureFactoryConfig = changePropertyGuiElementRequest.featureFactoryConfig
)

// Check that the data that was saved corresponds to the data that was submitted. To make this comparison,
// we have to undo the SPARQL-escaping of the input.

loadedPropertyDef <- loadPropertyDefinition(
propertyIri = internalPropertyIri,
featureFactoryConfig = changePropertyGuiElementRequest.featureFactoryConfig
)

maybeNewGuiElementPredicate: Option[(SmartIri, PredicateInfoV2)] = changePropertyGuiElementRequest.newGuiElement
.map { guiElement: SmartIri =>
OntologyConstants.SalsahGui.GuiElementProp.toSmartIri -> PredicateInfoV2(
predicateIri = OntologyConstants.SalsahGui.GuiElementProp.toSmartIri,
objects = Seq(SmartIriLiteralV2(guiElement))
)
}

maybeUnescapedNewGuiAttributePredicate: Option[(SmartIri, PredicateInfoV2)] = if (changePropertyGuiElementRequest.newGuiAttributes.nonEmpty) {
Some(
OntologyConstants.SalsahGui.GuiAttribute.toSmartIri -> PredicateInfoV2(
predicateIri = OntologyConstants.SalsahGui.GuiAttribute.toSmartIri,
objects = changePropertyGuiElementRequest.newGuiAttributes.map(StringLiteralV2(_)).toSeq
))
} else {
None
}

unescapedNewPropertyDef: PropertyInfoContentV2 = currentReadPropertyInfo.entityInfoContent.copy(
predicates = currentReadPropertyInfo.entityInfoContent.predicates -
OntologyConstants.SalsahGui.GuiElementProp.toSmartIri -
OntologyConstants.SalsahGui.GuiAttribute.toSmartIri ++
maybeNewGuiElementPredicate ++
maybeUnescapedNewGuiAttributePredicate
)

_ = if (loadedPropertyDef != unescapedNewPropertyDef) {
throw InconsistentRepositoryDataException(
s"Attempted to save property definition $unescapedNewPropertyDef, but $loadedPropertyDef was saved")
}

maybeLoadedLinkValuePropertyDefFuture: Option[Future[PropertyInfoContentV2]] = maybeCurrentLinkValueReadPropertyInfo
.map { linkValueReadPropertyInfo =>
loadPropertyDefinition(
propertyIri = linkValueReadPropertyInfo.entityInfoContent.propertyIri,
featureFactoryConfig = changePropertyGuiElementRequest.featureFactoryConfig
)
}

maybeLoadedLinkValuePropertyDef: Option[PropertyInfoContentV2] <- ActorUtil.optionFuture2FutureOption(
maybeLoadedLinkValuePropertyDefFuture)

maybeUnescapedNewLinkValuePropertyDef: Option[PropertyInfoContentV2] = maybeLoadedLinkValuePropertyDef.map {
loadedLinkValuePropertyDef =>
val unescapedNewLinkPropertyDef = maybeCurrentLinkValueReadPropertyInfo.get.entityInfoContent.copy(
predicates = maybeCurrentLinkValueReadPropertyInfo.get.entityInfoContent.predicates -
OntologyConstants.SalsahGui.GuiElementProp.toSmartIri -
OntologyConstants.SalsahGui.GuiAttribute.toSmartIri ++
maybeNewGuiElementPredicate ++
maybeUnescapedNewGuiAttributePredicate
)

if (loadedLinkValuePropertyDef != unescapedNewLinkPropertyDef) {
throw InconsistentRepositoryDataException(
s"Attempted to save link value property definition $unescapedNewLinkPropertyDef, but $loadedLinkValuePropertyDef was saved")
}

unescapedNewLinkPropertyDef
}

// Update the ontology cache, using the unescaped definition(s).

newReadPropertyInfo = ReadPropertyInfoV2(
entityInfoContent = unescapedNewPropertyDef,
isEditable = true,
isResourceProp = true,
isLinkProp = currentReadPropertyInfo.isLinkProp
)

maybeLinkValuePropertyCacheEntry: Option[(SmartIri, ReadPropertyInfoV2)] = maybeUnescapedNewLinkValuePropertyDef
.map { unescapedNewLinkPropertyDef =>
unescapedNewLinkPropertyDef.propertyIri -> ReadPropertyInfoV2(
entityInfoContent = unescapedNewLinkPropertyDef,
isResourceProp = true,
isLinkValueProp = true
)
}

updatedOntologyMetadata = ontology.ontologyMetadata.copy(
lastModificationDate = Some(currentTime)
)

updatedOntology = ontology.copy(
ontologyMetadata = updatedOntologyMetadata,
properties = ontology.properties ++ maybeLinkValuePropertyCacheEntry + (internalPropertyIri -> newReadPropertyInfo)
)

_ = storeCacheData(
cacheData.copy(
ontologies = cacheData.ontologies + (internalOntologyIri -> updatedOntology)
))

// Read the data back from the cache.

response <- getPropertyDefinitionsFromOntologyV2(
propertyIris = Set(internalPropertyIri),
allLanguages = true,
requestingUser = changePropertyGuiElementRequest.requestingUser)
} yield response
}

for {
requestingUser <- FastFuture.successful(changePropertyGuiElementRequest.requestingUser)

externalPropertyIri = changePropertyGuiElementRequest.propertyIri
externalOntologyIri = externalPropertyIri.getOntologyFromEntity

_ <- checkOntologyAndEntityIrisForUpdate(
externalOntologyIri = externalOntologyIri,
externalEntityIri = externalPropertyIri,
requestingUser = requestingUser
)

internalPropertyIri = externalPropertyIri.toOntologySchema(InternalSchema)
internalOntologyIri = externalOntologyIri.toOntologySchema(InternalSchema)

// Do the remaining pre-update checks and the update while holding a global ontology cache lock.
taskResult <- IriLocker.runWithIriLock(
apiRequestID = changePropertyGuiElementRequest.apiRequestID,
iri = ONTOLOGY_CACHE_LOCK_IRI,
task = () =>
makeTaskFuture(
internalPropertyIri = internalPropertyIri,
internalOntologyIri = internalOntologyIri
)
)
} yield taskResult
}

/**
* Changes the values of `rdfs:label` or `rdfs:comment` in a property definition.
*
Expand Down