Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat(api-v2): Change GUI element and attribute of a property (DSP-160…
…0) (#1855)
  • Loading branch information
Benjamin Geer committed May 17, 2021
1 parent 74feb2c commit ce9ba3a
Show file tree
Hide file tree
Showing 8 changed files with 769 additions and 4 deletions.
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

0 comments on commit ce9ba3a

Please sign in to comment.