From d8dbb4f4aa788d0b4cc8c1b059205d6b57455629 Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Mon, 3 May 2021 08:44:22 +0200 Subject: [PATCH] feat(api-v2): Add route for changing GUI order of cardinalities (#1850) --- docs/03-apis/api-v2/ontology-information.md | 66 ++++++-- webapi/scripts/expected-client-test-data.txt | 1 + .../webapi/messages/StringFormatter.scala | 1 - .../listsmessages/ListsMessagesADM.scala | 7 + .../ontologymessages/OntologyMessagesV2.scala | 54 +++++++ .../responders/v2/OntologyResponderV2.scala | 149 ++++++++++++++++++ .../webapi/routing/v2/OntologiesRouteV2.scala | 42 +++++ .../webapi/e2e/v2/OntologyV2R2RSpec.scala | 64 ++++++++ .../v2/OntologyResponderV2Spec.scala | 64 +++++++- 9 files changed, 437 insertions(+), 11 deletions(-) diff --git a/docs/03-apis/api-v2/ontology-information.md b/docs/03-apis/api-v2/ontology-information.md index 3a89340767..05af7b328f 100644 --- a/docs/03-apis/api-v2/ontology-information.md +++ b/docs/03-apis/api-v2/ontology-information.md @@ -1066,7 +1066,7 @@ HTTP POST to http://host/v2/ontologies/classes "@value" : "ONTOLOGY_LAST_MODIFICATION_DATE" }, "@graph" : [ { - "CLASS_IRI" : { + { "@id" : "CLASS_IRI", "@type" : "owl:Class", "rdfs:label" : { @@ -1119,7 +1119,7 @@ HTTP POST to http://host/v2/ontologies/classes "@value" : "ONTOLOGY_LAST_MODIFICATION_DATE" }, "@graph" : [ { - "CLASS_IRI" : { + { "@id" : "CLASS_IRI", "@type" : "owl:Class", "rdfs:label" : { @@ -1185,7 +1185,7 @@ HTTP PUT to http://host/v2/ontologies/classes "@value" : "ONTOLOGY_LAST_MODIFICATION_DATE" }, "@graph" : [ { - "CLASS_IRI" : { + { "@id" : "CLASS_IRI", "@type" : "owl:Class", "rdfs:label" : { @@ -1224,7 +1224,7 @@ HTTP PUT to http://host/v2/ontologies/classes "@value" : "ONTOLOGY_LAST_MODIFICATION_DATE" }, "@graph" : [ { - "CLASS_IRI" : { + { "@id" : "CLASS_IRI", "@type" : "owl:Class", "rdfs:comment" : { @@ -1262,7 +1262,7 @@ HTTP POST to http://host/v2/ontologies/properties "@value" : "ONTOLOGY_LAST_MODIFICATION_DATE" }, "@graph" : [ { - "PROPERTY_IRI" : { + { "@id" : "PROPERTY_IRI", "@type" : "owl:ObjectProperty", "knora-api:subjectType" : { @@ -1351,7 +1351,7 @@ HTTP PUT to http://host/v2/ontologies/properties "@value" : "ONTOLOGY_LAST_MODIFICATION_DATE" }, "@graph" : [ { - "PROPERTY_IRI" : { + { "@id" : "PROPERTY_IRI", "@type" : "owl:ObjectProperty", "rdfs:label" : { @@ -1389,7 +1389,7 @@ HTTP PUT to http://host/v2/ontologies/properties "@value" : "ONTOLOGY_LAST_MODIFICATION_DATE" }, "@graph" : [ { - "PROPERTY_IRI" : { + { "@id" : "PROPERTY_IRI", "@type" : "owl:ObjectProperty", "rdfs:comment" : { @@ -1428,7 +1428,7 @@ HTTP POST to http://host/v2/ontologies/cardinalities "@value" : "ONTOLOGY_LAST_MODIFICATION_DATE" }, "@graph" : [ { - "CLASS_IRI" : { + { "@id" : "CLASS_IRI", "@type" : "owl:Class", "rdfs:subClassOf" : { @@ -1487,7 +1487,7 @@ HTTP PUT to http://host/v2/ontologies/cardinalities "@value" : "ONTOLOGY_LAST_MODIFICATION_DATE" }, "@graph" : [ { - "CLASS_IRI" : { + { "@id" : "CLASS_IRI", "@type" : "owl:Class", "rdfs:subClassOf" : { @@ -1521,6 +1521,54 @@ on the corresponding link value property is automatically added (see A successful response will be a JSON-LD document providing the new class definition (but not any of the other entities in the ontology). +### Changing the GUI Order of Cardinalities + +To change the GUI order of one or more cardinalities in a class: + +``` +HTTP PUT to http://host/v2/ontologies/guiorder +``` + +This can be done even if the class is used in data. + +The request body includes the cardinalities whose GUI order should be changed, +using the predicate `salsah-gui:guiOrder`, whose object is an integer: + +```jsonld +{ + "@id" : "ONTOLOGY_IRI", + "@type" : "owl:Ontology", + "knora-api:lastModificationDate" : { + "@type" : "xsd:dateTimeStamp", + "@value" : "ONTOLOGY_LAST_MODIFICATION_DATE" + }, + "@graph" : [ { + "@id" : "CLASS_IRI", + "@type" : "owl:Class", + "rdfs:subClassOf" : { + "@type": "owl:Restriction", + "OWL_CARDINALITY_PREDICATE": "OWL_CARDINALITY_VALUE", + "owl:onProperty": { + "@id" : "PROPERTY_IRI" + }, + "salsah-gui:guiOrder": "GUI_ORDER_VALUE" + } + } ], + "@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#", + } +} +``` + +Only the cardinalities whose GUI order is to be changed need to be included +in the request. The `OWL_CARDINALITY_PREDICATE` and `OWL_CARDINALITY_VALUE` +are ignored; only the `GUI_ORDER_VALUE` is changed. + ### Deleting a Property A property can be deleted only if no other ontology entity refers to it, diff --git a/webapi/scripts/expected-client-test-data.txt b/webapi/scripts/expected-client-test-data.txt index cfa31871f6..e4f7495817 100644 --- a/webapi/scripts/expected-client-test-data.txt +++ b/webapi/scripts/expected-client-test-data.txt @@ -182,6 +182,7 @@ test-data/v2/ontologies/all-ontology-metadata-response.json test-data/v2/ontologies/anything-ontology.json 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-label-request.json test-data/v2/ontologies/create-class-with-cardinalities-request.json diff --git a/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala b/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala index d4eaf634cd..badebc9429 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala @@ -3259,6 +3259,5 @@ class StringFormatter private (val maybeSettings: Option[KnoraSettingsImpl] = No stringLiterals = stringLiteralSeq.stringLiterals.map(stringLiteral => StringLiteralV2(value = fromSparqlEncodedString(stringLiteral.value), language = stringLiteral.language)) ) - } } diff --git a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADM.scala b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADM.scala index 4936fc00ac..563d480f95 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADM.scala @@ -668,10 +668,12 @@ case class ListRootNodeInfoADM(id: IRI, val unescapedLabels = stringFormatter.unescapeStringLiteralSeq(labels) val unescapedComments = stringFormatter.unescapeStringLiteralSeq(comments) + val unescapedName: Option[String] = name match { case None => None case Some(value) => Some(stringFormatter.fromSparqlEncodedString(value)) } + copy(name = unescapedName, labels = unescapedLabels, comments = unescapedComments) } @@ -733,10 +735,12 @@ case class ListChildNodeInfoADM(id: IRI, val unescapedLabels = stringFormatter.unescapeStringLiteralSeq(labels) val unescapedComments = stringFormatter.unescapeStringLiteralSeq(comments) + val unescapedName: Option[String] = name match { case None => None case Some(value) => Some(stringFormatter.fromSparqlEncodedString(value)) } + copy(name = unescapedName, labels = unescapedLabels, comments = unescapedComments) } @@ -857,10 +861,12 @@ case class ListRootNodeADM(id: IRI, val unescapedLabels = stringFormatter.unescapeStringLiteralSeq(labels) val unescapedComments = stringFormatter.unescapeStringLiteralSeq(comments) + val unescapedName: Option[String] = name match { case None => None case Some(value) => Some(stringFormatter.fromSparqlEncodedString(value)) } + copy(name = unescapedName, labels = unescapedLabels, comments = unescapedComments) } @@ -938,6 +944,7 @@ case class ListChildNodeADM(id: IRI, case None => None case Some(value) => Some(stringFormatter.fromSparqlEncodedString(value)) } + copy(name = unescapedName, labels = unescapedLabels, comments = unescapedComments) } 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 0ecb15bf45..0ce82a9c5c 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 @@ -913,6 +913,60 @@ object ChangeClassLabelsOrCommentsRequestV2 extends KnoraJsonLDRequestReaderV2[C } } +case class ChangeGuiOrderRequestV2(classInfoContent: ClassInfoContentV2, + lastModificationDate: Instant, + apiRequestID: UUID, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM) + extends OntologiesResponderRequestV2 + +object ChangeGuiOrderRequestV2 extends KnoraJsonLDRequestReaderV2[ChangeGuiOrderRequestV2] { + 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[ChangeGuiOrderRequestV2] = { + Future { + fromJsonLDSync( + jsonLDDocument = jsonLDDocument, + apiRequestID = apiRequestID, + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser + ) + } + } + + private def fromJsonLDSync(jsonLDDocument: JsonLDDocument, + apiRequestID: UUID, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM): ChangeGuiOrderRequestV2 = { + // Get the class definition and the ontology's last modification date from the JSON-LD. + + val inputOntologiesV2 = InputOntologyV2.fromJsonLD(jsonLDDocument) + val classUpdateInfo = OntologyUpdateHelper.getClassDef(inputOntologiesV2) + val classInfoContent = classUpdateInfo.classInfoContent + val lastModificationDate = classUpdateInfo.lastModificationDate + + // The request must provide cardinalities. + + if (classInfoContent.directCardinalities.isEmpty) { + throw BadRequestException("No cardinalities specified") + } + + ChangeGuiOrderRequestV2( + classInfoContent = classInfoContent, + lastModificationDate = lastModificationDate, + apiRequestID = apiRequestID, + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser + ) + } +} + /** * Requests a change in the metadata of an ontology. A successful response will be a [[ReadOntologyMetadataV2]]. * 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 0b52f815d4..38b942b45f 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 @@ -145,6 +145,7 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon addCardinalitiesToClass(addCardinalitiesToClassRequest) case changeCardinalitiesRequest: ChangeCardinalitiesRequestV2 => changeClassCardinalities(changeCardinalitiesRequest) + case changeGuiOrderRequest: ChangeGuiOrderRequestV2 => changeGuiOrder(changeGuiOrderRequest) case deleteClassRequest: DeleteClassRequestV2 => deleteClass(deleteClassRequest) case createPropertyRequest: CreatePropertyRequestV2 => createProperty(createPropertyRequest) case changePropertyLabelsOrCommentsRequest: ChangePropertyLabelsOrCommentsRequestV2 => @@ -2941,6 +2942,154 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon } yield taskResult } + private def changeGuiOrder(changeGuiOrderRequest: ChangeGuiOrderRequestV2): Future[ReadOntologyV2] = { + def makeTaskFuture(internalClassIri: SmartIri, internalOntologyIri: SmartIri): Future[ReadOntologyV2] = { + for { + cacheData <- getCacheData + internalClassDef: ClassInfoContentV2 = changeGuiOrderRequest.classInfoContent.toOntologySchema(InternalSchema) + + // Check that the ontology exists and has not been updated by another user since the client last read it. + _ <- checkOntologyLastModificationDateBeforeUpdate( + internalOntologyIri = internalOntologyIri, + expectedLastModificationDate = changeGuiOrderRequest.lastModificationDate, + featureFactoryConfig = changeGuiOrderRequest.featureFactoryConfig + ) + + // Check that the class's rdf:type is owl:Class. + + rdfType: SmartIri = internalClassDef.requireIriObject(OntologyConstants.Rdf.Type.toSmartIri, + throw BadRequestException(s"No rdf:type specified")) + + _ = if (rdfType != OntologyConstants.Owl.Class.toSmartIri) { + throw BadRequestException(s"Invalid rdf:type for property: $rdfType") + } + + // Check that the class exists. + + ontology = cacheData.ontologies(internalOntologyIri) + + currentReadClassInfo: ReadClassInfoV2 = ontology.classes + .getOrElse( + internalClassIri, + throw BadRequestException(s"Class ${changeGuiOrderRequest.classInfoContent.classIri} does not exist")) + + // Check that the properties submitted already have cardinalities. + + wrongProperties: Set[SmartIri] = internalClassDef.directCardinalities.keySet -- currentReadClassInfo.entityInfoContent.directCardinalities.keySet + + _ = if (wrongProperties.nonEmpty) { + throw BadRequestException( + s"One or more submitted properties do not have cardinalities in class ${changeGuiOrderRequest.classInfoContent.classIri}: ${wrongProperties + .map(_.toOntologySchema(ApiV2Complex)) + .mkString(", ")}") + } + + // Make an updated class definition. + + newReadClassInfo = currentReadClassInfo.copy( + entityInfoContent = currentReadClassInfo.entityInfoContent.copy( + directCardinalities = currentReadClassInfo.entityInfoContent.directCardinalities.map { + case (propertyIri: SmartIri, cardinalityWithCurrentGuiOrder: KnoraCardinalityInfo) => + internalClassDef.directCardinalities.get(propertyIri) match { + case Some(cardinalityWithNewGuiOrder) => + propertyIri -> cardinalityWithCurrentGuiOrder.copy(guiOrder = cardinalityWithNewGuiOrder.guiOrder) + + case None => propertyIri -> cardinalityWithCurrentGuiOrder + } + } + ) + ) + + // Replace the cardinalities in the class definition in the triplestore. + + currentTime: Instant = Instant.now + + updateSparql = org.knora.webapi.messages.twirl.queries.sparql.v2.txt + .replaceClassCardinalities( + triplestore = settings.triplestoreType, + ontologyNamedGraphIri = internalOntologyIri, + ontologyIri = internalOntologyIri, + classIri = internalClassIri, + newCardinalities = newReadClassInfo.entityInfoContent.directCardinalities, + lastModificationDate = changeGuiOrderRequest.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 = changeGuiOrderRequest.featureFactoryConfig + ) + + // Check that the data that was saved corresponds to the data that was submitted. + + loadedClassDef: ClassInfoContentV2 <- loadClassDefinition( + classIri = internalClassIri, + featureFactoryConfig = changeGuiOrderRequest.featureFactoryConfig + ) + + _ = if (loadedClassDef != newReadClassInfo.entityInfoContent) { + throw InconsistentRepositoryDataException( + s"Attempted to save class definition ${newReadClassInfo.entityInfoContent}, but $loadedClassDef was saved") + } + + // Update the cache. + + updatedOntology = ontology.copy( + ontologyMetadata = ontology.ontologyMetadata.copy( + lastModificationDate = Some(currentTime) + ), + classes = ontology.classes + (internalClassIri -> newReadClassInfo) + ) + + _ = storeCacheData( + cacheData.copy( + ontologies = cacheData.ontologies + (internalOntologyIri -> updatedOntology) + )) + + // Read the data back from the cache. + + response <- getClassDefinitionsFromOntologyV2( + classIris = Set(internalClassIri), + allLanguages = true, + requestingUser = changeGuiOrderRequest.requestingUser + ) + } yield response + } + + for { + requestingUser <- FastFuture.successful(changeGuiOrderRequest.requestingUser) + + externalClassIri = changeGuiOrderRequest.classInfoContent.classIri + externalOntologyIri = externalClassIri.getOntologyFromEntity + + _ <- checkOntologyAndEntityIrisForUpdate( + externalOntologyIri = externalOntologyIri, + externalEntityIri = externalClassIri, + requestingUser = requestingUser + ) + + internalClassIri = externalClassIri.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 = changeGuiOrderRequest.apiRequestID, + iri = ONTOLOGY_CACHE_LOCK_IRI, + task = () => + makeTaskFuture( + internalClassIri = internalClassIri, + internalOntologyIri = internalOntologyIri + ) + ) + } yield taskResult + } + /** * Replaces a class's cardinalities with new ones. * 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 270cc165e3..20e942722c 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 @@ -61,6 +61,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) updateClass(featureFactoryConfig) ~ addCardinalities(featureFactoryConfig) ~ replaceCardinalities(featureFactoryConfig) ~ + changeGuiOrder(featureFactoryConfig) ~ getClasses(featureFactoryConfig) ~ deleteClass(featureFactoryConfig) ~ deleteOntologyComment(featureFactoryConfig) ~ @@ -440,6 +441,47 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } + private def changeGuiOrder(featureFactoryConfig: FeatureFactoryConfig): Route = + path(OntologiesBasePath / "guiorder") { + put { + // Change a class's cardinalities. + entity(as[String]) { jsonRequest => requestContext => + { + val requestMessageFuture: Future[ChangeGuiOrderRequestV2] = for { + requestingUser <- getUserADM( + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig + ) + + requestDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonRequest) + + requestMessage: ChangeGuiOrderRequestV2 <- ChangeGuiOrderRequestV2.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 getClasses(featureFactoryConfig: FeatureFactoryConfig): Route = path(OntologiesBasePath / "classes" / Segments) { externalResourceClassIris: List[IRI] => get { requestContext => 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 8b126cbb44..5983058e42 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 @@ -1631,6 +1631,70 @@ class OntologyV2R2RSpec extends R2RSpec { } } + "change the GUI order of the cardinality on anything:hasNothingness in the class anything:Nothing" in { + val params = + s"""{ + | "@id" : "${SharedOntologyTestDataADM.ANYTHING_ONTOLOGY_IRI_LocalHost}", + | "@type" : "owl:Ontology", + | "knora-api:lastModificationDate" : { + | "@type" : "xsd:dateTimeStamp", + | "@value" : "$anythingLastModDate" + | }, + | "@graph" : [ { + | "@id" : "anything:Nothing", + | "@type" : "owl:Class", + | "rdfs:subClassOf" : { + | "@type": "owl:Restriction", + | "owl:maxCardinality": 1, + | "owl:onProperty": { + | "@id" : "anything:hasNothingness" + | }, + | "salsah-gui:guiOrder": 2 + | } + | } ], + | "@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-gui-order-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/guiorder", HttpEntity(RdfMediaTypes.`application/ld+json`, params)) ~> addCredentials( + BasicHttpCredentials(anythingUsername, password)) ~> ontologiesPath ~> check { + assert(status == StatusCodes.OK, response.toString) + 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.classes.head._2.directCardinalities should ===( + paramsAsInput.classes.head._2.directCardinalities) + + // Check that the ontology's last modification date was updated. + val newAnythingLastModDate = responseAsInput.ontologyMetadata.lastModificationDate.get + assert(newAnythingLastModDate.isAfter(anythingLastModDate)) + anythingLastModDate = newAnythingLastModDate + } + } + "create a property anything:hasEmptiness with knora-api:subjectType anything:Nothing" in { val params = s"""{ 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 f2d84c1847..63255372e6 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 @@ -2296,7 +2296,7 @@ class OntologyResponderV2Spec extends CoreSpec() with ImplicitSender { } } - "create a class anything:CardinalityThing cardinalities on anything:hasInterestingThing and anything:hasInterestingThingValue" in { + "create a class anything:CardinalityThing with cardinalities on anything:hasInterestingThing and anything:hasInterestingThingValue" in { val classIri = AnythingOntologyIri.makeEntityIri("CardinalityThing") val classInfoContent = ClassInfoContentV2( @@ -3897,6 +3897,68 @@ class OntologyResponderV2Spec extends CoreSpec() with ImplicitSender { } + "change the GUI order of the cardinalities of the class anything:Nothing" in { + val classIri = AnythingOntologyIri.makeEntityIri("Nothing") + + val classInfoContent = ClassInfoContentV2( + classIri = classIri, + predicates = Map( + OntologyConstants.Rdf.Type.toSmartIri -> PredicateInfoV2( + predicateIri = OntologyConstants.Rdf.Type.toSmartIri, + objects = Seq(SmartIriLiteralV2(OntologyConstants.Owl.Class.toSmartIri)) + ) + ), + directCardinalities = Map( + AnythingOntologyIri.makeEntityIri("hasOtherNothing") -> KnoraCardinalityInfo(cardinality = + Cardinality.MayHaveOne, + guiOrder = Some(1)), + AnythingOntologyIri.makeEntityIri("hasNothingness") -> KnoraCardinalityInfo(cardinality = + Cardinality.MayHaveOne, + guiOrder = Some(2)), + AnythingOntologyIri.makeEntityIri("hasEmptiness") -> KnoraCardinalityInfo( + cardinality = Cardinality.MayHaveOne, + guiOrder = Some(3)) + ), + ontologySchema = ApiV2Complex + ) + + responderManager ! ChangeCardinalitiesRequestV2( + classInfoContent = classInfoContent, + lastModificationDate = anythingLastModDate, + apiRequestID = UUID.randomUUID, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = anythingAdminUser + ) + + val expectedCardinalities = Map( + AnythingOntologyIri.makeEntityIri("hasOtherNothing") -> KnoraCardinalityInfo(cardinality = + Cardinality.MayHaveOne, + guiOrder = Some(1)), + AnythingOntologyIri.makeEntityIri("hasOtherNothingValue") -> KnoraCardinalityInfo(cardinality = + Cardinality.MayHaveOne, + guiOrder = Some(1)), + AnythingOntologyIri.makeEntityIri("hasNothingness") -> KnoraCardinalityInfo( + cardinality = Cardinality.MayHaveOne, + guiOrder = Some(2)), + AnythingOntologyIri.makeEntityIri("hasEmptiness") -> KnoraCardinalityInfo(cardinality = Cardinality.MayHaveOne, + guiOrder = Some(3)) + ) + + expectMsgPF(timeout) { + case msg: ReadOntologyV2 => + val externalOntology = msg.toOntologySchema(ApiV2Complex) + assert(externalOntology.classes.size == 1) + val readClassInfo = externalOntology.classes(classIri) + readClassInfo.entityInfoContent.directCardinalities should ===(expectedCardinalities) + + 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 + } + } + "change the cardinalities of the class anything:Nothing, removing anything:hasOtherNothing and anything:hasNothingness and leaving anything:hasEmptiness" in { val classIri = AnythingOntologyIri.makeEntityIri("Nothing")