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")