From ab6fe3603f0eff3e0d984440a7f2345f6ae9d15d Mon Sep 17 00:00:00 2001 From: Sepideh Alassi Date: Wed, 9 Sep 2020 16:58:00 +0200 Subject: [PATCH] Add comments to ontology metadata (#1703) * feature (ontologyComment) allow comment to be given for ontology * feature (ontologyComment) responder adds the comment to the ontology metadata * feature (ontologyComment) return ontology comment when loading the ontology metadata + test * test (ontologyComment) e2e test * feature (ontologyComment) test data * docs (ontologyComment) update documentation --- docs/03-apis/api-v2/ontology-information.md | 7 ++++ .../ontologymessages/OntologyMessagesV2.scala | 17 +++++++- .../responders/v2/OntologyResponderV2.scala | 8 ++++ .../webapi/routing/v2/OntologiesRouteV2.scala | 36 +++++++++++------ .../sharedtestdata/SharedTestDataADM.scala | 39 ++++++++++++++++++- .../sparql/v2/createOntology.scala.txt | 4 ++ .../webapi/e2e/v2/OntologyV2R2RSpec.scala | 17 ++++++++ .../v2/OntologyResponderV2Spec.scala | 18 +++++++++ 8 files changed, 133 insertions(+), 13 deletions(-) diff --git a/docs/03-apis/api-v2/ontology-information.md b/docs/03-apis/api-v2/ontology-information.md index b74507c685..b03836cdae 100644 --- a/docs/03-apis/api-v2/ontology-information.md +++ b/docs/03-apis/api-v2/ontology-information.md @@ -951,6 +951,13 @@ HTTP POST to http://host/v2/ontologies The ontology name must follow the rules given in [Knora IRIs](knora-iris.md). +The ontology metadata can have an optional comment given in the request +body as: + +``` +"rdfs:comment": "some comment", +``` + If the ontology is to be shared by multiple projects, it must be created in the default shared ontologies project, `http://www.knora.org/ontology/knora-base#DefaultSharedOntologiesProject`, 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 d85689526f..98ab5c37b4 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 @@ -61,6 +61,9 @@ case class LoadOntologiesRequestV2(requestingUser: UserADM) extends OntologiesRe * * @param ontologyName the name of the ontology to be created. * @param projectIri the IRI of the project that the ontology will belong to. + * @param isShared the flag that shows if an ontology is a shared one. + * @param label the label of the ontology. + * @param comment the optional comment that described the ontology to be created. * @param apiRequestID the ID of the API request. * @param requestingUser the user making the request. */ @@ -68,6 +71,7 @@ case class CreateOntologyRequestV2(ontologyName: String, projectIri: SmartIri, isShared: Boolean = false, label: String, + comment: Option[String] = None, apiRequestID: UUID, requestingUser: UserADM) extends OntologiesResponderRequestV2 @@ -111,6 +115,7 @@ object CreateOntologyRequestV2 extends KnoraJsonLDRequestReaderV2[CreateOntology val ontologyName: String = jsonLDDocument.requireStringWithValidation(OntologyConstants.KnoraApiV2Complex.OntologyName, stringFormatter.validateProjectSpecificOntologyName) val label: String = jsonLDDocument.requireStringWithValidation(OntologyConstants.Rdfs.Label, stringFormatter.toSparqlEncodedString) + val comment: Option[String] = jsonLDDocument.maybeStringWithValidation(OntologyConstants.Rdfs.Comment, stringFormatter.toSparqlEncodedString) val projectIri: SmartIri = jsonLDDocument.requireIriInObject(OntologyConstants.KnoraApiV2Complex.AttachedToProject, stringFormatter.toSmartIriWithErr) val isShared: Boolean = jsonLDDocument.maybeBoolean(OntologyConstants.KnoraApiV2Complex.IsShared).exists(identity) @@ -119,6 +124,7 @@ object CreateOntologyRequestV2 extends KnoraJsonLDRequestReaderV2[CreateOntology projectIri = projectIri, isShared = isShared, label = label, + comment = comment, apiRequestID = apiRequestID, requestingUser = requestingUser ) @@ -1358,6 +1364,8 @@ object InputOntologyV2 { val ontologyLabel: Option[String] = ontologyObj.maybeStringWithValidation(OntologyConstants.Rdfs.Label, stringFormatter.toSparqlEncodedString) + val ontologyComment: Option[String] = ontologyObj.maybeStringWithValidation(OntologyConstants.Rdfs.Comment, stringFormatter.toSparqlEncodedString) + val lastModificationDate: Option[Instant] = ontologyObj.maybeDatatypeValueInObject( key = OntologyConstants.KnoraApiV2Complex.LastModificationDate, expectedDatatype = OntologyConstants.Xsd.DateTimeStamp.toSmartIri, @@ -1368,6 +1376,7 @@ object InputOntologyV2 { ontologyIri = externalOntologyIri, projectIri = projectIri, label = ontologyLabel, + comment = ontologyComment, lastModificationDate = lastModificationDate ) @@ -3008,12 +3017,14 @@ case class SubClassInfoV2(id: SmartIri, label: String) * @param ontologyIri the IRI of the ontology. * @param projectIri the IRI of the project that the ontology belongs to. * @param label the label of the ontology, if any. + * @param comment the comment of the ontology, if any. * @param lastModificationDate the ontology's last modification date, if any. * @param ontologyVersion the version string attached to the ontology, if any. */ case class OntologyMetadataV2(ontologyIri: SmartIri, projectIri: Option[SmartIri] = None, label: Option[String] = None, + comment: Option[String] = None, lastModificationDate: Option[Instant] = None, ontologyVersion: Option[String] = None) extends KnoraContentV2[OntologyMetadataV2] { implicit private val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -3071,6 +3082,10 @@ case class OntologyMetadataV2(ontologyIri: SmartIri, labelStr => OntologyConstants.Rdfs.Label -> JsonLDString(labelStr) } + val commentStatement: Option[(IRI, JsonLDString)] = comment.map { + commentStr => OntologyConstants.Rdfs.Comment -> JsonLDString(commentStr) + } + val lastModDateStatement: Option[(IRI, JsonLDObject)] = if (targetSchema == ApiV2Complex) { lastModificationDate.map { lastModDate => @@ -3085,6 +3100,6 @@ case class OntologyMetadataV2(ontologyIri: SmartIri, Map(JsonLDConstants.ID -> JsonLDString(ontologyIri.toString), JsonLDConstants.TYPE -> JsonLDString(OntologyConstants.Owl.Ontology) - ) ++ projectIriStatement ++ labelStatement ++ lastModDateStatement ++ isSharedStatement ++ isBuiltInStatement + ) ++ projectIriStatement ++ labelStatement ++ commentStatement ++ lastModDateStatement ++ isSharedStatement ++ isBuiltInStatement } } 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 741f9dba12..e9edee7095 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 @@ -1638,6 +1638,7 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon val projectIris: Seq[String] = statementMap.getOrElse(OntologyConstants.KnoraBase.AttachedToProject, throw InconsistentTriplestoreDataException(s"Ontology $internalOntologyIri has no knora-base:attachedToProject")) val labels: Seq[String] = statementMap.getOrElse(OntologyConstants.Rdfs.Label, Seq.empty[String]) + val comments: Seq[String] = statementMap.getOrElse(OntologyConstants.Rdfs.Comment, Seq.empty[String]) val lastModDates: Seq[String] = statementMap.getOrElse(OntologyConstants.KnoraBase.LastModificationDate, Seq.empty[String]) val projectIri = if (projectIris.size > 1) { @@ -1664,6 +1665,10 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon labels.head } + val comment: Option[String] = if (comments.size > 1) { + throw InconsistentTriplestoreDataException(s"Ontology $internalOntologyIri has more than one rdfs:comment") + } else comments.headOption + val lastModificationDate: Option[Instant] = if (lastModDates.size > 1) { throw InconsistentTriplestoreDataException(s"Ontology $internalOntologyIri has more than one ${OntologyConstants.KnoraBase.LastModificationDate}") } else if (lastModDates.isEmpty) { @@ -1677,6 +1682,7 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon ontologyIri = internalOntologyIri, projectIri = Some(projectIri), label = Some(label), + comment = comment, lastModificationDate = lastModificationDate )) @@ -1725,6 +1731,7 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon projectIri = createOntologyRequest.projectIri, isShared = createOntologyRequest.isShared, ontologyLabel = createOntologyRequest.label, + ontologyComment = createOntologyRequest.comment, currentTime = currentTime ).toString @@ -1736,6 +1743,7 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon ontologyIri = internalOntologyIri, projectIri = Some(createOntologyRequest.projectIri), label = Some(createOntologyRequest.label), + comment = createOntologyRequest.comment, lastModificationDate = Some(currentTime) ).unescape 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 fa2c3618a3..f89c926b08 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 @@ -906,21 +906,35 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def createOntologyTestRequest: Future[TestDataFileContent] = { + private def createOntologyTestRequest: Future[Set[TestDataFileContent]] = { FastFuture.successful( - TestDataFileContent( - filePath = TestDataFilePath.makeJsonPath("create-empty-foo-ontology-request"), - text = SharedTestDataADM.createOntology(SharedTestDataADM.IMAGES_PROJECT_IRI, "The foo ontology") + Set( + TestDataFileContent( + filePath = TestDataFilePath.makeJsonPath("create-empty-foo-ontology-request"), + text = SharedTestDataADM.createOntology(SharedTestDataADM.IMAGES_PROJECT_IRI, "The foo ontology") + ), + TestDataFileContent( + filePath = TestDataFilePath.makeJsonPath("create-ontology-with-comment-request"), + text = SharedTestDataADM.createOntologyWithComment(SharedTestDataADM.IMAGES_PROJECT_IRI, + "The bar ontology", "some comment") + ) ) ) } - private def createOntologyTestResponse: Future[TestDataFileContent] = { + private def createOntologyTestResponse: Future[Set[TestDataFileContent]] = { FastFuture.successful( - TestDataFileContent( - filePath = TestDataFilePath.makeJsonPath("create-empty-foo-ontology-response"), - text = SharedTestDataADM.createOntologyResponse + Set( + TestDataFileContent( + filePath = TestDataFilePath.makeJsonPath("create-empty-foo-ontology-response"), + text = SharedTestDataADM.createFooOntologyResponse + ), + TestDataFileContent( + filePath = TestDataFilePath.makeJsonPath("create-ontology-with-comment-response"), + text = SharedTestDataADM.createOntologyWithCommentResponse + ) ) + ) } @@ -979,8 +993,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) projectOntologiesResponses: Set[TestDataFileContent] <- getOntologyMetadataForProjectsTestResponses ontologyClassResponses: Set[TestDataFileContent] <- getClassesTestResponses ontologyPropertyResponses: Set[TestDataFileContent] <- getPropertiesTestResponses - createOntologyRequest: TestDataFileContent <- createOntologyTestRequest - createOntologyResponse: TestDataFileContent <- createOntologyTestResponse + createOntologyRequest: Set[TestDataFileContent] <- createOntologyTestRequest + createOntologyResponse: Set[TestDataFileContent] <- createOntologyTestResponse updateOntologyMetadataRequest: TestDataFileContent <- updateOntologyMetadataTestRequest createClassRequest: Set[TestDataFileContent] <- createClassTestRequest createClassResponse: TestDataFileContent <- createClassTestResponse @@ -991,7 +1005,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) updatePropertyRequest: Set[TestDataFileContent] <- updatePropertyTestRequest deleteOntologyResponse: TestDataFileContent <- deleteOntologyTestResponse } yield ontologyResponses + ontologyMetadataResponses ++ projectOntologiesResponses ++ ontologyClassResponses ++ - ontologyPropertyResponses + createOntologyRequest + createOntologyResponse + updateOntologyMetadataRequest ++ + ontologyPropertyResponses ++ createOntologyRequest ++ createOntologyResponse + updateOntologyMetadataRequest ++ createClassRequest + createClassResponse + addCardinalitiesRequest + createPropertyRequest ++ updateClassRequest ++ replaceCardinalitiesRequest ++ updatePropertyRequest + deleteOntologyResponse } diff --git a/webapi/src/main/scala/org/knora/webapi/sharedtestdata/SharedTestDataADM.scala b/webapi/src/main/scala/org/knora/webapi/sharedtestdata/SharedTestDataADM.scala index 86eb930dc5..8325826f43 100644 --- a/webapi/src/main/scala/org/knora/webapi/sharedtestdata/SharedTestDataADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/sharedtestdata/SharedTestDataADM.scala @@ -2314,7 +2314,23 @@ object SharedTestDataADM { |}""".stripMargin } - val createOntologyResponse: String = + def createOntologyWithComment(projectIri: IRI, label: String, comment: String): String = { + s""" + |{ + | "knora-api:ontologyName": "bar", + | "knora-api:attachedToProject": { + | "@id": "$projectIri" + | }, + | "rdfs:label": "$label", + | "rdfs:comment": "$comment", + | "@context": { + | "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + | "knora-api": "http://api.knora.org/ontology/knora-api/v2#" + | } + |}""".stripMargin + } + + val createFooOntologyResponse: String = """{ | "@id" : "http://0.0.0.0:3333/ontology/00FF/foo/v2", | "@type" : "owl:Ontology", @@ -2334,6 +2350,27 @@ object SharedTestDataADM { | } |}""".stripMargin + val createOntologyWithCommentResponse: String = + """{ + | "@id": "http://0.0.0.0:3333/ontology/00FF/bar/v2", + | "@type": "owl:Ontology", + | "knora-api:attachedToProject": { + | "@id": "http://rdfh.ch/projects/00FF" + | }, + | "knora-api:lastModificationDate": { + | "@type": "xsd:dateTimeStamp", + | "@value": "2020-09-09T09:37:19.137090Z" + | }, + | "rdfs:comment": "some comment", + | "rdfs:label": "The bar ontology", + | "@context": { + | "knora-api": "http://api.knora.org/ontology/knora-api/v2#", + | "xsd": "http://www.w3.org/2001/XMLSchema#", + | "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + | "owl": "http://www.w3.org/2002/07/owl#" + | } + |}""".stripMargin + def changeOntologyMetadata(ontologyIri: IRI, newLabel: String, modificationDate: Instant): String = { s""" |{ diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/createOntology.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/createOntology.scala.txt index f2d143cc27..ee368dd6a0 100644 --- a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/createOntology.scala.txt +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/createOntology.scala.txt @@ -35,6 +35,7 @@ projectIri: SmartIri, isShared: Boolean, ontologyLabel: String, + ontologyComment: Option[String], currentTime: Instant) PREFIX rdf: @@ -49,6 +50,9 @@ INSERT { knora-base:attachedToProject ?project ; knora-base:isShared @isShared ; rdfs:label """@ontologyLabel"""^^xsd:string ; + @if(ontologyComment.nonEmpty) { + rdfs:comment """@ontologyComment.get"""^^xsd:string ; + } knora-base:lastModificationDate "@currentTime"^^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 b48d88c34e..497483bbd7 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 @@ -255,6 +255,23 @@ class OntologyV2R2RSpec extends R2RSpec { } } + "create an empty ontology called 'bar' with a comment" in { + val label = "The bar ontology" + var comment = "some comment" + + val params = SharedTestDataADM.createOntologyWithComment(imagesProjectIri, label, comment) + + + Post("/v2/ontologies", HttpEntity(RdfMediaTypes.`application/ld+json`, params)) ~> addCredentials(BasicHttpCredentials(imagesUsername, password)) ~> ontologiesPath ~> check { + assert(status == StatusCodes.OK, response.toString) + val responseJsonDoc = responseToJsonLDDocument(response) + val metadata = responseJsonDoc.body + val ontologyIri = metadata.value("@id").asInstanceOf[JsonLDString].value + assert(ontologyIri == "http://0.0.0.0:3333/ontology/00FF/bar/v2") + assert(metadata.value(OntologyConstants.Rdfs.Comment) == JsonLDString(comment)) + } + } + "change the metadata of 'foo'" in { val newLabel = "The modified foo ontology" 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 0eed59eea2..51e33a7a3e 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 @@ -117,6 +117,24 @@ class OntologyResponderV2Spec extends CoreSpec() with ImplicitSender { fooLastModDate = metadata.lastModificationDate.getOrElse(throw AssertionException(s"${metadata.ontologyIri} has no last modification date")) } + "create an empty ontology called 'bar' with a comment" in { + responderManager ! CreateOntologyRequestV2( + ontologyName = "bar", + projectIri = imagesProjectIri, + label = "The bar ontology", + comment = Some("some comment"), + apiRequestID = UUID.randomUUID, + requestingUser = imagesUser + ) + + val response = expectMsgType[ReadOntologyMetadataV2](timeout) + assert(response.ontologies.size == 1) + val metadata = response.ontologies.head + assert(metadata.ontologyIri.toString == "http://www.knora.org/ontology/00FF/bar") + val returnedComment: String = metadata.comment.getOrElse(throw AssertionException("The bar ontology has no comment!")) + assert(returnedComment == "some comment") + } + "change the metadata of 'foo'" in { val newLabel = "The modified foo ontology"