diff --git a/docs/03-apis/api-v2/ontology-information.md b/docs/03-apis/api-v2/ontology-information.md
index 05af7b328f..b54918aaba 100644
--- a/docs/03-apis/api-v2/ontology-information.md
+++ b/docs/03-apis/api-v2/ontology-information.md
@@ -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
diff --git a/webapi/scripts/expected-client-test-data.txt b/webapi/scripts/expected-client-test-data.txt
index e4f7495817..8ad3232871 100644
--- a/webapi/scripts/expected-client-test-data.txt
+++ b/webapi/scripts/expected-client-test-data.txt
@@ -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
@@ -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/
diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/OntologyMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/OntologyMessagesV2.scala
index b71654847a..635706ee87 100644
--- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/OntologyMessagesV2.scala
+++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/OntologyMessagesV2.scala
@@ -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]].
*
diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/OntologyResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/OntologyResponderV2.scala
index 72f7d697c6..6cc77676e6 100644
--- a/webapi/src/main/scala/org/knora/webapi/responders/v2/OntologyResponderV2.scala
+++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/OntologyResponderV2.scala
@@ -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)
@@ -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.
*
diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/OntologiesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/OntologiesRouteV2.scala
index 20e942722c..8c317310ea 100644
--- a/webapi/src/main/scala/org/knora/webapi/routing/v2/OntologiesRouteV2.scala
+++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/OntologiesRouteV2.scala
@@ -66,7 +66,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData)
deleteClass(featureFactoryConfig) ~
deleteOntologyComment(featureFactoryConfig) ~
createProperty(featureFactoryConfig) ~
- updateProperty(featureFactoryConfig) ~
+ updatePropertyLabelsOrComments(featureFactoryConfig) ~
+ updatePropertyGuiElement(featureFactoryConfig) ~
getProperties(featureFactoryConfig) ~
deleteProperty(featureFactoryConfig) ~
createOntology(featureFactoryConfig) ~
@@ -692,7 +693,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData)
}
}
- private def updateProperty(featureFactoryConfig: FeatureFactoryConfig): Route =
+ private def updatePropertyLabelsOrComments(featureFactoryConfig: FeatureFactoryConfig): Route =
path(OntologiesBasePath / "properties") {
put {
// Change the labels or comments of a property.
@@ -734,6 +735,48 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData)
}
}
+ private def updatePropertyGuiElement(featureFactoryConfig: FeatureFactoryConfig): Route =
+ path(OntologiesBasePath / "properties" / "guielement") {
+ put {
+ // Change the salsah-gui:guiElement and/or salsah-gui:guiAttribute of a property.
+ entity(as[String]) { jsonRequest => requestContext =>
+ {
+ val requestMessageFuture: Future[ChangePropertyGuiElementRequest] = for {
+ requestingUser <- getUserADM(
+ requestContext = requestContext,
+ featureFactoryConfig = featureFactoryConfig
+ )
+
+ requestDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonRequest)
+
+ requestMessage: ChangePropertyGuiElementRequest <- ChangePropertyGuiElementRequest
+ .fromJsonLD(
+ jsonLDDocument = requestDoc,
+ apiRequestID = UUID.randomUUID,
+ requestingUser = requestingUser,
+ responderManager = responderManager,
+ storeManager = storeManager,
+ featureFactoryConfig = featureFactoryConfig,
+ settings = settings,
+ log = log
+ )
+ } yield requestMessage
+
+ RouteUtilV2.runRdfRouteWithFuture(
+ requestMessageF = requestMessageFuture,
+ requestContext = requestContext,
+ featureFactoryConfig = featureFactoryConfig,
+ settings = settings,
+ responderManager = responderManager,
+ log = log,
+ targetSchema = ApiV2Complex,
+ schemaOptions = RouteUtilV2.getSchemaOptions(requestContext)
+ )
+ }
+ }
+ }
+ }
+
private def getProperties(featureFactoryConfig: FeatureFactoryConfig): Route =
path(OntologiesBasePath / "properties" / Segments) { externalPropertyIris: List[IRI] =>
get { requestContext =>
diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/changePropertyGuiElement.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/changePropertyGuiElement.scala.txt
new file mode 100644
index 0000000000..5b3870a0e2
--- /dev/null
+++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/changePropertyGuiElement.scala.txt
@@ -0,0 +1,164 @@
+@*
+ * Copyright © 2015-2021 the contributors (see Contributors.md).
+ *
+ * This file is part of Knora.
+ *
+ * Knora is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Knora is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with Knora. If not, see .
+ *@
+
+@import org.knora.webapi.exceptions.SparqlGenerationException
+@import org.knora.webapi.messages.SmartIri
+@import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2
+@import java.time.Instant
+
+@*
+ * Changes the salsah-gui:guiElement and salsah-gui:guiAttribute of a property.
+ *
+ * @param triplestore the name of the triplestore being used.
+ * @param ontologyNamedGraphIri the IRI of the named graph where the ontology is stored.
+ * @param ontologyIri the IRI of the ontology to be modified.
+ * @param propertyIri the IRI of the property to be modified.
+ * @param maybeLinkValuePropertyIri the IRI of the corresponding link value property, if any, to be updated.
+ * @param maybeNewGuiElement the property's new salsah-gui:guiElement, or None if it should just be deleted.
+ * @param newGuiAttributes the property's new salsah-gui:guiAttribute values, or an empty set if they should just be deleted.
+ * @param lastModificationDate the xsd:dateTimeStamp that was attached to the ontology when it was last modified.
+ * @param currentTime an xsd:dateTimeStamp that will be attached to the ontology.
+ *@
+@(triplestore: String,
+ ontologyNamedGraphIri: SmartIri,
+ ontologyIri: SmartIri,
+ propertyIri: SmartIri,
+ maybeLinkValuePropertyIri: Option[SmartIri],
+ maybeNewGuiElement: Option[SmartIri],
+ newGuiAttributes: Set[String],
+ lastModificationDate: Instant,
+ currentTime: Instant)
+
+PREFIX rdf:
+PREFIX rdfs:
+PREFIX owl:
+PREFIX xsd:
+PREFIX knora-base:
+PREFIX salsah-gui:
+
+DELETE {
+ GRAPH <@ontologyNamedGraphIri> {
+ <@propertyIri> salsah-gui:guiElement ?oldGuiElement .
+ <@propertyIri> salsah-gui:guiAttribute ?oldGuiAttribute .
+
+ @maybeLinkValuePropertyIri match {
+ case Some(linkValuePropertyIri) => {
+ <@linkValuePropertyIri> salsah-gui:guiElement ?oldLinkValuePropertyGuiElement .
+ <@linkValuePropertyIri> salsah-gui:guiAttribute ?oldLinkValuePropertyGuiAttribute .
+ }
+
+ case None => {}
+ }
+ }
+}
+@* Ensure that inference is not used in the WHERE clause of this update. *@
+@if(triplestore.startsWith("graphdb")) {
+ USING
+}
+WHERE {
+ GRAPH <@ontologyNamedGraphIri> {
+ <@ontologyIri> rdf:type owl:Ontology ;
+ knora-base:lastModificationDate "@lastModificationDate"^^xsd:dateTime .
+
+ OPTIONAL {
+ <@propertyIri> salsah-gui:guiElement ?oldGuiElement .
+ }
+
+ OPTIONAL {
+ <@propertyIri> salsah-gui:guiAttribute ?oldGuiAttribute .
+ }
+
+ @maybeLinkValuePropertyIri match {
+ case Some(linkValuePropertyIri) => {
+ OPTIONAL {
+ <@linkValuePropertyIri> salsah-gui:guiElement ?oldLinkValuePropertyGuiElement .
+ }
+
+ OPTIONAL {
+ <@linkValuePropertyIri> salsah-gui:guiAttribute ?oldLinkValuePropertyGuiAttribute .
+ }
+ }
+
+ case None => {}
+ }
+ }
+};
+INSERT {
+ GRAPH <@ontologyNamedGraphIri> {
+ @maybeNewGuiElement match {
+ case Some(newGuiElement) => {
+ <@propertyIri> salsah-gui:guiElement <@newGuiElement> .
+ }
+
+ case None => {}
+ }
+
+ @for(newGuiAttribute <- newGuiAttributes) {
+ <@propertyIri> salsah-gui:guiAttribute "@newGuiAttribute" .
+ }
+
+ @maybeLinkValuePropertyIri match {
+ case Some(linkValuePropertyIri) => {
+ @maybeNewGuiElement match {
+ case Some(newGuiElement) => {
+ <@linkValuePropertyIri> salsah-gui:guiElement <@newGuiElement> .
+ }
+
+ case None => {}
+ }
+
+ @for(newGuiAttribute <- newGuiAttributes) {
+ <@linkValuePropertyIri> salsah-gui:guiAttribute "@newGuiAttribute" .
+ }
+ }
+
+ case None => {}
+ }
+ }
+}
+@* Ensure that inference is not used in the WHERE clause of this update. *@
+@if(triplestore.startsWith("graphdb")) {
+ USING
+}
+WHERE {
+ GRAPH <@ontologyNamedGraphIri> {
+ <@ontologyIri> rdf:type owl:Ontology ;
+ knora-base:lastModificationDate "@lastModificationDate"^^xsd:dateTime .
+ }
+};
+DELETE {
+ GRAPH <@ontologyNamedGraphIri> {
+ <@ontologyIri> knora-base:lastModificationDate "@lastModificationDate"^^xsd:dateTime .
+ }
+}
+INSERT {
+ GRAPH <@ontologyNamedGraphIri> {
+ <@ontologyIri> knora-base:lastModificationDate "@currentTime"^^xsd:dateTime .
+ }
+}
+@* Ensure that inference is not used in the WHERE clause of this update. *@
+@if(triplestore.startsWith("graphdb")) {
+ USING
+}
+WHERE {
+ GRAPH <@ontologyNamedGraphIri> {
+ <@ontologyIri> rdf:type owl:Ontology ;
+ knora-base:lastModificationDate "@lastModificationDate"^^xsd:dateTime .
+ }
+}
diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/OntologyV2R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/OntologyV2R2RSpec.scala
index 5983058e42..77c7ec53ea 100644
--- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/OntologyV2R2RSpec.scala
+++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/OntologyV2R2RSpec.scala
@@ -973,6 +973,146 @@ class OntologyV2R2RSpec extends R2RSpec {
}
}
+ "change the salsah-gui:guiElement and salsah-gui:guiAttribute of a property" in {
+ val params =
+ s"""{
+ | "@id" : "${SharedOntologyTestDataADM.ANYTHING_ONTOLOGY_IRI_LocalHost}",
+ | "@type" : "owl:Ontology",
+ | "knora-api:lastModificationDate" : {
+ | "@type" : "xsd:dateTimeStamp",
+ | "@value" : "$anythingLastModDate"
+ | },
+ | "@graph" : [ {
+ | "@id" : "anything:hasName",
+ | "@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#",
+ | "anything" : "${SharedOntologyTestDataADM.ANYTHING_ONTOLOGY_IRI_LocalHost}#"
+ | }
+ |}""".stripMargin
+
+ clientTestDataCollector.addFile(
+ TestDataFileContent(
+ filePath = TestDataFilePath(
+ directoryPath = clientTestDataPath,
+ filename = "change-property-guielement-request",
+ fileExtension = "json"
+ ),
+ text = params
+ )
+ )
+
+ // Convert the submitted JSON-LD to an InputOntologyV2, without SPARQL-escaping, so we can compare it to the response.
+ val paramsAsInput: InputOntologyV2 = InputOntologyV2.fromJsonLD(JsonLDUtil.parseJsonLD(params)).unescape
+
+ Put("/v2/ontologies/properties/guielement", HttpEntity(RdfMediaTypes.`application/ld+json`, params)) ~> addCredentials(
+ BasicHttpCredentials(anythingUsername, password)) ~> ontologiesPath ~> check {
+ val responseStr = responseAs[String]
+ assert(status == StatusCodes.OK, responseStr)
+ val responseJsonDoc = responseToJsonLDDocument(response)
+
+ // Convert the response to an InputOntologyV2 and compare the relevant part of it to the request.
+ val responseAsInput: InputOntologyV2 =
+ InputOntologyV2.fromJsonLD(responseJsonDoc, parsingMode = TestResponseParsingModeV2).unescape
+
+ responseAsInput.properties.head._2
+ .predicates(OntologyConstants.SalsahGuiApiV2WithValueObjects.GuiElementProp.toSmartIri)
+ .objects
+ .toSet should ===(
+ paramsAsInput.properties.head._2
+ .predicates(OntologyConstants.SalsahGuiApiV2WithValueObjects.GuiElementProp.toSmartIri)
+ .objects
+ .toSet)
+
+ responseAsInput.properties.head._2
+ .predicates(OntologyConstants.SalsahGuiApiV2WithValueObjects.GuiAttribute.toSmartIri)
+ .objects
+ .toSet should ===(
+ paramsAsInput.properties.head._2
+ .predicates(OntologyConstants.SalsahGuiApiV2WithValueObjects.GuiAttribute.toSmartIri)
+ .objects
+ .toSet)
+
+ // Check that the ontology's last modification date was updated.
+ val newAnythingLastModDate = responseAsInput.ontologyMetadata.lastModificationDate.get
+ assert(newAnythingLastModDate.isAfter(anythingLastModDate))
+ anythingLastModDate = newAnythingLastModDate
+ }
+ }
+
+ "remove the salsah-gui:guiElement and salsah-gui:guiAttribute from a property" in {
+ val params =
+ s"""{
+ | "@id" : "${SharedOntologyTestDataADM.ANYTHING_ONTOLOGY_IRI_LocalHost}",
+ | "@type" : "owl:Ontology",
+ | "knora-api:lastModificationDate" : {
+ | "@type" : "xsd:dateTimeStamp",
+ | "@value" : "$anythingLastModDate"
+ | },
+ | "@graph" : [ {
+ | "@id" : "anything:hasName",
+ | "@type" : "owl:ObjectProperty"
+ | } ],
+ | "@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#",
+ | "anything" : "${SharedOntologyTestDataADM.ANYTHING_ONTOLOGY_IRI_LocalHost}#"
+ | }
+ |}""".stripMargin
+
+ clientTestDataCollector.addFile(
+ TestDataFileContent(
+ filePath = TestDataFilePath(
+ directoryPath = clientTestDataPath,
+ filename = "remove-property-guielement-request",
+ fileExtension = "json"
+ ),
+ text = params
+ )
+ )
+
+ // Convert the submitted JSON-LD to an InputOntologyV2, without SPARQL-escaping, so we can compare it to the response.
+ val paramsAsInput: InputOntologyV2 = InputOntologyV2.fromJsonLD(JsonLDUtil.parseJsonLD(params)).unescape
+
+ Put("/v2/ontologies/properties/guielement", HttpEntity(RdfMediaTypes.`application/ld+json`, params)) ~> addCredentials(
+ BasicHttpCredentials(anythingUsername, password)) ~> ontologiesPath ~> check {
+ val responseStr = responseAs[String]
+ assert(status == StatusCodes.OK, responseStr)
+ val responseJsonDoc = responseToJsonLDDocument(response)
+
+ // Convert the response to an InputOntologyV2 and compare the relevant part of it to the request.
+ val responseAsInput: InputOntologyV2 =
+ InputOntologyV2.fromJsonLD(responseJsonDoc, parsingMode = TestResponseParsingModeV2).unescape
+
+ assert(
+ !responseAsInput.properties.head._2.predicates
+ .contains(OntologyConstants.SalsahGuiApiV2WithValueObjects.GuiElementProp.toSmartIri))
+
+ assert(
+ !responseAsInput.properties.head._2.predicates
+ .contains(OntologyConstants.SalsahGuiApiV2WithValueObjects.GuiAttribute.toSmartIri))
+
+ // Check that the ontology's last modification date was updated.
+ val newAnythingLastModDate = responseAsInput.ontologyMetadata.lastModificationDate.get
+ assert(newAnythingLastModDate.isAfter(anythingLastModDate))
+ anythingLastModDate = newAnythingLastModDate
+ }
+ }
+
"create a class anything:WildThing that is a subclass of anything:Thing, with a direct cardinality for anything:hasName" in {
val params =
diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/OntologyResponderV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/OntologyResponderV2Spec.scala
index 63255372e6..f1c618e2da 100644
--- a/webapi/src/test/scala/org/knora/webapi/responders/v2/OntologyResponderV2Spec.scala
+++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/OntologyResponderV2Spec.scala
@@ -225,8 +225,6 @@ class OntologyResponderV2Spec extends CoreSpec() with ImplicitSender {
}
"delete the comment from 'foo'" in {
- val newLabel = "a label changed again"
-
responderManager ! DeleteOntologyCommentRequestV2(
ontologyIri = fooIri.get.toSmartIri.toOntologySchema(ApiV2Complex),
lastModificationDate = fooLastModDate,
@@ -3254,6 +3252,80 @@ class OntologyResponderV2Spec extends CoreSpec() with ImplicitSender {
}
}
+ "change the salsah-gui:guiElement and salsah-gui:guiAttribute of anything:hasNothingness" in {
+ val propertyIri = AnythingOntologyIri.makeEntityIri("hasNothingness")
+
+ responderManager ! ChangePropertyGuiElementRequest(
+ propertyIri = propertyIri,
+ newGuiElement = Some("http://api.knora.org/ontology/salsah-gui/v2#SimpleText".toSmartIri),
+ newGuiAttributes = Set("size=80"),
+ lastModificationDate = anythingLastModDate,
+ apiRequestID = UUID.randomUUID,
+ featureFactoryConfig = defaultFeatureFactoryConfig,
+ requestingUser = anythingAdminUser
+ )
+
+ expectMsgPF(timeout) {
+ case msg: ReadOntologyV2 =>
+ val externalOntology = msg.toOntologySchema(ApiV2Complex)
+ assert(externalOntology.properties.size == 1)
+ val property = externalOntology.properties(propertyIri)
+
+ property.entityInfoContent.predicates(
+ OntologyConstants.SalsahGuiApiV2WithValueObjects.GuiElementProp.toSmartIri) should ===(
+ PredicateInfoV2(
+ predicateIri = OntologyConstants.SalsahGuiApiV2WithValueObjects.GuiElementProp.toSmartIri,
+ objects = Seq(SmartIriLiteralV2("http://api.knora.org/ontology/salsah-gui/v2#SimpleText".toSmartIri))
+ ))
+
+ property.entityInfoContent.predicates(
+ OntologyConstants.SalsahGuiApiV2WithValueObjects.GuiAttribute.toSmartIri) should ===(
+ PredicateInfoV2(
+ predicateIri = OntologyConstants.SalsahGuiApiV2WithValueObjects.GuiAttribute.toSmartIri,
+ objects = Seq(StringLiteralV2("size=80"))
+ ))
+
+ val metadata = externalOntology.ontologyMetadata
+ val newAnythingLastModDate = metadata.lastModificationDate.getOrElse(
+ throw AssertionException(s"${metadata.ontologyIri} has no last modification date"))
+ assert(newAnythingLastModDate.isAfter(anythingLastModDate))
+ anythingLastModDate = newAnythingLastModDate
+ }
+ }
+
+ "delete the salsah-gui:guiElement and salsah-gui:guiAttribute of anything:hasNothingness" in {
+ val propertyIri = AnythingOntologyIri.makeEntityIri("hasNothingness")
+
+ responderManager ! ChangePropertyGuiElementRequest(
+ propertyIri = propertyIri,
+ newGuiElement = None,
+ newGuiAttributes = Set.empty,
+ lastModificationDate = anythingLastModDate,
+ apiRequestID = UUID.randomUUID,
+ featureFactoryConfig = defaultFeatureFactoryConfig,
+ requestingUser = anythingAdminUser
+ )
+
+ expectMsgPF(timeout) {
+ case msg: ReadOntologyV2 =>
+ val externalOntology = msg.toOntologySchema(ApiV2Complex)
+ assert(externalOntology.properties.size == 1)
+ val property = externalOntology.properties(propertyIri)
+
+ property.entityInfoContent.predicates
+ .get(OntologyConstants.SalsahGuiApiV2WithValueObjects.GuiElementProp.toSmartIri) should ===(None)
+
+ property.entityInfoContent.predicates
+ .get(OntologyConstants.SalsahGuiApiV2WithValueObjects.GuiAttribute.toSmartIri) should ===(None)
+
+ val metadata = externalOntology.ontologyMetadata
+ val newAnythingLastModDate = metadata.lastModificationDate.getOrElse(
+ throw AssertionException(s"${metadata.ontologyIri} has no last modification date"))
+ assert(newAnythingLastModDate.isAfter(anythingLastModDate))
+ anythingLastModDate = newAnythingLastModDate
+ }
+ }
+
"not create a property called anything:Thing, because that IRI is already used for a class" in {
val propertyIri = AnythingOntologyIri.makeEntityIri("Thing")