From a8b4bab882ba36809169c76ff83060032b53aa1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Fri, 3 Jan 2025 08:59:53 +0100 Subject: [PATCH] feat: Support JSON-LD ontology v2 change requests (#3451) --- .../org/knora/webapi/core/LayersTest.scala | 3 + .../org/knora/webapi/core/LayersLive.scala | 5 +- .../ontologymessages/OntologyMessagesV2.scala | 38 --------- .../org/knora/webapi/routing/ApiRoutes.scala | 6 +- .../webapi/routing/v2/OntologiesRouteV2.scala | 9 ++- .../ontology/api/OntologyApiModule.scala | 16 ++++ .../api/OntologyV2RequestParser.scala | 53 +++++++++++++ .../org/knora/webapi/TestDataFactory.scala | 30 ++++++++ .../api/OntologyV2RequestParserSpec.scala | 77 +++++++++++++++++++ 9 files changed, 193 insertions(+), 44 deletions(-) create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologyApiModule.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologyV2RequestParser.scala create mode 100644 webapi/src/test/scala/org/knora/webapi/slice/ontology/api/OntologyV2RequestParserSpec.scala diff --git a/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala b/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala index e8a3f64a58..b4b32b9cd9 100644 --- a/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala +++ b/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala @@ -50,6 +50,7 @@ import org.knora.webapi.slice.infrastructure.api.ManagementEndpoints import org.knora.webapi.slice.infrastructure.api.ManagementRoutes import org.knora.webapi.slice.lists.api.ListsApiModule import org.knora.webapi.slice.lists.domain.ListsService +import org.knora.webapi.slice.ontology.api.OntologyApiModule import org.knora.webapi.slice.ontology.api.service.RestCardinalityService import org.knora.webapi.slice.ontology.api.service.RestCardinalityServiceLive import org.knora.webapi.slice.ontology.domain.service.CardinalityService @@ -123,6 +124,7 @@ object LayersTest { ListsApiModule.Provided & ListsResponder & MessageRelay & + OntologyApiModule.Provided & OntologyCache & OntologyCacheHelpers & OntologyInferencer & @@ -193,6 +195,7 @@ object LayersTest { ManagementEndpoints.layer, ManagementRoutes.layer, MessageRelayLive.layer, + OntologyApiModule.layer, OntologyCacheLive.layer, OntologyCacheHelpersLive.layer, OntologyRepoLive.layer, diff --git a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala index b218e760d3..3b3c12b7f4 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala @@ -46,6 +46,7 @@ import org.knora.webapi.slice.infrastructure.api.ManagementEndpoints import org.knora.webapi.slice.infrastructure.api.ManagementRoutes import org.knora.webapi.slice.lists.api.ListsApiModule import org.knora.webapi.slice.lists.domain.ListsService +import org.knora.webapi.slice.ontology.api.OntologyApiModule import org.knora.webapi.slice.ontology.api.service.RestCardinalityService import org.knora.webapi.slice.ontology.api.service.RestCardinalityServiceLive import org.knora.webapi.slice.ontology.domain.service.CardinalityService @@ -107,6 +108,7 @@ object LayersLive { OntologyCache & OntologyCacheHelpers & OntologyInferencer & + OntologyApiModule.Provided & OntologyResponderV2 & OntologyTriplestoreHelpers & PermissionRestService & @@ -168,8 +170,9 @@ object LayersLive { ManagementEndpoints.layer, ManagementRoutes.layer, MessageRelayLive.layer, - OntologyCacheLive.layer, + OntologyApiModule.layer, OntologyCacheHelpersLive.layer, + OntologyCacheLive.layer, OntologyRepoLive.layer, OntologyResponderV2.layer, OntologyServiceLive.layer, 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 bf05700d11..1699faa39f 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 @@ -1040,44 +1040,6 @@ case class ChangeOntologyMetadataRequestV2( requestingUser: User, ) extends OntologiesResponderRequestV2 -/** - * Constructs instances of [[ChangeOntologyMetadataRequestV2]] based on JSON-LD requests. - */ -object ChangeOntologyMetadataRequestV2 { - - /** - * Converts a JSON-LD request to a [[ChangeOntologyMetadataRequestV2]]. - * - * @param jsonLDDocument the JSON-LD input. - * @param apiRequestID the UUID of the API request. - * @param requestingUser the user making the request. - * @return a [[ChangeClassLabelsOrCommentsRequestV2]] representing the input. - */ - def fromJsonLd( - jsonLDDocument: JsonLDDocument, - apiRequestID: UUID, - requestingUser: User, - ): ChangeOntologyMetadataRequestV2 = { - val inputOntologyV2 = InputOntologyV2.fromJsonLD(jsonLDDocument) - val inputMetadata = inputOntologyV2.ontologyMetadata - val ontologyIri = inputMetadata.ontologyIri - val label: Option[String] = inputMetadata.label - val comment: Option[String] = inputMetadata.comment - val lastModificationDate = inputMetadata.lastModificationDate.getOrElse( - throw BadRequestException("No knora-api:lastModificationDate submitted"), - ) - - ChangeOntologyMetadataRequestV2( - ontologyIri = ontologyIri, - label = label, - comment = comment, - lastModificationDate = lastModificationDate, - apiRequestID = apiRequestID, - requestingUser = requestingUser, - ) - } -} - /** * Deletes the comment from an ontology. A successful response will be a [[ReadOntologyMetadataV2]]. * diff --git a/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala b/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala index dd0dada496..200043a203 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala @@ -32,6 +32,7 @@ import org.knora.webapi.slice.common.ApiComplexV2JsonLdRequestParser import org.knora.webapi.slice.common.api.AuthorizationRestService import org.knora.webapi.slice.infrastructure.api.ManagementRoutes import org.knora.webapi.slice.lists.api.ListsApiV2Routes +import org.knora.webapi.slice.ontology.api.OntologyV2RequestParser import org.knora.webapi.slice.ontology.api.service.RestCardinalityService import org.knora.webapi.slice.resourceinfo.api.ResourceInfoRoutes import org.knora.webapi.slice.resourceinfo.domain.IriConverter @@ -100,8 +101,9 @@ object ApiRoutes { private type ApiRoutesRuntime = ApiComplexV2JsonLdRequestParser & AppConfig & AuthenticationApiRoutes & AuthorizationRestService & core.State & - IriConverter & MessageRelay & ProjectService & RestCardinalityService & WebApiAuthenticator & SearchApiRoutes & - SearchResponderV2 & SipiService & StringFormatter & UserService & ValuesResponderV2 & ListsApiV2Routes + IriConverter & ListsApiV2Routes & MessageRelay & OntologyV2RequestParser & ProjectService & + RestCardinalityService & SearchApiRoutes & SearchResponderV2 & SipiService & StringFormatter & UserService & + ValuesResponderV2 & WebApiAuthenticator /** * All routes composed together. 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 f9d6bcda18..059af3c67b 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 @@ -36,6 +36,7 @@ import org.knora.webapi.routing.RouteUtilV2 import org.knora.webapi.routing.RouteUtilV2.completeResponse import org.knora.webapi.routing.RouteUtilV2.getStringQueryParam import org.knora.webapi.routing.RouteUtilZ +import org.knora.webapi.slice.ontology.api.OntologyV2RequestParser import org.knora.webapi.slice.ontology.api.service.RestCardinalityService import org.knora.webapi.slice.resourceinfo.domain.IriConverter import org.knora.webapi.slice.security.Authenticator @@ -45,11 +46,13 @@ import org.knora.webapi.slice.security.Authenticator */ final case class OntologiesRouteV2()( private implicit val runtime: Runtime[ - AppConfig & Authenticator & IriConverter & MessageRelay & RestCardinalityService & StringFormatter, + AppConfig & Authenticator & IriConverter & MessageRelay & OntologyV2RequestParser & RestCardinalityService & + StringFormatter, ], ) { private val ontologiesBasePath: PathMatcher[Unit] = PathMatcher("v2" / "ontologies") + private val requestParser = ZIO.serviceWithZIO[OntologyV2RequestParser] private val allLanguagesKey = "allLanguages" private val lastModificationDateKey = "lastModificationDate" @@ -151,11 +154,11 @@ final case class OntologiesRouteV2()( entity(as[String]) { jsonRequest => requestContext => { val requestTask = for { - requestDoc <- RouteUtilV2.parseJsonLd(jsonRequest) requestingUser <- ZIO.serviceWithZIO[Authenticator](_.getUserADM(requestContext)) apiRequestId <- RouteUtilZ.randomUuid() requestMessage <- - ZIO.attempt(ChangeOntologyMetadataRequestV2.fromJsonLd(requestDoc, apiRequestId, requestingUser)) + requestParser(_.changeOntologyMetadataRequestV2(jsonRequest, apiRequestId, requestingUser)) + .mapError(BadRequestException.apply) } yield requestMessage RouteUtilV2.runRdfRouteZ(requestTask, requestContext) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologyApiModule.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologyApiModule.scala new file mode 100644 index 0000000000..da58f7dc91 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologyApiModule.scala @@ -0,0 +1,16 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.ontology.api +import zio.* + +import org.knora.webapi.slice.URModule +import org.knora.webapi.slice.resourceinfo.domain.IriConverter + +object OntologyApiModule extends URModule[IriConverter, OntologyV2RequestParser] { self => + + val layer: URLayer[self.Dependencies, self.Provided] = + ZLayer.makeSome[self.Dependencies, self.Provided](OntologyV2RequestParser.layer) +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologyV2RequestParser.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologyV2RequestParser.scala new file mode 100644 index 0000000000..a3855770e2 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologyV2RequestParser.scala @@ -0,0 +1,53 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.ontology.api + +import org.apache.jena.vocabulary.RDFS +import zio.* + +import java.util.UUID +import scala.language.implicitConversions + +import org.knora.webapi.messages.OntologyConstants.KnoraApiV2Complex.* +import org.knora.webapi.messages.SmartIri +import org.knora.webapi.messages.v2.responder.ontologymessages.ChangeOntologyMetadataRequestV2 +import org.knora.webapi.slice.admin.domain.model.User +import org.knora.webapi.slice.common.jena.JenaConversions.given_Conversion_String_Property +import org.knora.webapi.slice.common.jena.ModelOps +import org.knora.webapi.slice.common.jena.ModelOps.* +import org.knora.webapi.slice.common.jena.ResourceOps.* +import org.knora.webapi.slice.resourceinfo.domain.IriConverter + +final case class OntologyV2RequestParser(iriConverter: IriConverter) { + + def changeOntologyMetadataRequestV2( + jsonLd: String, + apiRequestId: UUID, + requestingUser: User, + ): IO[String, ChangeOntologyMetadataRequestV2] = ZIO.scoped { + for { + model <- ModelOps.fromJsonLd(jsonLd) + r <- ZIO.fromEither(model.singleRootResource) + ontologyIri: SmartIri <- + ZIO.fromOption(r.uri).orElseFail("No IRI found").flatMap(iriConverter.asSmartIri(_).mapError(_.getMessage)) + label <- ZIO.fromEither(r.objectStringOption(RDFS.label)) + comment <- ZIO.fromEither(r.objectStringOption(RDFS.comment)) + lastModificationDate <- ZIO.fromEither(r.objectInstant(LastModificationDate)) + } yield ChangeOntologyMetadataRequestV2( + ontologyIri, + label, + comment, + lastModificationDate, + apiRequestId, + requestingUser, + ) + } + +} + +object OntologyV2RequestParser { + val layer = ZLayer.derive[OntologyV2RequestParser] +} diff --git a/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala b/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala index 15e9a7277b..6d93af2a19 100644 --- a/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala +++ b/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala @@ -9,7 +9,11 @@ import zio.Chunk import zio.NonEmptyChunk import dsp.valueobjects.LanguageCode +import org.knora.webapi.TestDataFactory.Project.systemProjectIri +import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionADM +import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionsDataADM import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2 +import org.knora.webapi.slice.admin.api.model.Project import org.knora.webapi.slice.admin.domain.model.* import org.knora.webapi.slice.admin.domain.model.Email import org.knora.webapi.slice.admin.domain.model.FamilyName @@ -23,13 +27,39 @@ import org.knora.webapi.slice.admin.domain.model.SystemAdmin import org.knora.webapi.slice.admin.domain.model.UserIri import org.knora.webapi.slice.admin.domain.model.UserStatus import org.knora.webapi.slice.admin.domain.model.Username +import org.knora.webapi.slice.admin.domain.service.KnoraGroupRepo +import org.knora.webapi.slice.admin.domain.service.KnoraProjectRepo /** * Helps in creating value objects for tests. */ object TestDataFactory { + object Project { + val systemProjectIri: IRI = KnoraProjectRepo.builtIn.SystemProject.id.value // built-in project + } object User { + /* represents the user profile of 'root' as found in admin-data.ttl */ + val rootUser: User = + org.knora.webapi.slice.admin.domain.model.User( + id = "http://rdfh.ch/users/root", + username = "root", + email = "root@example.com", + givenName = "System", + familyName = "Administrator", + status = true, + lang = "de", + password = Option("$2a$12$7XEBehimXN1rbhmVgQsyve08.vtDmKK7VMin4AdgCEtE4DWgfQbTK"), + groups = Seq.empty[Group], + projects = Seq.empty[Project], + permissions = PermissionsDataADM( + groupsPerProject = Map( + systemProjectIri -> List(KnoraGroupRepo.builtIn.SystemAdmin.id.value), + ), + administrativePermissionsPerProject = Map.empty[IRI, Set[PermissionADM]], + ), + ) + val testUser: KnoraUser = KnoraUser( UserIri.unsafeFrom("http://rdfh.ch/users/exists"), Username.unsafeFrom("testuser"), diff --git a/webapi/src/test/scala/org/knora/webapi/slice/ontology/api/OntologyV2RequestParserSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/ontology/api/OntologyV2RequestParserSpec.scala new file mode 100644 index 0000000000..93002f78f9 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/slice/ontology/api/OntologyV2RequestParserSpec.scala @@ -0,0 +1,77 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.ontology.api +import zio.* +import zio.test.* +import zio.test.check + +import java.time.Instant + +import org.knora.webapi.TestDataFactory +import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.messages.v2.responder.ontologymessages.ChangeOntologyMetadataRequestV2 +import org.knora.webapi.slice.common.JsonLdTestUtil.JsonLdTransformations +import org.knora.webapi.slice.resourceinfo.domain.IriConverter + +object OntologyV2RequestParserSpec extends ZIOSpecDefault { + private val sf = StringFormatter.getInitializedTestInstance + + private val parser = ZIO.serviceWithZIO[OntologyV2RequestParser] + private val user = TestDataFactory.User.rootUser + + private val changeOntologyMetadataRequestV2Suite = + suite("ChangeOntologyMetadataRequestV2") { + test("should parse correct jsonLd") { + val instant = Instant.parse("2017-12-19T15:23:42.166Z") + val jsonLd: String = + """ + |{ + | "@id" : "http://0.0.0.0:3333/ontology/0001/anything/v2", + | "@type" : "owl:Ontology", + | "knora-api:lastModificationDate" : { + | "@type" : "xsd:dateTimeStamp", + | "@value" : "2017-12-19T15:23:42.166Z" + | }, + | "rdfs:label" : { + | "@language" : "en", + | "@value" : "Some Label" + | }, + | "rdfs:comment" : { + | "@language" : "en", + | "@value" : "Some Comment" + | }, + | "@context" : { + | "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + | "knora-api" : "http://api.knora.org/ontology/knora-api/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" : "http://0.0.0.0:3333/ontology/0001/anything/v2#" + | } + |} + |""".stripMargin + + check(JsonLdTransformations.allGen) { t => + for { + uuid <- Random.nextUUID + req <- parser(_.changeOntologyMetadataRequestV2(t(jsonLd), uuid, user)) + } yield assertTrue( + req == ChangeOntologyMetadataRequestV2( + sf.toSmartIri("http://0.0.0.0:3333/ontology/0001/anything/v2"), + Some("Some Label"), + Some("Some Comment"), + instant, + uuid, + user, + ), + ) + } + } + } + + val spec = suite("OntologyV2RequestParser")(changeOntologyMetadataRequestV2Suite) + .provide(OntologyV2RequestParser.layer, IriConverter.layer, StringFormatter.test) +}