Skip to content

Commit

Permalink
feat: Support JSON-LD ontology v2 change requests (#3451)
Browse files Browse the repository at this point in the history
  • Loading branch information
seakayone authored Jan 3, 2025
1 parent 63aed0f commit a8b4bab
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -123,6 +124,7 @@ object LayersTest {
ListsApiModule.Provided &
ListsResponder &
MessageRelay &
OntologyApiModule.Provided &
OntologyCache &
OntologyCacheHelpers &
OntologyInferencer &
Expand Down Expand Up @@ -193,6 +195,7 @@ object LayersTest {
ManagementEndpoints.layer,
ManagementRoutes.layer,
MessageRelayLive.layer,
OntologyApiModule.layer,
OntologyCacheLive.layer,
OntologyCacheHelpersLive.layer,
OntologyRepoLive.layer,
Expand Down
5 changes: 4 additions & 1 deletion webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -107,6 +108,7 @@ object LayersLive {
OntologyCache &
OntologyCacheHelpers &
OntologyInferencer &
OntologyApiModule.Provided &
OntologyResponderV2 &
OntologyTriplestoreHelpers &
PermissionRestService &
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]].
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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]
}
30 changes: 30 additions & 0 deletions webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = "[email protected]",
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"),
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit a8b4bab

Please sign in to comment.