From ced76b7199608dd92689474fa157b8284d17a4b5 Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Fri, 7 May 2021 15:54:18 +0200 Subject: [PATCH 1/5] feat(api-v2): Change GUI element and attribute of a property. --- .../ontologymessages/OntologyMessagesV2.scala | 99 +++++++++ .../responders/v2/OntologyResponderV2.scala | 200 ++++++++++++++++++ .../v2/changePropertyGuiElement.scala.txt | 141 ++++++++++++ 3 files changed, 440 insertions(+) create mode 100644 webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/changePropertyGuiElement.scala.txt 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..fddc7b185a 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,105 @@ 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 newGuiAttribute the new GUI attribute 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], + newGuiAttribute: Option[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.SalsahGui.GuiElementProp.toSmartIri).map { + predicateInfoV2: PredicateInfoV2 => + predicateInfoV2.objects.head match { + case iriLiteralV2: IriLiteralV2 => iriLiteralV2.value.toSmartIri + case other => throw BadRequestException(s"Unexpected object for salsah-gui:guiElement: $other") + } + } + + val newGuiAttribute: Option[String] = + propertyInfoContent.predicates.get(OntologyConstants.SalsahGui.GuiAttribute.toSmartIri).map { + predicateInfoV2: PredicateInfoV2 => + predicateInfoV2.objects.head match { + case stringLiteralV2: StringLiteralV2 => stringLiteralV2.value + case other => throw BadRequestException(s"Unexpected object for salsah-gui:guiAttribute: $other") + } + } + + ChangePropertyGuiElementRequest( + propertyIri = propertyInfoContent.propertyIri, + newGuiElement = newGuiElement, + newGuiAttribute = newGuiAttribute, + 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..b1ed77785f 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 @@ -3978,6 +3978,206 @@ 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, + maybeNewGuiAttribute = changePropertyGuiElementRequest.newGuiAttribute, + 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)] = changePropertyGuiElementRequest.newGuiAttribute + .map { guiAttribute: String => + OntologyConstants.SalsahGui.GuiAttribute.toSmartIri -> PredicateInfoV2( + predicateIri = OntologyConstants.SalsahGui.GuiAttribute.toSmartIri, + objects = Seq(StringLiteralV2(guiAttribute)) + ).unescape + } + + 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/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..89bb40a11c --- /dev/null +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/changePropertyGuiElement.scala.txt @@ -0,0 +1,141 @@ +@* + * 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 be deleted. + * @param maybeNewGuiAttribute the property's new salsah-gui:guiAttribute, or None if it should 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], + maybeNewGuiAttribute: Option[String], + lastModificationDate: Instant, + currentTime: Instant) + +PREFIX rdf: +PREFIX rdfs: +PREFIX owl: +PREFIX xsd: +PREFIX knora-base: +PREFIX salsah-gui: + +DELETE { + GRAPH ?ontologyNamedGraph { + ?ontology knora-base:lastModificationDate "@lastModificationDate"^^xsd:dateTime . + ?property salsah-gui:guiElement ?oldGuiElement . + ?property salsah-gui:guiAttribute ?oldGuiAttribute . + + @maybeLinkValuePropertyIri match { + case Some(linkValuePropertyIri) => { + <@linkValuePropertyIri> salsah-gui:guiElement ?oldLinkValuePropertyGuiElement . + <@linkValuePropertyIri> salsah-gui:guiAttribute ?oldLinkValuePropertyGuiAttribute . + } + + case None => {} + } + } +} INSERT { + GRAPH ?ontologyNamedGraph { + ?ontology knora-base:lastModificationDate "@currentTime"^^xsd:dateTime . + + @maybeNewGuiElement match { + case Some(newGuiElement) => { + ?property salsah-gui:guiElement <@newGuiElement> . + } + } + + @maybeNewGuiAttribute match { + case Some(newGuiAttribute) => { + ?property salsah-gui:guiAttribute <@newGuiAttribute> . + } + } + + @maybeLinkValuePropertyIri match { + case Some(linkValuePropertyIri) => { + @maybeNewGuiElement match { + case Some(newGuiElement) => { + <@linkValuePropertyIri> salsah-gui:guiElement <@newGuiElement> . + } + } + + @maybeNewGuiAttribute match { + case Some(newGuiAttribute) => { + <@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 { + BIND(IRI("@ontologyNamedGraphIri") AS ?ontologyNamedGraph) + + GRAPH ?ontologyNamedGraph { + BIND(IRI("@ontologyIri") AS ?ontology) + BIND(IRI("@propertyIri") AS ?property) + + ?ontology rdf:type owl:Ontology ; + knora-base:lastModificationDate "@lastModificationDate"^^xsd:dateTime . + + OPTIONAL { + ?property salsah-gui:guiElement ?oldGuiElement . + } + + OPTIONAL { + ?property salsah-gui:guiAttribute ?oldGuiAttribute . + } + + @maybeLinkValuePropertyIri match { + case Some(linkValuePropertyIri) => { + OPTIONAL { + <@linkValuePropertyIri> salsah-gui:guiElement ?oldLinkValuePropertyGuiElement . + } + + OPTIONAL { + <@linkValuePropertyIri> salsah-gui:guiAttribute ?oldLinkValuePropertyGuiAttribute . + } + } + + case None => {} + } + } +} From e3e02bdcc50cf0aaa7d67e449b2a76dfae30a8d4 Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Mon, 10 May 2021 11:39:51 +0200 Subject: [PATCH 2/5] test(api-v2): Add test. --- .../responders/v2/OntologyResponderV2.scala | 2 + .../webapi/routing/v2/OntologiesRouteV2.scala | 47 +++++++++++- .../v2/changePropertyGuiElement.scala.txt | 12 ++- .../v2/OntologyResponderV2Spec.scala | 76 ++++++++++++++++++- 4 files changed, 131 insertions(+), 6 deletions(-) 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 b1ed77785f..abfc939853 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) 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..a102cfa8fa 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" / "guiattribute") { + 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 index 89bb40a11c..e4c017dd1c 100644 --- 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 @@ -75,12 +75,16 @@ DELETE { case Some(newGuiElement) => { ?property salsah-gui:guiElement <@newGuiElement> . } + + case None => {} } @maybeNewGuiAttribute match { case Some(newGuiAttribute) => { - ?property salsah-gui:guiAttribute <@newGuiAttribute> . + ?property salsah-gui:guiAttribute "@newGuiAttribute" . } + + case None => {} } @maybeLinkValuePropertyIri match { @@ -89,12 +93,16 @@ DELETE { case Some(newGuiElement) => { <@linkValuePropertyIri> salsah-gui:guiElement <@newGuiElement> . } + + case None => {} } @maybeNewGuiAttribute match { case Some(newGuiAttribute) => { - <@linkValuePropertyIri> salsah-gui:guiAttribute <@newGuiAttribute> . + <@linkValuePropertyIri> salsah-gui:guiAttribute "@newGuiAttribute" . } + + case None => {} } } 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..0c330ec656 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), + newGuiAttribute = Some("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, + newGuiAttribute = None, + 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") From a98bd3512e3919ef2bd5571c769c2462ef2aab12 Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Mon, 10 May 2021 14:54:45 +0200 Subject: [PATCH 3/5] test(api-v2): Add e2e test. --- .../ontologymessages/OntologyMessagesV2.scala | 32 +++---- .../responders/v2/OntologyResponderV2.scala | 14 ++-- .../webapi/routing/v2/OntologiesRouteV2.scala | 2 +- .../v2/changePropertyGuiElement.scala.txt | 83 +++++++++---------- .../webapi/e2e/v2/OntologyV2R2RSpec.scala | 78 +++++++++++++++++ .../v2/OntologyResponderV2Spec.scala | 4 +- 6 files changed, 145 insertions(+), 68 deletions(-) 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 fddc7b185a..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 @@ -753,7 +753,7 @@ sealed trait ChangeLabelsOrCommentsRequest { * * @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 newGuiAttribute the new GUI attribute 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. @@ -761,7 +761,7 @@ sealed trait ChangeLabelsOrCommentsRequest { */ case class ChangePropertyGuiElementRequest(propertyIri: SmartIri, newGuiElement: Option[SmartIri], - newGuiAttribute: Option[String], + newGuiAttributes: Set[String], lastModificationDate: Instant, apiRequestID: UUID, featureFactoryConfig: FeatureFactoryConfig, @@ -818,27 +818,31 @@ object ChangePropertyGuiElementRequest extends KnoraJsonLDRequestReaderV2[Change val lastModificationDate = propertyUpdateInfo.lastModificationDate val newGuiElement: Option[SmartIri] = - propertyInfoContent.predicates.get(OntologyConstants.SalsahGui.GuiElementProp.toSmartIri).map { - predicateInfoV2: PredicateInfoV2 => + propertyInfoContent.predicates + .get(OntologyConstants.SalsahGuiApiV2WithValueObjects.GuiElementProp.toSmartIri) + .map { predicateInfoV2: PredicateInfoV2 => predicateInfoV2.objects.head match { - case iriLiteralV2: IriLiteralV2 => iriLiteralV2.value.toSmartIri - case other => throw BadRequestException(s"Unexpected object for salsah-gui:guiElement: $other") + case iriLiteralV2: SmartIriLiteralV2 => iriLiteralV2.value + case other => + throw BadRequestException(s"Unexpected object for salsah-gui:guiElement: $other") } - } + } - val newGuiAttribute: Option[String] = - propertyInfoContent.predicates.get(OntologyConstants.SalsahGui.GuiAttribute.toSmartIri).map { - predicateInfoV2: PredicateInfoV2 => - predicateInfoV2.objects.head match { + 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, - newGuiAttribute = newGuiAttribute, + newGuiAttributes = newGuiAttributes, lastModificationDate = lastModificationDate, apiRequestID = apiRequestID, featureFactoryConfig = featureFactoryConfig, 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 abfc939853..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 @@ -4029,7 +4029,7 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon propertyIri = internalPropertyIri, maybeLinkValuePropertyIri = maybeCurrentLinkValueReadPropertyInfo.map(_.entityInfoContent.propertyIri), maybeNewGuiElement = changePropertyGuiElementRequest.newGuiElement, - maybeNewGuiAttribute = changePropertyGuiElementRequest.newGuiAttribute, + newGuiAttributes = changePropertyGuiElementRequest.newGuiAttributes, lastModificationDate = changePropertyGuiElementRequest.lastModificationDate, currentTime = currentTime ) @@ -4061,13 +4061,15 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon ) } - maybeUnescapedNewGuiAttributePredicate: Option[(SmartIri, PredicateInfoV2)] = changePropertyGuiElementRequest.newGuiAttribute - .map { guiAttribute: String => + maybeUnescapedNewGuiAttributePredicate: Option[(SmartIri, PredicateInfoV2)] = if (changePropertyGuiElementRequest.newGuiAttributes.nonEmpty) { + Some( OntologyConstants.SalsahGui.GuiAttribute.toSmartIri -> PredicateInfoV2( predicateIri = OntologyConstants.SalsahGui.GuiAttribute.toSmartIri, - objects = Seq(StringLiteralV2(guiAttribute)) - ).unescape - } + objects = changePropertyGuiElementRequest.newGuiAttributes.map(StringLiteralV2(_)).toSeq + )) + } else { + None + } unescapedNewPropertyDef: PropertyInfoContentV2 = currentReadPropertyInfo.entityInfoContent.copy( predicates = currentReadPropertyInfo.entityInfoContent.predicates - 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 a102cfa8fa..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 @@ -736,7 +736,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } private def updatePropertyGuiElement(featureFactoryConfig: FeatureFactoryConfig): Route = - path(OntologiesBasePath / "properties" / "guiattribute") { + path(OntologiesBasePath / "properties" / "guielement") { put { // Change the salsah-gui:guiElement and/or salsah-gui:guiAttribute of a property. entity(as[String]) { jsonRequest => 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 index e4c017dd1c..4277c74a9b 100644 --- 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 @@ -30,8 +30,8 @@ * @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 be deleted. - * @param maybeNewGuiAttribute the property's new salsah-gui:guiAttribute, or None if it should be deleted. + * @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. *@ @@ -41,7 +41,7 @@ propertyIri: SmartIri, maybeLinkValuePropertyIri: Option[SmartIri], maybeNewGuiElement: Option[SmartIri], - maybeNewGuiAttribute: Option[String], + newGuiAttributes: Set[String], lastModificationDate: Instant, currentTime: Instant) @@ -64,48 +64,6 @@ DELETE { <@linkValuePropertyIri> salsah-gui:guiAttribute ?oldLinkValuePropertyGuiAttribute . } - case None => {} - } - } -} INSERT { - GRAPH ?ontologyNamedGraph { - ?ontology knora-base:lastModificationDate "@currentTime"^^xsd:dateTime . - - @maybeNewGuiElement match { - case Some(newGuiElement) => { - ?property salsah-gui:guiElement <@newGuiElement> . - } - - case None => {} - } - - @maybeNewGuiAttribute match { - case Some(newGuiAttribute) => { - ?property salsah-gui:guiAttribute "@newGuiAttribute" . - } - - case None => {} - } - - @maybeLinkValuePropertyIri match { - case Some(linkValuePropertyIri) => { - @maybeNewGuiElement match { - case Some(newGuiElement) => { - <@linkValuePropertyIri> salsah-gui:guiElement <@newGuiElement> . - } - - case None => {} - } - - @maybeNewGuiAttribute match { - case Some(newGuiAttribute) => { - <@linkValuePropertyIri> salsah-gui:guiAttribute "@newGuiAttribute" . - } - - case None => {} - } - } - case None => {} } } @@ -143,6 +101,41 @@ WHERE { } } + case None => {} + } + } +} ; +INSERT DATA { + GRAPH <@ontologyNamedGraphIri> { + <@ontologyIri> knora-base:lastModificationDate "@currentTime"^^xsd:dateTime . + + @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 => {} } } 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..349e727c06 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,84 @@ 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] + println(responseStr) + 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 + } + } + "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 0c330ec656..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 @@ -3258,7 +3258,7 @@ class OntologyResponderV2Spec extends CoreSpec() with ImplicitSender { responderManager ! ChangePropertyGuiElementRequest( propertyIri = propertyIri, newGuiElement = Some("http://api.knora.org/ontology/salsah-gui/v2#SimpleText".toSmartIri), - newGuiAttribute = Some("size=80"), + newGuiAttributes = Set("size=80"), lastModificationDate = anythingLastModDate, apiRequestID = UUID.randomUUID, featureFactoryConfig = defaultFeatureFactoryConfig, @@ -3299,7 +3299,7 @@ class OntologyResponderV2Spec extends CoreSpec() with ImplicitSender { responderManager ! ChangePropertyGuiElementRequest( propertyIri = propertyIri, newGuiElement = None, - newGuiAttribute = None, + newGuiAttributes = Set.empty, lastModificationDate = anythingLastModDate, apiRequestID = UUID.randomUUID, featureFactoryConfig = defaultFeatureFactoryConfig, From 65cc2409df4308a3bcf191f98401fc72cf85036d Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Tue, 11 May 2021 11:03:53 +0200 Subject: [PATCH 4/5] test(api-v2): Add test. --- webapi/scripts/expected-client-test-data.txt | 2 + .../v2/changePropertyGuiElement.scala.txt | 56 +++++++++++----- .../webapi/e2e/v2/OntologyV2R2RSpec.scala | 64 ++++++++++++++++++- 3 files changed, 104 insertions(+), 18 deletions(-) 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/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 index 4277c74a9b..5b3870a0e2 100644 --- 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 @@ -53,10 +53,9 @@ PREFIX knora-base: PREFIX salsah-gui: DELETE { - GRAPH ?ontologyNamedGraph { - ?ontology knora-base:lastModificationDate "@lastModificationDate"^^xsd:dateTime . - ?property salsah-gui:guiElement ?oldGuiElement . - ?property salsah-gui:guiAttribute ?oldGuiAttribute . + GRAPH <@ontologyNamedGraphIri> { + <@propertyIri> salsah-gui:guiElement ?oldGuiElement . + <@propertyIri> salsah-gui:guiAttribute ?oldGuiAttribute . @maybeLinkValuePropertyIri match { case Some(linkValuePropertyIri) => { @@ -73,21 +72,16 @@ DELETE { USING } WHERE { - BIND(IRI("@ontologyNamedGraphIri") AS ?ontologyNamedGraph) - - GRAPH ?ontologyNamedGraph { - BIND(IRI("@ontologyIri") AS ?ontology) - BIND(IRI("@propertyIri") AS ?property) - - ?ontology rdf:type owl:Ontology ; + GRAPH <@ontologyNamedGraphIri> { + <@ontologyIri> rdf:type owl:Ontology ; knora-base:lastModificationDate "@lastModificationDate"^^xsd:dateTime . OPTIONAL { - ?property salsah-gui:guiElement ?oldGuiElement . + <@propertyIri> salsah-gui:guiElement ?oldGuiElement . } OPTIONAL { - ?property salsah-gui:guiAttribute ?oldGuiAttribute . + <@propertyIri> salsah-gui:guiAttribute ?oldGuiAttribute . } @maybeLinkValuePropertyIri match { @@ -104,11 +98,9 @@ WHERE { case None => {} } } -} ; -INSERT DATA { +}; +INSERT { GRAPH <@ontologyNamedGraphIri> { - <@ontologyIri> knora-base:lastModificationDate "@currentTime"^^xsd:dateTime . - @maybeNewGuiElement match { case Some(newGuiElement) => { <@propertyIri> salsah-gui:guiElement <@newGuiElement> . @@ -140,3 +132,33 @@ INSERT DATA { } } } +@* 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 349e727c06..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 @@ -1018,7 +1018,6 @@ class OntologyV2R2RSpec extends R2RSpec { Put("/v2/ontologies/properties/guielement", HttpEntity(RdfMediaTypes.`application/ld+json`, params)) ~> addCredentials( BasicHttpCredentials(anythingUsername, password)) ~> ontologiesPath ~> check { val responseStr = responseAs[String] - println(responseStr) assert(status == StatusCodes.OK, responseStr) val responseJsonDoc = responseToJsonLDDocument(response) @@ -1051,6 +1050,69 @@ class OntologyV2R2RSpec extends R2RSpec { } } + "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 = From a98d8c07836c1ad659efe647eed7665ccabb1f6c Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Wed, 12 May 2021 11:19:32 +0200 Subject: [PATCH 5/5] docs(api-v2): Document route for changing GUI element and attribute. --- docs/03-apis/api-v2/ontology-information.md | 37 +++++++++++++++++++++ 1 file changed, 37 insertions(+) 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