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 7f1d3f2ef9..8a71a787a9 100644 --- a/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala +++ b/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala @@ -27,8 +27,10 @@ import org.knora.webapi.responders.IriService import org.knora.webapi.responders.admin.* import org.knora.webapi.responders.v2.* import org.knora.webapi.responders.v2.ontology.CardinalityHandler -import org.knora.webapi.responders.v2.ontology.OntologyHelpers -import org.knora.webapi.responders.v2.ontology.OntologyHelpersLive +import org.knora.webapi.responders.v2.ontology.OntologyCacheHelpers +import org.knora.webapi.responders.v2.ontology.OntologyCacheHelpersLive +import org.knora.webapi.responders.v2.ontology.OntologyTriplestoreHelpers +import org.knora.webapi.responders.v2.ontology.OntologyTriplestoreHelpersLive import org.knora.webapi.routing.* import org.knora.webapi.slice.admin.AdminModule import org.knora.webapi.slice.admin.api.* @@ -111,10 +113,11 @@ object LayersTest { ListsResponder & MessageRelay & OntologyCache & - OntologyHelpers & + OntologyCacheHelpers & OntologyInferencer & OntologyRepo & OntologyService & + OntologyTriplestoreHelpers & PermissionRestService & PermissionUtilADM & PermissionsResponder & @@ -175,9 +178,10 @@ object LayersTest { ManagementRoutes.layer, MessageRelayLive.layer, OntologyCacheLive.layer, - OntologyHelpersLive.layer, + OntologyCacheHelpersLive.layer, OntologyRepoLive.layer, OntologyServiceLive.layer, + OntologyTriplestoreHelpersLive.layer, PermissionUtilADMLive.layer, PermissionsResponder.layer, PredicateObjectMapper.layer, diff --git a/webapi/src/main/scala/org/knora/webapi/core/AppRouter.scala b/webapi/src/main/scala/org/knora/webapi/core/AppRouter.scala index e26991eb21..c40de7c2ef 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/AppRouter.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/AppRouter.scala @@ -18,7 +18,8 @@ import org.knora.webapi.messages.util.PermissionUtilADM import org.knora.webapi.messages.util.standoff.StandoffTagUtilV2 import org.knora.webapi.responders.v2.ResourceUtilV2 import org.knora.webapi.responders.v2.ontology.CardinalityHandler -import org.knora.webapi.responders.v2.ontology.OntologyHelpers +import org.knora.webapi.responders.v2.ontology.OntologyCacheHelpers +import org.knora.webapi.responders.v2.ontology.OntologyTriplestoreHelpers import org.knora.webapi.slice.ontology.domain.service.CardinalityService import org.knora.webapi.slice.ontology.domain.service.OntologyRepo import org.knora.webapi.slice.ontology.repo.service.OntologyCache @@ -29,7 +30,8 @@ object AppRouter { val layer: ZLayer[ pekko.actor.ActorSystem & CardinalityHandler & CardinalityService & ConstructResponseUtilV2 & MessageRelay & - OntologyCache & OntologyHelpers & OntologyRepo & PermissionUtilADM & ResourceUtilV2 & StandoffTagUtilV2, + OntologyCache & OntologyTriplestoreHelpers & OntologyCacheHelpers & OntologyRepo & PermissionUtilADM & + ResourceUtilV2 & StandoffTagUtilV2, Nothing, AppRouter, ] = @@ -40,8 +42,9 @@ object AppRouter { messageRelay <- ZIO.service[MessageRelay] runtime <- ZIO.runtime[ - CardinalityHandler & CardinalityService & ConstructResponseUtilV2 & OntologyCache & OntologyHelpers & - OntologyRepo & PermissionUtilADM & ResourceUtilV2 & StandoffTagUtilV2, + CardinalityHandler & CardinalityService & ConstructResponseUtilV2 & OntologyCache & + OntologyTriplestoreHelpers & OntologyCacheHelpers & OntologyRepo & PermissionUtilADM & ResourceUtilV2 & + StandoffTagUtilV2, ] ref = system.actorOf( Props(core.actors.RoutingActor(messageRelay)(runtime)).withRouter(new RoundRobinPool(1_000)), 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 17debc3f5c..02092ee2ac 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala @@ -25,8 +25,10 @@ import org.knora.webapi.responders.admin.* import org.knora.webapi.responders.admin.ListsResponder import org.knora.webapi.responders.v2.* import org.knora.webapi.responders.v2.ontology.CardinalityHandler -import org.knora.webapi.responders.v2.ontology.OntologyHelpers -import org.knora.webapi.responders.v2.ontology.OntologyHelpersLive +import org.knora.webapi.responders.v2.ontology.OntologyCacheHelpers +import org.knora.webapi.responders.v2.ontology.OntologyCacheHelpersLive +import org.knora.webapi.responders.v2.ontology.OntologyTriplestoreHelpers +import org.knora.webapi.responders.v2.ontology.OntologyTriplestoreHelpersLive import org.knora.webapi.routing.* import org.knora.webapi.slice.admin.AdminModule import org.knora.webapi.slice.admin.api.* @@ -94,9 +96,10 @@ object LayersLive { ListsResponderV2 & MessageRelay & OntologyCache & - OntologyHelpers & + OntologyCacheHelpers & OntologyInferencer & OntologyResponderV2 & + OntologyTriplestoreHelpers & PermissionRestService & PermissionUtilADM & PermissionsResponder & @@ -153,10 +156,11 @@ object LayersLive { ManagementRoutes.layer, MessageRelayLive.layer, OntologyCacheLive.layer, - OntologyHelpersLive.layer, + OntologyCacheHelpersLive.layer, OntologyRepoLive.layer, OntologyResponderV2Live.layer, OntologyServiceLive.layer, + OntologyTriplestoreHelpersLive.layer, PermissionUtilADMLive.layer, PermissionsResponder.layer, PredicateObjectMapper.layer, diff --git a/webapi/src/main/scala/org/knora/webapi/core/actors/RoutingActor.scala b/webapi/src/main/scala/org/knora/webapi/core/actors/RoutingActor.scala index c504bf340d..f8b65ed099 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/actors/RoutingActor.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/actors/RoutingActor.scala @@ -16,7 +16,8 @@ import org.knora.webapi.messages.util.PermissionUtilADM import org.knora.webapi.messages.util.standoff.StandoffTagUtilV2 import org.knora.webapi.responders.v2.ResourceUtilV2 import org.knora.webapi.responders.v2.ontology.CardinalityHandler -import org.knora.webapi.responders.v2.ontology.OntologyHelpers +import org.knora.webapi.responders.v2.ontology.OntologyCacheHelpers +import org.knora.webapi.responders.v2.ontology.OntologyTriplestoreHelpers import org.knora.webapi.slice.ontology.domain.service.CardinalityService import org.knora.webapi.slice.ontology.domain.service.OntologyRepo import org.knora.webapi.slice.ontology.repo.service.OntologyCache @@ -24,8 +25,8 @@ import org.knora.webapi.util.ActorUtil final case class RoutingActor(messageRelay: MessageRelay)(implicit val runtime: Runtime[ - CardinalityHandler & CardinalityService & ConstructResponseUtilV2 & OntologyCache & OntologyHelpers & OntologyRepo & - PermissionUtilADM & ResourceUtilV2 & StandoffTagUtilV2, + CardinalityHandler & CardinalityService & ConstructResponseUtilV2 & OntologyCache & OntologyTriplestoreHelpers & + OntologyCacheHelpers & OntologyRepo & PermissionUtilADM & ResourceUtilV2 & StandoffTagUtilV2, ], ) extends Actor { 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 4eace6d229..9c9fe9f368 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 @@ -33,7 +33,9 @@ import org.knora.webapi.responders.IriLocker import org.knora.webapi.responders.IriService import org.knora.webapi.responders.Responder import org.knora.webapi.responders.v2.ontology.CardinalityHandler +import org.knora.webapi.responders.v2.ontology.OntologyCacheHelpers import org.knora.webapi.responders.v2.ontology.OntologyHelpers +import org.knora.webapi.responders.v2.ontology.OntologyTriplestoreHelpers import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.admin.domain.service.KnoraProjectService @@ -71,7 +73,8 @@ final case class OntologyResponderV2Live( cardinalityService: CardinalityService, iriService: IriService, ontologyCache: OntologyCache, - ontologyHelpers: OntologyHelpers, + ontologyCacheHelpers: OntologyCacheHelpers, + ontologyTriplestoreHelpers: OntologyTriplestoreHelpers, ontologyRepo: OntologyRepo, knoraProjectService: KnoraProjectService, triplestoreService: TriplestoreService, @@ -98,7 +101,7 @@ final case class OntologyResponderV2Live( case OntologyEntitiesGetRequestV2(ontologyIri, allLanguages, requestingUser) => getOntologyEntitiesV2(ontologyIri, allLanguages, requestingUser) case ClassesGetRequestV2(resourceClassIris, allLanguages, requestingUser) => - ontologyHelpers.getClassDefinitionsFromOntologyV2(resourceClassIris, allLanguages, requestingUser) + ontologyCacheHelpers.getClassDefinitionsFromOntologyV2(resourceClassIris, allLanguages, requestingUser) case PropertiesGetRequestV2(propertyIris, allLanguages, requestingUser) => getPropertyDefinitionsFromOntologyV2(propertyIris, allLanguages, requestingUser) case OntologyMetadataGetByProjectRequestV2(projectIris, _) => @@ -151,7 +154,8 @@ final case class OntologyResponderV2Live( classIris: Set[SmartIri] = Set.empty[SmartIri], propertyIris: Set[SmartIri], requestingUser: User, - ): Task[EntityInfoGetResponseV2] = ontologyHelpers.getEntityInfoResponseV2(classIris, propertyIris, requestingUser) + ): Task[EntityInfoGetResponseV2] = + ontologyCacheHelpers.getEntityInfoResponseV2(classIris, propertyIris, requestingUser) /** * Given a list of standoff class IRIs and a list of property IRIs (ontology entities), returns an [[StandoffEntityInfoGetResponseV2]] describing both resource and property entities. @@ -441,7 +445,7 @@ final case class OntologyResponderV2Live( def makeTaskFuture(internalOntologyIri: SmartIri): Task[ReadOntologyMetadataV2] = for { // Make sure the ontology doesn't already exist. - existingOntologyMetadata <- ontologyHelpers.loadOntologyMetadata(internalOntologyIri) + existingOntologyMetadata <- ontologyTriplestoreHelpers.loadOntologyMetadata(internalOntologyIri) _ <- ZIO.when(existingOntologyMetadata.nonEmpty) { val msg = @@ -495,7 +499,7 @@ final case class OntologyResponderV2Live( lastModificationDate = Some(currentTime), ).unescape - maybeLoadedOntologyMetadata <- ontologyHelpers.loadOntologyMetadata(internalOntologyIri) + maybeLoadedOntologyMetadata <- ontologyTriplestoreHelpers.loadOntologyMetadata(internalOntologyIri) _ <- maybeLoadedOntologyMetadata match { case Some(loadedOntologyMetadata) => @@ -567,13 +571,13 @@ final case class OntologyResponderV2Live( // Check that the user has permission to update the ontology. projectIri <- - ontologyHelpers.checkPermissionsForOntologyUpdate( + ontologyCacheHelpers.checkPermissionsForOntologyUpdate( internalOntologyIri, changeOntologyMetadataRequest.requestingUser, ) // Check that the ontology exists and has not been updated by another user since the client last read its metadata. - _ <- ontologyHelpers.checkOntologyLastModificationDateBeforeUpdate( + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateBeforeUpdate( internalOntologyIri, changeOntologyMetadataRequest.lastModificationDate, ) @@ -629,7 +633,7 @@ final case class OntologyResponderV2Live( lastModificationDate = Some(currentTime), ).unescape - maybeLoadedOntologyMetadata <- ontologyHelpers.loadOntologyMetadata(internalOntologyIri) + maybeLoadedOntologyMetadata <- ontologyTriplestoreHelpers.loadOntologyMetadata(internalOntologyIri) _ <- maybeLoadedOntologyMetadata match { case Some(loadedOntologyMetadata) => @@ -649,7 +653,7 @@ final case class OntologyResponderV2Live( } yield ReadOntologyMetadataV2(ontologies = Set(unescapedNewMetadata)) for { - _ <- ontologyHelpers.checkExternalOntologyIriForUpdate(changeOntologyMetadataRequest.ontologyIri) + _ <- OntologyHelpers.checkExternalOntologyIriForUpdate(changeOntologyMetadataRequest.ontologyIri) internalOntologyIri = changeOntologyMetadataRequest.ontologyIri.toOntologySchema(InternalSchema) // Do the remaining pre-update checks and the update while holding a global ontology cache lock. @@ -669,13 +673,13 @@ final case class OntologyResponderV2Live( cacheData <- ontologyCache.getCacheData // Check that the user has permission to update the ontology. - projectIri <- ontologyHelpers.checkPermissionsForOntologyUpdate( + projectIri <- ontologyCacheHelpers.checkPermissionsForOntologyUpdate( internalOntologyIri, deleteOntologyCommentRequestV2.requestingUser, ) // Check that the ontology exists and has not been updated by another user since the client last read its metadata. - _ <- ontologyHelpers.checkOntologyLastModificationDateBeforeUpdate( + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateBeforeUpdate( internalOntologyIri, deleteOntologyCommentRequestV2.lastModificationDate, ) @@ -711,7 +715,7 @@ final case class OntologyResponderV2Live( lastModificationDate = Some(currentTime), ).unescape - maybeLoadedOntologyMetadata <- ontologyHelpers.loadOntologyMetadata(internalOntologyIri) + maybeLoadedOntologyMetadata <- ontologyTriplestoreHelpers.loadOntologyMetadata(internalOntologyIri) _ <- maybeLoadedOntologyMetadata match { case Some(loadedOntologyMetadata) => @@ -728,7 +732,7 @@ final case class OntologyResponderV2Live( } yield ReadOntologyMetadataV2(ontologies = Set(unescapedNewMetadata)) for { - _ <- ontologyHelpers.checkExternalOntologyIriForUpdate(deleteOntologyCommentRequestV2.ontologyIri) + _ <- OntologyHelpers.checkExternalOntologyIriForUpdate(deleteOntologyCommentRequestV2.ontologyIri) internalOntologyIri = deleteOntologyCommentRequestV2.ontologyIri.toOntologySchema(InternalSchema) // Do the remaining pre-update checks and the update while holding a global ontology cache lock. @@ -753,7 +757,7 @@ final case class OntologyResponderV2Live( internalClassDef: ClassInfoContentV2 = createClassRequest.classInfoContent.toOntologySchema(InternalSchema) // Check that the ontology exists and has not been updated by another user since the client last read it. - _ <- ontologyHelpers.checkOntologyLastModificationDateBeforeUpdate( + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateBeforeUpdate( internalOntologyIri, createClassRequest.lastModificationDate, ) @@ -862,7 +866,7 @@ final case class OntologyResponderV2Live( // Add the SPARQL-escaped class to the triplestore. - currentTime: Instant = Instant.now + currentTime = Instant.now updateSparql = sparql.v2.txt.createClass( ontologyNamedGraphIri = internalOntologyIri, @@ -874,10 +878,10 @@ final case class OntologyResponderV2Live( _ <- triplestoreService.query(Update(updateSparql)) // Check that the ontology's last modification date was updated. - _ <- ontologyHelpers.checkOntologyLastModificationDateAfterUpdate(internalOntologyIri, currentTime) + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateAfterUpdate(internalOntologyIri, currentTime) // Check that the data that was saved corresponds to the data that was submitted. - loadedClassDef <- ontologyHelpers.loadClassDefinition(internalClassIri) + loadedClassDef <- ontologyTriplestoreHelpers.loadClassDefinition(internalClassIri) _ <- ZIO.when(loadedClassDef != unescapedClassDefWithLinkValueProps) { val msg = @@ -897,7 +901,7 @@ final case class OntologyResponderV2Live( _ <- ontologyCache.cacheUpdatedOntologyWithClass(internalOntologyIri, updatedOntology, internalClassIri) // Read the data back from the cache. - response <- ontologyHelpers.getClassDefinitionsFromOntologyV2( + response <- ontologyCacheHelpers.getClassDefinitionsFromOntologyV2( classIris = Set(internalClassIri), allLanguages = true, requestingUser = createClassRequest.requestingUser, @@ -911,7 +915,8 @@ final case class OntologyResponderV2Live( externalClassIri = createClassRequest.classInfoContent.classIri externalOntologyIri = externalClassIri.getOntologyFromEntity - _ <- ontologyHelpers.checkOntologyAndEntityIrisForUpdate(externalOntologyIri, externalClassIri, requestingUser) + _ <- + ontologyCacheHelpers.checkOntologyAndEntityIrisForUpdate(externalOntologyIri, externalClassIri, requestingUser) internalClassIri = externalClassIri.toOntologySchema(InternalSchema) internalOntologyIri = externalOntologyIri.toOntologySchema(InternalSchema) @@ -938,7 +943,7 @@ final case class OntologyResponderV2Live( 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. - _ <- ontologyHelpers.checkOntologyLastModificationDateBeforeUpdate( + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateBeforeUpdate( internalOntologyIri, changeGuiOrderRequest.lastModificationDate, ) @@ -1017,10 +1022,10 @@ final case class OntologyResponderV2Live( _ <- triplestoreService.query(Update(updateSparql)) // Check that the ontology's last modification date was updated. - _ <- ontologyHelpers.checkOntologyLastModificationDateAfterUpdate(internalOntologyIri, currentTime) + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateAfterUpdate(internalOntologyIri, currentTime) // Check that the data that was saved corresponds to the data that was submitted. - loadedClassDef <- ontologyHelpers.loadClassDefinition(internalClassIri) + loadedClassDef <- ontologyTriplestoreHelpers.loadClassDefinition(internalClassIri) _ <- ZIO.when(loadedClassDef != newReadClassInfo.entityInfoContent) { val msg = @@ -1040,7 +1045,7 @@ final case class OntologyResponderV2Live( _ <- ontologyCache.cacheUpdatedOntologyWithClass(internalOntologyIri, updatedOntology, internalClassIri) // Read the data back from the cache. - response <- ontologyHelpers.getClassDefinitionsFromOntologyV2( + response <- ontologyCacheHelpers.getClassDefinitionsFromOntologyV2( classIris = Set(internalClassIri), allLanguages = true, requestingUser = changeGuiOrderRequest.requestingUser, @@ -1055,7 +1060,8 @@ final case class OntologyResponderV2Live( externalClassIri = changeGuiOrderRequest.classInfoContent.classIri externalOntologyIri = externalClassIri.getOntologyFromEntity - _ <- ontologyHelpers.checkOntologyAndEntityIrisForUpdate(externalOntologyIri, externalClassIri, requestingUser) + _ <- + ontologyCacheHelpers.checkOntologyAndEntityIrisForUpdate(externalOntologyIri, externalClassIri, requestingUser) internalClassIri = externalClassIri.toOntologySchema(InternalSchema) internalOntologyIri = externalOntologyIri.toOntologySchema(InternalSchema) @@ -1084,7 +1090,7 @@ final case class OntologyResponderV2Live( internalClassDef: ClassInfoContentV2 = addCardinalitiesRequest.classInfoContent.toOntologySchema(InternalSchema) // Check that the ontology exists and has not been updated by another user since the client last read it. - _ <- ontologyHelpers.checkOntologyLastModificationDateBeforeUpdate( + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateBeforeUpdate( internalOntologyIri, addCardinalitiesRequest.lastModificationDate, ) @@ -1222,10 +1228,10 @@ final case class OntologyResponderV2Live( _ <- triplestoreService.query(Update(updateSparql)) // Check that the ontology's last modification date was updated. - _ <- ontologyHelpers.checkOntologyLastModificationDateAfterUpdate(internalOntologyIri, currentTime) + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateAfterUpdate(internalOntologyIri, currentTime) // Check that the data that was saved corresponds to the data that was submitted. - loadedClassDef <- ontologyHelpers.loadClassDefinition(internalClassIri) + loadedClassDef <- ontologyTriplestoreHelpers.loadClassDefinition(internalClassIri) _ <- ZIO.when(loadedClassDef != newInternalClassDefWithLinkValueProps) { val msg = @@ -1245,7 +1251,7 @@ final case class OntologyResponderV2Live( _ <- ontologyCache.cacheUpdatedOntologyWithClass(internalOntologyIri, updatedOntology, internalClassIri) // Read the data back from the cache. - response <- ontologyHelpers.getClassDefinitionsFromOntologyV2( + response <- ontologyCacheHelpers.getClassDefinitionsFromOntologyV2( classIris = Set(internalClassIri), allLanguages = true, requestingUser = addCardinalitiesRequest.requestingUser, @@ -1259,7 +1265,8 @@ final case class OntologyResponderV2Live( externalClassIri = addCardinalitiesRequest.classInfoContent.classIri externalOntologyIri = externalClassIri.getOntologyFromEntity - _ <- ontologyHelpers.checkOntologyAndEntityIrisForUpdate(externalOntologyIri, externalClassIri, requestingUser) + _ <- + ontologyCacheHelpers.checkOntologyAndEntityIrisForUpdate(externalOntologyIri, externalClassIri, requestingUser) internalClassIri = externalClassIri.toOntologySchema(InternalSchema) internalOntologyIri = externalOntologyIri.toOntologySchema(InternalSchema) @@ -1290,7 +1297,7 @@ final case class OntologyResponderV2Live( val classIriExternal = request.classInfoContent.classIri val ontologyIriExternal = classIriExternal.getOntologyFromEntity for { - _ <- ontologyHelpers.checkOntologyAndEntityIrisForUpdate( + _ <- ontologyCacheHelpers.checkOntologyAndEntityIrisForUpdate( ontologyIriExternal, classIriExternal, request.requestingUser, @@ -1385,7 +1392,7 @@ final case class OntologyResponderV2Live( val ontologyIri = classIri.getOntologyFromEntity val lastModificationDate = request.lastModificationDate for { - _ <- ontologyHelpers.checkOntologyLastModificationDateBeforeUpdate(ontologyIri, lastModificationDate) + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateBeforeUpdate(ontologyIri, lastModificationDate) _ <- checkCanCardinalitiesBeSet(newModel.entityInfoContent).mapError(e => BadRequestException(e.getMessage)) } yield newModel } @@ -1418,7 +1425,7 @@ final case class OntologyResponderV2Live( _ <- replaceClassCardinalitiesInTripleStore(request, newReadClassInfo, timeOfUpdate) _ <- replaceClassCardinalitiesInOntologyCache(request, newReadClassInfo, timeOfUpdate) // Return the response with the new data from the cache - response <- ontologyHelpers.getClassDefinitionsFromOntologyV2( + response <- ontologyCacheHelpers.getClassDefinitionsFromOntologyV2( classIris = Set(classIri), allLanguages = true, requestingUser = request.requestingUser, @@ -1443,8 +1450,8 @@ final case class OntologyResponderV2Live( ) for { _ <- triplestoreService.query(Update(updateSparql)) - _ <- ontologyHelpers.checkOntologyLastModificationDateAfterUpdate(ontologyIri, timeOfUpdate) - loadedClassDef <- ontologyHelpers.loadClassDefinition(classIri) + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateAfterUpdate(ontologyIri, timeOfUpdate) + loadedClassDef <- ontologyTriplestoreHelpers.loadClassDefinition(classIri) _ <- ZIO.when(loadedClassDef != newReadClassInfo.entityInfoContent) { val msg = s"Attempted to save class definition ${newReadClassInfo.entityInfoContent}, but $loadedClassDef was saved instead." @@ -1490,7 +1497,8 @@ final case class OntologyResponderV2Live( externalClassIri = canDeleteCardinalitiesFromClassRequest.classInfoContent.classIri externalOntologyIri = externalClassIri.getOntologyFromEntity - _ <- ontologyHelpers.checkOntologyAndEntityIrisForUpdate(externalOntologyIri, externalClassIri, requestingUser) + _ <- + ontologyCacheHelpers.checkOntologyAndEntityIrisForUpdate(externalOntologyIri, externalClassIri, requestingUser) internalClassIri = externalClassIri.toOntologySchema(InternalSchema) internalOntologyIri = externalOntologyIri.toOntologySchema(InternalSchema) @@ -1523,7 +1531,8 @@ final case class OntologyResponderV2Live( externalClassIri = deleteCardinalitiesFromClassRequest.classInfoContent.classIri externalOntologyIri = externalClassIri.getOntologyFromEntity - _ <- ontologyHelpers.checkOntologyAndEntityIrisForUpdate(externalOntologyIri, externalClassIri, requestingUser) + _ <- + ontologyCacheHelpers.checkOntologyAndEntityIrisForUpdate(externalOntologyIri, externalClassIri, requestingUser) internalClassIri = externalClassIri.toOntologySchema(InternalSchema) internalOntologyIri = externalOntologyIri.toOntologySchema(InternalSchema) @@ -1565,7 +1574,7 @@ final case class OntologyResponderV2Live( } userCanUpdateOntology <- - ontologyHelpers.canUserUpdateOntology(internalOntologyIri, canDeleteClassRequest.requestingUser) + ontologyCacheHelpers.canUserUpdateOntology(internalOntologyIri, canDeleteClassRequest.requestingUser) classIsUsed <- iriService.isEntityUsed(internalClassIri) } yield CanDoResponseV2.of(userCanUpdateOntology && !classIsUsed) } @@ -1582,7 +1591,7 @@ final case class OntologyResponderV2Live( cacheData <- ontologyCache.getCacheData // Check that the ontology exists and has not been updated by another user since the client last read it. - _ <- ontologyHelpers.checkOntologyLastModificationDateBeforeUpdate( + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateBeforeUpdate( internalOntologyIri, deleteClassRequest.lastModificationDate, ) @@ -1617,7 +1626,7 @@ final case class OntologyResponderV2Live( _ <- triplestoreService.query(Update(updateSparql)) // Check that the ontology's last modification date was updated. - _ <- ontologyHelpers.checkOntologyLastModificationDateAfterUpdate(internalOntologyIri, currentTime) + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateAfterUpdate(internalOntologyIri, currentTime) // Update the cache. @@ -1638,7 +1647,8 @@ final case class OntologyResponderV2Live( externalClassIri = deleteClassRequest.classIri externalOntologyIri = externalClassIri.getOntologyFromEntity - _ <- ontologyHelpers.checkOntologyAndEntityIrisForUpdate(externalOntologyIri, externalClassIri, requestingUser) + _ <- + ontologyCacheHelpers.checkOntologyAndEntityIrisForUpdate(externalOntologyIri, externalClassIri, requestingUser) internalClassIri = externalClassIri.toOntologySchema(InternalSchema) internalOntologyIri = externalOntologyIri.toOntologySchema(InternalSchema) @@ -1684,7 +1694,7 @@ final case class OntologyResponderV2Live( } userCanUpdateOntology <- - ontologyHelpers.canUserUpdateOntology(internalOntologyIri, canDeletePropertyRequest.requestingUser) + ontologyCacheHelpers.canUserUpdateOntology(internalOntologyIri, canDeletePropertyRequest.requestingUser) propertyIsUsed <- iriService.isEntityUsed(internalPropertyIri) } yield CanDoResponseV2.of(userCanUpdateOntology && !propertyIsUsed) } @@ -1701,7 +1711,7 @@ final case class OntologyResponderV2Live( cacheData <- ontologyCache.getCacheData // Check that the ontology exists and has not been updated by another user since the client last read it. - _ <- ontologyHelpers.checkOntologyLastModificationDateBeforeUpdate( + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateBeforeUpdate( internalOntologyIri, deletePropertyRequest.lastModificationDate, ) @@ -1766,7 +1776,7 @@ final case class OntologyResponderV2Live( _ <- triplestoreService.query(Update(updateSparql)) // Check that the ontology's last modification date was updated. - _ <- ontologyHelpers.checkOntologyLastModificationDateAfterUpdate(internalOntologyIri, currentTime) + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateAfterUpdate(internalOntologyIri, currentTime) // Update the cache. @@ -1790,7 +1800,11 @@ final case class OntologyResponderV2Live( externalPropertyIri = deletePropertyRequest.propertyIri externalOntologyIri = externalPropertyIri.getOntologyFromEntity - _ <- ontologyHelpers.checkOntologyAndEntityIrisForUpdate(externalOntologyIri, externalPropertyIri, requestingUser) + _ <- ontologyCacheHelpers.checkOntologyAndEntityIrisForUpdate( + externalOntologyIri, + externalPropertyIri, + requestingUser, + ) internalPropertyIri = externalPropertyIri.toOntologySchema(InternalSchema) internalOntologyIri = externalOntologyIri.toOntologySchema(InternalSchema) @@ -1822,8 +1836,8 @@ final case class OntologyResponderV2Live( } userCanUpdateOntology <- - ontologyHelpers.canUserUpdateOntology(internalOntologyIri, canDeleteOntologyRequest.requestingUser) - subjectsUsingOntology <- ontologyHelpers.getSubjectsUsingOntology(ontology) + ontologyCacheHelpers.canUserUpdateOntology(internalOntologyIri, canDeleteOntologyRequest.requestingUser) + subjectsUsingOntology <- ontologyTriplestoreHelpers.getSubjectsUsingOntology(ontology) } yield CanDoResponseV2.of(userCanUpdateOntology && subjectsUsingOntology.isEmpty) } @@ -1834,10 +1848,13 @@ final case class OntologyResponderV2Live( // Check that the user has permission to update the ontology. _ <- - ontologyHelpers.checkPermissionsForOntologyUpdate(internalOntologyIri, deleteOntologyRequest.requestingUser) + ontologyCacheHelpers.checkPermissionsForOntologyUpdate( + internalOntologyIri, + deleteOntologyRequest.requestingUser, + ) // Check that the ontology exists and has not been updated by another user since the client last read it. - _ <- ontologyHelpers.checkOntologyLastModificationDateBeforeUpdate( + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateBeforeUpdate( internalOntologyIri, deleteOntologyRequest.lastModificationDate, ) @@ -1845,7 +1862,7 @@ final case class OntologyResponderV2Live( // Check that none of the entities in the ontology are used in data or in other ontologies. ontology = cacheData.ontologies(internalOntologyIri) - subjectsUsingOntology <- ontologyHelpers.getSubjectsUsingOntology(ontology) + subjectsUsingOntology <- ontologyTriplestoreHelpers.getSubjectsUsingOntology(ontology) _ <- ZIO.when(subjectsUsingOntology.nonEmpty) { val sortedSubjects = subjectsUsingOntology.map(s => "<" + s + ">").toVector.sorted.mkString(", ") @@ -1860,7 +1877,7 @@ final case class OntologyResponderV2Live( _ <- ontologyCache.deleteOntology(internalOntologyIri) // Check that the ontology has been deleted. - maybeOntologyMetadata <- ontologyHelpers.loadOntologyMetadata(internalOntologyIri) + maybeOntologyMetadata <- ontologyTriplestoreHelpers.loadOntologyMetadata(internalOntologyIri) _ <- ZIO.when(maybeOntologyMetadata.nonEmpty) { val msg = @@ -1871,7 +1888,7 @@ final case class OntologyResponderV2Live( } yield SuccessResponseV2(s"Ontology ${internalOntologyIri.toOntologySchema(ApiV2Complex)} has been deleted") for { - _ <- ontologyHelpers.checkExternalOntologyIriForUpdate(deleteOntologyRequest.ontologyIri) + _ <- OntologyHelpers.checkExternalOntologyIriForUpdate(deleteOntologyRequest.ontologyIri) internalOntologyIri = deleteOntologyRequest.ontologyIri.toOntologySchema(InternalSchema) // Do the remaining pre-update checks and the update while holding a global ontology cache lock. @@ -1896,7 +1913,7 @@ final case class OntologyResponderV2Live( internalPropertyDef = createPropertyRequest.propertyInfoContent.toOntologySchema(InternalSchema) // Check that the ontology exists and has not been updated by another user since the client last read it. - _ <- ontologyHelpers.checkOntologyLastModificationDateBeforeUpdate( + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateBeforeUpdate( internalOntologyIri, createPropertyRequest.lastModificationDate, ) @@ -2084,12 +2101,12 @@ final case class OntologyResponderV2Live( _ <- triplestoreService.query(Update(updateSparql)) // Check that the ontology's last modification date was updated. - _ <- ontologyHelpers.checkOntologyLastModificationDateAfterUpdate(internalOntologyIri, currentTime) + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateAfterUpdate(internalOntologyIri, currentTime) // 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 <- ontologyHelpers.loadPropertyDefinition(internalPropertyIri) + loadedPropertyDef <- ontologyTriplestoreHelpers.loadPropertyDefinition(internalPropertyIri) unescapedInputPropertyDef = internalPropertyDef.unescape @@ -2101,7 +2118,7 @@ final case class OntologyResponderV2Live( maybeLoadedLinkValuePropertyDefFuture: Option[Task[PropertyInfoContentV2]] = maybeLinkValuePropertyDef.map { linkValuePropertyDef => - ontologyHelpers.loadPropertyDefinition(linkValuePropertyDef.propertyIri) + ontologyTriplestoreHelpers.loadPropertyDefinition(linkValuePropertyDef.propertyIri) } maybeLoadedLinkValuePropertyDef <- @@ -2165,7 +2182,11 @@ final case class OntologyResponderV2Live( externalPropertyIri = createPropertyRequest.propertyInfoContent.propertyIri externalOntologyIri = externalPropertyIri.getOntologyFromEntity - _ <- ontologyHelpers.checkOntologyAndEntityIrisForUpdate(externalOntologyIri, externalPropertyIri, requestingUser) + _ <- ontologyCacheHelpers.checkOntologyAndEntityIrisForUpdate( + externalOntologyIri, + externalPropertyIri, + requestingUser, + ) internalPropertyIri = externalPropertyIri.toOntologySchema(InternalSchema) internalOntologyIri = externalOntologyIri.toOntologySchema(InternalSchema) @@ -2199,7 +2220,7 @@ final case class OntologyResponderV2Live( .orElseFail(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. - _ <- ontologyHelpers.checkOntologyLastModificationDateBeforeUpdate( + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateBeforeUpdate( internalOntologyIri, changePropertyGuiElementRequest.lastModificationDate, ) @@ -2240,12 +2261,12 @@ final case class OntologyResponderV2Live( // Check that the ontology's last modification date was updated. - _ <- ontologyHelpers.checkOntologyLastModificationDateAfterUpdate(internalOntologyIri, currentTime) + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateAfterUpdate(internalOntologyIri, currentTime) // 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 <- ontologyHelpers.loadPropertyDefinition(internalPropertyIri) + loadedPropertyDef <- ontologyTriplestoreHelpers.loadPropertyDefinition(internalPropertyIri) maybeNewGuiElementPredicate = newGuiElementIri.map { (guiElement: SmartIri) => @@ -2283,7 +2304,7 @@ final case class OntologyResponderV2Live( maybeLoadedLinkValuePropertyDefFuture = maybeCurrentLinkValueReadPropertyInfo.map { linkValueReadPropertyInfo => - ontologyHelpers.loadPropertyDefinition(linkValueReadPropertyInfo.entityInfoContent.propertyIri) + ontologyTriplestoreHelpers.loadPropertyDefinition(linkValueReadPropertyInfo.entityInfoContent.propertyIri) } maybeLoadedLinkValuePropertyDef <- ZIO.collectAll(maybeLoadedLinkValuePropertyDefFuture) @@ -2354,7 +2375,11 @@ final case class OntologyResponderV2Live( externalPropertyIri = changePropertyGuiElementRequest.propertyIri.value.toSmartIri externalOntologyIri = externalPropertyIri.getOntologyFromEntity - _ <- ontologyHelpers.checkOntologyAndEntityIrisForUpdate(externalOntologyIri, externalPropertyIri, requestingUser) + _ <- ontologyCacheHelpers.checkOntologyAndEntityIrisForUpdate( + externalOntologyIri, + externalPropertyIri, + requestingUser, + ) internalPropertyIri = externalPropertyIri.toOntologySchema(InternalSchema) internalOntologyIri = externalOntologyIri.toOntologySchema(InternalSchema) @@ -2389,7 +2414,7 @@ final case class OntologyResponderV2Live( .orElseFail(NotFoundException(s"Property ${changePropertyLabelsOrCommentsRequest.propertyIri} not found")) // Check that the ontology exists and has not been updated by another user since the client last read it. - _ <- ontologyHelpers.checkOntologyLastModificationDateBeforeUpdate( + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateBeforeUpdate( internalOntologyIri, changePropertyLabelsOrCommentsRequest.lastModificationDate, ) @@ -2424,12 +2449,12 @@ final case class OntologyResponderV2Live( _ <- triplestoreService.query(Update(updateSparql)) // Check that the ontology's last modification date was updated. - _ <- ontologyHelpers.checkOntologyLastModificationDateAfterUpdate(internalOntologyIri, currentTime) + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateAfterUpdate(internalOntologyIri, currentTime) // 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 <- ontologyHelpers.loadPropertyDefinition(internalPropertyIri) + loadedPropertyDef <- ontologyTriplestoreHelpers.loadPropertyDefinition(internalPropertyIri) unescapedNewLabelOrCommentPredicate: PredicateInfoV2 = PredicateInfoV2( @@ -2451,7 +2476,7 @@ final case class OntologyResponderV2Live( maybeLoadedLinkValuePropertyDefFuture: Option[Task[PropertyInfoContentV2]] = maybeCurrentLinkValueReadPropertyInfo.map { linkValueReadPropertyInfo => - ontologyHelpers.loadPropertyDefinition(linkValueReadPropertyInfo.entityInfoContent.propertyIri) + ontologyTriplestoreHelpers.loadPropertyDefinition(linkValueReadPropertyInfo.entityInfoContent.propertyIri) } maybeLoadedLinkValuePropertyDef <- ZIO.collectAll(maybeLoadedLinkValuePropertyDefFuture) @@ -2523,7 +2548,11 @@ final case class OntologyResponderV2Live( externalPropertyIri = changePropertyLabelsOrCommentsRequest.propertyIri externalOntologyIri = externalPropertyIri.getOntologyFromEntity - _ <- ontologyHelpers.checkOntologyAndEntityIrisForUpdate(externalOntologyIri, externalPropertyIri, requestingUser) + _ <- ontologyCacheHelpers.checkOntologyAndEntityIrisForUpdate( + externalOntologyIri, + externalPropertyIri, + requestingUser, + ) internalPropertyIri = externalPropertyIri.toOntologySchema(InternalSchema) internalOntologyIri = externalOntologyIri.toOntologySchema(InternalSchema) @@ -2557,7 +2586,7 @@ final case class OntologyResponderV2Live( .orElseFail(NotFoundException(s"Class ${changeClassLabelsOrCommentsRequest.classIri} not found")) // Check that the ontology exists and has not been updated by another user since the client last read it. - _ <- ontologyHelpers.checkOntologyLastModificationDateBeforeUpdate( + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateBeforeUpdate( internalOntologyIri, changeClassLabelsOrCommentsRequest.lastModificationDate, ) @@ -2578,11 +2607,11 @@ final case class OntologyResponderV2Live( _ <- triplestoreService.query(Update(updateSparql)) // Check that the ontology's last modification date was updated. - _ <- ontologyHelpers.checkOntologyLastModificationDateAfterUpdate(internalOntologyIri, currentTime) + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateAfterUpdate(internalOntologyIri, currentTime) // 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. - loadedClassDef <- ontologyHelpers.loadClassDefinition(internalClassIri) + loadedClassDef <- ontologyTriplestoreHelpers.loadClassDefinition(internalClassIri) unescapedNewLabelOrCommentPredicate = PredicateInfoV2( predicateIri = changeClassLabelsOrCommentsRequest.predicateToUpdate, @@ -2614,7 +2643,7 @@ final case class OntologyResponderV2Live( // Read the data back from the cache. - response <- ontologyHelpers.getClassDefinitionsFromOntologyV2( + response <- ontologyCacheHelpers.getClassDefinitionsFromOntologyV2( classIris = Set(internalClassIri), allLanguages = true, requestingUser = changeClassLabelsOrCommentsRequest.requestingUser, @@ -2627,7 +2656,8 @@ final case class OntologyResponderV2Live( externalClassIri = changeClassLabelsOrCommentsRequest.classIri externalOntologyIri = externalClassIri.getOntologyFromEntity - _ <- ontologyHelpers.checkOntologyAndEntityIrisForUpdate(externalOntologyIri, externalClassIri, requestingUser) + _ <- + ontologyCacheHelpers.checkOntologyAndEntityIrisForUpdate(externalOntologyIri, externalClassIri, requestingUser) internalClassIri = externalClassIri.toOntologySchema(InternalSchema) internalOntologyIri = externalOntologyIri.toOntologySchema(InternalSchema) @@ -2659,7 +2689,7 @@ final case class OntologyResponderV2Live( for { // Check that the ontology exists and has not been updated by another user since the client last read its metadata. - _ <- ontologyHelpers.checkOntologyLastModificationDateBeforeUpdate( + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateBeforeUpdate( internalOntologyIri, deletePropertyCommentRequest.lastModificationDate, ) @@ -2697,10 +2727,10 @@ final case class OntologyResponderV2Live( _ <- triplestoreService.query(Update(updateSparql)) // Check that the ontology's last modification date was updated. - _ <- ontologyHelpers.checkOntologyLastModificationDateAfterUpdate(internalOntologyIri, currentTime) + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateAfterUpdate(internalOntologyIri, currentTime) // Check that the update was successful. - loadedPropertyDef <- ontologyHelpers.loadPropertyDefinition(internalPropertyIri) + loadedPropertyDef <- ontologyTriplestoreHelpers.loadPropertyDefinition(internalPropertyIri) propertyDefWithoutComment: PropertyInfoContentV2 = propertyToUpdate.entityInfoContent.copy( @@ -2717,7 +2747,7 @@ final case class OntologyResponderV2Live( maybeLoadedLinkValuePropertyDefFuture: Option[Task[PropertyInfoContentV2]] = maybeLinkValueOfPropertyToUpdate.map { (linkValueReadPropertyInfo: ReadPropertyInfoV2) => - ontologyHelpers.loadPropertyDefinition(linkValueReadPropertyInfo.entityInfoContent.propertyIri) + ontologyTriplestoreHelpers.loadPropertyDefinition(linkValueReadPropertyInfo.entityInfoContent.propertyIri) } maybeLoadedLinkValuePropertyDef <- ZIO.collectAll(maybeLoadedLinkValuePropertyDefFuture) @@ -2783,7 +2813,11 @@ final case class OntologyResponderV2Live( externalPropertyIri: SmartIri = deletePropertyCommentRequest.propertyIri externalOntologyIri: SmartIri = externalPropertyIri.getOntologyFromEntity - _ <- ontologyHelpers.checkOntologyAndEntityIrisForUpdate(externalOntologyIri, externalPropertyIri, requestingUser) + _ <- ontologyCacheHelpers.checkOntologyAndEntityIrisForUpdate( + externalOntologyIri, + externalPropertyIri, + requestingUser, + ) internalPropertyIri: SmartIri = externalPropertyIri.toOntologySchema(InternalSchema) internalOntologyIri: SmartIri = externalOntologyIri.toOntologySchema(InternalSchema) @@ -2843,7 +2877,7 @@ final case class OntologyResponderV2Live( for { // Check that the ontology exists and has not been updated by another user since the client last read its metadata. - _ <- ontologyHelpers.checkOntologyLastModificationDateBeforeUpdate( + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateBeforeUpdate( internalOntologyIri, deleteClassCommentRequest.lastModificationDate, ) @@ -2862,10 +2896,10 @@ final case class OntologyResponderV2Live( _ <- triplestoreService.query(Update(updateSparql)) // Check that the ontology's last modification date was updated. - _ <- ontologyHelpers.checkOntologyLastModificationDateAfterUpdate(internalOntologyIri, currentTime) + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateAfterUpdate(internalOntologyIri, currentTime) // Check that the update was successful. - loadedClassDef <- ontologyHelpers.loadClassDefinition(internalClassIri) + loadedClassDef <- ontologyTriplestoreHelpers.loadClassDefinition(internalClassIri) classDefWithoutComment: ClassInfoContentV2 = classToUpdate.entityInfoContent.copy( @@ -2892,7 +2926,7 @@ final case class OntologyResponderV2Live( _ <- ontologyCache.cacheUpdatedOntologyWithoutUpdatingMaps(internalOntologyIri, updatedOntology) // Read the data back from the cache. - response <- ontologyHelpers.getClassDefinitionsFromOntologyV2( + response <- ontologyCacheHelpers.getClassDefinitionsFromOntologyV2( classIris = Set(internalClassIri), allLanguages = true, requestingUser = deleteClassCommentRequest.requestingUser, @@ -2906,7 +2940,8 @@ final case class OntologyResponderV2Live( externalClassIri: SmartIri = deleteClassCommentRequest.classIri externalOntologyIri: SmartIri = externalClassIri.getOntologyFromEntity - _ <- ontologyHelpers.checkOntologyAndEntityIrisForUpdate(externalOntologyIri, externalClassIri, requestingUser) + _ <- + ontologyCacheHelpers.checkOntologyAndEntityIrisForUpdate(externalOntologyIri, externalClassIri, requestingUser) internalClassIri: SmartIri = externalClassIri.toOntologySchema(InternalSchema) internalOntologyIri: SmartIri = externalOntologyIri.toOntologySchema(InternalSchema) @@ -2939,7 +2974,7 @@ final case class OntologyResponderV2Live( } yield taskResult else { // not change anything if class has no comment - ontologyHelpers.getClassDefinitionsFromOntologyV2( + ontologyCacheHelpers.getClassDefinitionsFromOntologyV2( classIris = Set(internalClassIri), allLanguages = true, requestingUser = deleteClassCommentRequest.requestingUser, @@ -2952,7 +2987,8 @@ final case class OntologyResponderV2Live( object OntologyResponderV2Live { val layer: URLayer[ AppConfig & CardinalityHandler & CardinalityService & IriService & KnoraProjectService & MessageRelay & - OntologyCache & OntologyHelpers & OntologyRepo & StringFormatter & TriplestoreService, + OntologyCache & OntologyTriplestoreHelpers & OntologyCacheHelpers & OntologyRepo & StringFormatter & + TriplestoreService, OntologyResponderV2, ] = ZLayer.fromZIO { for { @@ -2962,11 +2998,12 @@ object OntologyResponderV2Live { is <- ZIO.service[IriService] kr <- ZIO.service[KnoraProjectService] oc <- ZIO.service[OntologyCache] - oh <- ZIO.service[OntologyHelpers] + oth <- ZIO.service[OntologyTriplestoreHelpers] + och <- ZIO.service[OntologyCacheHelpers] or <- ZIO.service[OntologyRepo] sf <- ZIO.service[StringFormatter] ts <- ZIO.service[TriplestoreService] - responder = OntologyResponderV2Live(ac, ch, cs, is, oc, oh, or, kr, ts)(sf) + responder = OntologyResponderV2Live(ac, ch, cs, is, oc, och, oth, or, kr, ts)(sf) _ <- ZIO.serviceWithZIO[MessageRelay](_.subscribe(responder)) } yield responder } diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/CardinalityHandler.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/CardinalityHandler.scala index 9d86460715..0cc92dfd47 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/CardinalityHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/CardinalityHandler.scala @@ -32,7 +32,8 @@ import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Update final case class CardinalityHandler( ontologyCache: OntologyCache, triplestoreService: TriplestoreService, - ontologyHelpers: OntologyHelpers, + ontologyCacheHelpers: OntologyCacheHelpers, + ontologyTriplestoreHelpers: OntologyTriplestoreHelpers, )(implicit val stringFormatter: StringFormatter) { /** @@ -51,7 +52,7 @@ final case class CardinalityHandler( cacheData <- ontologyCache.getCacheData _ <- // Check that the ontology exists and has not been updated by another user since the client last read it. - ontologyHelpers.checkOntologyLastModificationDateBeforeUpdate( + ontologyTriplestoreHelpers.checkOntologyLastModificationDateBeforeUpdate( internalOntologyIri = internalOntologyIri, expectedLastModificationDate = deleteCardinalitiesFromClassRequest.lastModificationDate, ) @@ -191,7 +192,7 @@ final case class CardinalityHandler( ontology = cacheData.ontologies(internalOntologyIri) // Check that the ontology exists and has not been updated by another user since the client last read it. - _ <- ontologyHelpers.checkOntologyLastModificationDateBeforeUpdate( + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateBeforeUpdate( internalOntologyIri, deleteCardinalitiesFromClassRequest.lastModificationDate, ) @@ -319,11 +320,11 @@ final case class CardinalityHandler( _ <- triplestoreService.query(Update(updateSparql)) // Check that the ontology's last modification date was updated. - _ <- ontologyHelpers.checkOntologyLastModificationDateAfterUpdate(internalOntologyIri, currentTime) + _ <- ontologyTriplestoreHelpers.checkOntologyLastModificationDateAfterUpdate(internalOntologyIri, currentTime) // Check that the data that was saved corresponds to the data that was submitted. - loadedClassDef <- ontologyHelpers.loadClassDefinition(internalClassIri) + loadedClassDef <- ontologyTriplestoreHelpers.loadClassDefinition(internalClassIri) _ = if (loadedClassDef != newInternalClassDefWithLinkValueProps) { throw InconsistentRepositoryDataException( @@ -345,7 +346,7 @@ final case class CardinalityHandler( // Read the data back from the cache. - response <- ontologyHelpers.getClassDefinitionsFromOntologyV2( + response <- ontologyCacheHelpers.getClassDefinitionsFromOntologyV2( Set(internalClassIri), allLanguages = true, deleteCardinalitiesFromClassRequest.requestingUser, diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/OntologyCacheHelpers.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/OntologyCacheHelpers.scala new file mode 100644 index 0000000000..5d7a1b459e --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/OntologyCacheHelpers.scala @@ -0,0 +1,367 @@ +/* + * Copyright © 2021 - 2024 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.responders.v2.ontology + +import zio.* + +import scala.collection.immutable + +import dsp.errors.* +import org.knora.webapi.* +import org.knora.webapi.messages.SmartIri +import org.knora.webapi.messages.util.ErrorHandlingMap +import org.knora.webapi.messages.v2.responder.ontologymessages.* +import org.knora.webapi.slice.admin.domain.model.User +import org.knora.webapi.slice.ontology.domain.model.Cardinality.* +import org.knora.webapi.slice.ontology.repo.service.OntologyCache + +trait OntologyCacheHelpers { + + /** + * Requests information about OWL classes in a single ontology. + * + * @param classIris the IRIs (internal or external) of the classes to query for. + * @param requestingUser the user making the request. + * @return a [[ReadOntologyV2]]. + */ + def getClassDefinitionsFromOntologyV2( + classIris: Set[SmartIri], + allLanguages: Boolean, + requestingUser: User, + ): Task[ReadOntologyV2] + + /** + * Given a list of resource IRIs and a list of property IRIs (ontology entities), returns an [[EntityInfoGetResponseV2]] describing both resource and property entities. + * + * @param classIris the IRIs of the resource entities to be queried. + * @param propertyIris the IRIs of the property entities to be queried. + * @param requestingUser the user making the request. + * @return an [[EntityInfoGetResponseV2]]. + */ + def getEntityInfoResponseV2( + classIris: Set[SmartIri] = Set.empty[SmartIri], + propertyIris: Set[SmartIri] = Set.empty[SmartIri], + requestingUser: User, + ): Task[EntityInfoGetResponseV2] + + /** + * Before an update of an ontology entity, checks that the entity's external IRI, and that of its ontology, + * are valid, and checks that the user has permission to update the ontology. + * + * @param externalOntologyIri the external IRI of the ontology. + * @param externalEntityIri the external IRI of the entity. + * @param requestingUser the user making the request. + */ + final def checkOntologyAndEntityIrisForUpdate( + externalOntologyIri: SmartIri, + externalEntityIri: SmartIri, + requestingUser: User, + ): Task[Unit] = + for { + _ <- OntologyHelpers.checkExternalOntologyIriForUpdate(externalOntologyIri) + _ <- OntologyHelpers.checkExternalEntityIriForUpdate(externalEntityIri) + _ <- checkPermissionsForOntologyUpdate( + internalOntologyIri = externalOntologyIri.toOntologySchema(InternalSchema), + requestingUser = requestingUser, + ) + } yield () + + /** + * Throws an exception if the requesting user does not have permission to update an ontology. + * + * @param internalOntologyIri the internal IRI of the ontology. + * @param requestingUser the user making the request. + * @return the project IRI. + */ + def checkPermissionsForOntologyUpdate(internalOntologyIri: SmartIri, requestingUser: User): Task[SmartIri] + + /** + * Checks whether the requesting user has permission to update an ontology. + * + * @param internalOntologyIri the internal IRI of the ontology. + * @param requestingUser the user making the request. + * @return `true` if the user has permission to update the ontology + */ + def canUserUpdateOntology(internalOntologyIri: SmartIri, requestingUser: User): Task[Boolean] +} + +final case class OntologyCacheHelpersLive( + ontologyCache: OntologyCache, +) extends OntologyCacheHelpers { + + override def getClassDefinitionsFromOntologyV2( + classIris: Set[SmartIri], + allLanguages: Boolean, + requestingUser: User, + ): Task[ReadOntologyV2] = + for { + cacheData <- ontologyCache.getCacheData + + ontologyIris = classIris.map(_.getOntologyFromEntity) + + _ = if (ontologyIris.size != 1) { + throw BadRequestException(s"Only one ontology may be queried per request") + } + + classInfoResponse <- getEntityInfoResponseV2(classIris = classIris, requestingUser = requestingUser) + internalOntologyIri = ontologyIris.head.toOntologySchema(InternalSchema) + + // Are we returning data in the user's preferred language, or in all available languages? + userLang = + if (!allLanguages) { + // Just the user's preferred language. + Some(requestingUser.lang) + } else { + // All available languages. + None + } + } yield ReadOntologyV2( + ontologyMetadata = cacheData.ontologies(internalOntologyIri).ontologyMetadata, + classes = classInfoResponse.classInfoMap, + userLang = userLang, + ) + + override def getEntityInfoResponseV2( + classIris: Set[SmartIri] = Set.empty[SmartIri], + propertyIris: Set[SmartIri] = Set.empty[SmartIri], + requestingUser: User, + ): Task[EntityInfoGetResponseV2] = { + for { + cacheData <- ontologyCache.getCacheData + + // See if any of the requested entities are not Knora entities. + + nonKnoraEntities = (classIris ++ propertyIris).filter(!_.isKnoraEntityIri) + + _ = if (nonKnoraEntities.nonEmpty) { + throw BadRequestException( + s"Some requested entities are not Knora entities: ${nonKnoraEntities.mkString(", ")}", + ) + } + + // See if any of the requested entities are unavailable in the requested schema. + + classesUnavailableInSchema = classIris.foldLeft(Set.empty[SmartIri]) { case (acc, classIri) => + // Is this class IRI hard-coded in the requested schema? + if ( + KnoraBaseToApiV2SimpleTransformationRules.externalClassesToAdd + .contains(classIri) || + KnoraBaseToApiV2ComplexTransformationRules.externalClassesToAdd + .contains(classIri) + ) { + // Yes, so it's available. + acc + } else { + // No. Is it among the classes removed from the internal ontology in the requested schema? + classIri.getOntologySchema.get match { + case apiV2Schema: ApiV2Schema => + val internalClassIri = + classIri.toOntologySchema(InternalSchema) + val knoraBaseClassesToRemove = OntologyTransformationRules + .getTransformationRules( + apiV2Schema, + ) + .internalClassesToRemove + + if (knoraBaseClassesToRemove.contains(internalClassIri)) { + // Yes. Include it in the set of unavailable classes. + acc + classIri + } else { + // No. It's available. + acc + } + + case InternalSchema => acc + } + } + } + + propertiesUnavailableInSchema = propertyIris.foldLeft(Set.empty[SmartIri]) { case (acc, propertyIri) => + // Is this property IRI hard-coded in the requested schema? + if ( + KnoraBaseToApiV2SimpleTransformationRules.externalPropertiesToAdd + .contains(propertyIri) || + KnoraBaseToApiV2ComplexTransformationRules.externalPropertiesToAdd + .contains(propertyIri) + ) { + // Yes, so it's available. + acc + } else { + // No. See if it's available in the requested schema. + propertyIri.getOntologySchema.get match { + case apiV2Schema: ApiV2Schema => + val internalPropertyIri = + propertyIri.toOntologySchema(InternalSchema) + + // If it's a link value property and it's requested in the simple schema, it's unavailable. + if ( + apiV2Schema == ApiV2Simple && OntologyHelpers + .isLinkValueProp( + internalPropertyIri, + cacheData, + ) + ) { + acc + propertyIri + } else { + // Is it among the properties removed from the internal ontology in the requested schema? + + val knoraBasePropertiesToRemove = + OntologyTransformationRules + .getTransformationRules( + apiV2Schema, + ) + .internalPropertiesToRemove + + if ( + knoraBasePropertiesToRemove.contains( + internalPropertyIri, + ) + ) { + // Yes. Include it in the set of unavailable properties. + acc + propertyIri + } else { + // No. It's available. + acc + } + } + + case InternalSchema => acc + } + } + } + + entitiesUnavailableInSchema = classesUnavailableInSchema ++ propertiesUnavailableInSchema + + _ <- ZIO.when(entitiesUnavailableInSchema.nonEmpty) { + ZIO.fail( + NotFoundException( + s"Some requested entities were not found: ${entitiesUnavailableInSchema.mkString(", ")}", + ), + ) + } + + // See if any of the requested entities are hard-coded for knora-api. + hardCodedExternalClassesAvailable = + KnoraBaseToApiV2SimpleTransformationRules.externalClassesToAdd.view + .filterKeys(classIris) + .toMap ++ + KnoraBaseToApiV2ComplexTransformationRules.externalClassesToAdd.view.filterKeys(classIris).toMap + + hardCodedExternalPropertiesAvailable = + KnoraBaseToApiV2SimpleTransformationRules.externalPropertiesToAdd.view + .filterKeys(propertyIris) + .toMap ++ + KnoraBaseToApiV2ComplexTransformationRules.externalPropertiesToAdd.view.filterKeys(propertyIris) + + // Convert the remaining external entity IRIs to internal ones. + + internalToExternalClassIris = + (classIris -- hardCodedExternalClassesAvailable.keySet) + .map(externalIri => externalIri.toOntologySchema(InternalSchema) -> externalIri) + .toMap + internalToExternalPropertyIris = + (propertyIris -- hardCodedExternalPropertiesAvailable.keySet) + .map(externalIri => externalIri.toOntologySchema(InternalSchema) -> externalIri) + .toMap + + classIrisForCache = internalToExternalClassIris.keySet + propertyIrisForCache = internalToExternalPropertyIris.keySet + + // Get the entities that are available in the ontology cache. + + classOntologiesForCache = + cacheData.ontologies.view.filterKeys(classIrisForCache.map(_.getOntologyFromEntity)).toMap.values + + propertyOntologiesForCache = + cacheData.ontologies.view.filterKeys(propertyIrisForCache.map(_.getOntologyFromEntity)).toMap.values + + classesAvailableFromCache = + classOntologiesForCache.flatMap(ontology => ontology.classes.view.filterKeys(classIrisForCache).toMap).toMap + + propertiesAvailableFromCache = propertyOntologiesForCache.flatMap { ontology => + ontology.properties.view.filterKeys(propertyIrisForCache).toMap + }.toMap + + allClassesAvailable = classesAvailableFromCache ++ hardCodedExternalClassesAvailable + allPropertiesAvailable = propertiesAvailableFromCache ++ hardCodedExternalPropertiesAvailable + + // See if any entities are missing. + + allExternalClassIrisAvailable = allClassesAvailable.keySet.map { classIri => + if (classIri.getOntologySchema.contains(InternalSchema)) { + internalToExternalClassIris(classIri) + } else { + classIri + } + } + + allExternalPropertyIrisAvailable = allPropertiesAvailable.keySet.map { propertyIri => + if (propertyIri.getOntologySchema.contains(InternalSchema)) { + internalToExternalPropertyIris(propertyIri) + } else { + propertyIri + } + } + + missingClasses = classIris -- allExternalClassIrisAvailable + missingProperties = propertyIris -- allExternalPropertyIrisAvailable + + missingEntities = missingClasses ++ missingProperties + + _ <- ZIO.when(missingEntities.nonEmpty) { + ZIO.fail(NotFoundException(s"Some requested entities were not found: ${missingEntities.mkString(", ")}")) + } + + response = EntityInfoGetResponseV2( + classInfoMap = new ErrorHandlingMap(allClassesAvailable, key => s"Resource class $key not found"), + propertyInfoMap = new ErrorHandlingMap(allPropertiesAvailable, key => s"Property $key not found"), + ) + } yield response + } + + override def checkPermissionsForOntologyUpdate( + internalOntologyIri: SmartIri, + requestingUser: User, + ): Task[SmartIri] = + for { + cacheData <- ontologyCache.getCacheData + + projectIri = + cacheData.ontologies + .getOrElse( + internalOntologyIri, + throw NotFoundException(s"Ontology ${internalOntologyIri.toOntologySchema(ApiV2Complex)} not found"), + ) + .ontologyMetadata + .projectIri + .get + + _ = if ( + !requestingUser.permissions.isProjectAdmin(projectIri.toString) && !requestingUser.permissions.isSystemAdmin + ) { + // not a project or system admin + throw ForbiddenException("Ontologies can be modified only by a project or system admin.") + } + + } yield projectIri + + override def canUserUpdateOntology(internalOntologyIri: SmartIri, requestingUser: User): Task[Boolean] = + for { + cacheData <- ontologyCache.getCacheData + + projectIri = + cacheData.ontologies + .getOrElse( + internalOntologyIri, + throw NotFoundException(s"Ontology ${internalOntologyIri.toOntologySchema(ApiV2Complex)} not found"), + ) + .ontologyMetadata + .projectIri + .get + } yield requestingUser.permissions.isProjectAdmin(projectIri.toString) || requestingUser.permissions.isSystemAdmin +} + +object OntologyCacheHelpersLive { val layer = ZLayer.derive[OntologyCacheHelpersLive] } diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/OntologyHelpers.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/OntologyHelpers.scala index 1c2f58d036..b113491121 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/OntologyHelpers.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/OntologyHelpers.scala @@ -21,117 +21,15 @@ import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.ValuesValidator import org.knora.webapi.messages.store.triplestoremessages.* -import org.knora.webapi.messages.twirl.queries.sparql -import org.knora.webapi.messages.util.ErrorHandlingMap import org.knora.webapi.messages.util.rdf.SparqlSelectResult import org.knora.webapi.messages.util.rdf.VariableResultsRow import org.knora.webapi.messages.v2.responder.ontologymessages.* import org.knora.webapi.messages.v2.responder.ontologymessages.OwlCardinality.* import org.knora.webapi.messages.v2.responder.standoffmessages.StandoffDataTypeClasses -import org.knora.webapi.slice.admin.domain.model.User -import org.knora.webapi.slice.admin.domain.service.KnoraProjectRepo import org.knora.webapi.slice.ontology.domain.model.Cardinality.* import org.knora.webapi.slice.ontology.repo.model.OntologyCacheData -import org.knora.webapi.slice.ontology.repo.service.OntologyCache -import org.knora.webapi.store.triplestore.api.TriplestoreService -import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Construct -import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Select -trait OntologyHelpers { - - /** - * Reads an ontology's metadata. - * - * @param internalOntologyIri the ontology's internal IRI. - * @return an [[OntologyMetadataV2]], or [[None]] if the ontology is not found. - */ - def loadOntologyMetadata(internalOntologyIri: SmartIri): Task[Option[OntologyMetadataV2]] - - /** - * Gets the set of subjects that refer to an ontology or its entities. - * - * @param ontology the ontology. - * @return the set of subjects that refer to the ontology or its entities. - */ - def getSubjectsUsingOntology(ontology: ReadOntologyV2): Task[Set[IRI]] - - /** - * Loads a property definition from the triplestore and converts it to a [[PropertyInfoContentV2]]. - * - * @param propertyIri the IRI of the property to be loaded. - * @return a [[PropertyInfoContentV2]] representing the property definition. - */ - def loadPropertyDefinition(propertyIri: SmartIri): Task[PropertyInfoContentV2] - - /** - * Given a list of resource IRIs and a list of property IRIs (ontology entities), returns an [[EntityInfoGetResponseV2]] describing both resource and property entities. - * - * @param classIris the IRIs of the resource entities to be queried. - * @param propertyIris the IRIs of the property entities to be queried. - * @param requestingUser the user making the request. - * @return an [[EntityInfoGetResponseV2]]. - */ - def getEntityInfoResponseV2( - classIris: Set[SmartIri] = Set.empty[SmartIri], - propertyIris: Set[SmartIri] = Set.empty[SmartIri], - requestingUser: User, - ): Task[EntityInfoGetResponseV2] - - /** - * Requests information about OWL classes in a single ontology. - * - * @param classIris the IRIs (internal or external) of the classes to query for. - * @param requestingUser the user making the request. - * @return a [[ReadOntologyV2]]. - */ - def getClassDefinitionsFromOntologyV2( - classIris: Set[SmartIri], - allLanguages: Boolean, - requestingUser: User, - ): Task[ReadOntologyV2] - - /** - * Loads a class definition from the triplestore and converts it to a [[ClassInfoContentV2]]. - * - * @param classIri the IRI of the class to be loaded. - * @return a [[ClassInfoContentV2]] representing the class definition. - */ - def loadClassDefinition(classIri: SmartIri): Task[ClassInfoContentV2] - - /** - * Checks that the last modification date of an ontology is the same as the one we expect it to be. If not, return - * an error message fitting for the "before update" case. - * - * @param internalOntologyIri the internal IRI of the ontology. - * @param expectedLastModificationDate the last modification date that should now be attached to the ontology. - * @return a failed Future if the expected last modification date is not found. - */ - def checkOntologyLastModificationDateBeforeUpdate( - internalOntologyIri: SmartIri, - expectedLastModificationDate: Instant, - ): Task[Unit] - - /** - * Checks that the last modification date of an ontology is the same as the one we expect it to be. If not, return - * an error message fitting for the "after update" case. - * - * @param internalOntologyIri the internal IRI of the ontology. - * @param expectedLastModificationDate the last modification date that should now be attached to the ontology. - * @return a failed Future if the expected last modification date is not found. - */ - def checkOntologyLastModificationDateAfterUpdate( - internalOntologyIri: SmartIri, - expectedLastModificationDate: Instant, - ): Task[Unit] - - /** - * Checks whether the requesting user has permission to update an ontology. - * - * @param internalOntologyIri the internal IRI of the ontology. - * @param requestingUser the user making the request. - * @return `true` if the user has permission to update the ontology - */ - def canUserUpdateOntology(internalOntologyIri: SmartIri, requestingUser: User): Task[Boolean] +object OntologyHelpers { /** * Checks whether an ontology IRI is valid for an update. @@ -139,33 +37,31 @@ trait OntologyHelpers { * @param externalOntologyIri the external IRI of the ontology. * @return a failed Future if the IRI is not valid for an update. */ - def checkExternalOntologyIriForUpdate(externalOntologyIri: SmartIri): Task[Unit] - - /** - * Before an update of an ontology entity, checks that the entity's external IRI, and that of its ontology, - * are valid, and checks that the user has permission to update the ontology. - * - * @param externalOntologyIri the external IRI of the ontology. - * @param externalEntityIri the external IRI of the entity. - * @param requestingUser the user making the request. - */ - def checkOntologyAndEntityIrisForUpdate( - externalOntologyIri: SmartIri, - externalEntityIri: SmartIri, - requestingUser: User, - ): Task[Unit] + def checkExternalOntologyIriForUpdate(externalOntologyIri: SmartIri): Task[Unit] = + if (!externalOntologyIri.isKnoraOntologyIri) { + ZIO.fail(BadRequestException(s"Invalid ontology IRI for request: $externalOntologyIri}")) + } else if (!externalOntologyIri.getOntologySchema.contains(ApiV2Complex)) { + ZIO.fail(BadRequestException(s"Invalid ontology schema for request: $externalOntologyIri")) + } else if (externalOntologyIri.isKnoraBuiltInDefinitionIri) { + ZIO.fail(BadRequestException(s"Ontology $externalOntologyIri cannot be modified via the Knora API")) + } else { + ZIO.succeed(()) + } /** - * Throws an exception if the requesting user does not have permission to update an ontology. + * Checks whether an entity IRI is valid for an update. * - * @param internalOntologyIri the internal IRI of the ontology. - * @param requestingUser the user making the request. - * @return the project IRI. + * @param externalEntityIri the external IRI of the entity. + * @return a failed Future if the entity IRI is not valid for an update, or is not from the specified ontology. */ - def checkPermissionsForOntologyUpdate(internalOntologyIri: SmartIri, requestingUser: User): Task[SmartIri] -} - -object OntologyHelpers { + def checkExternalEntityIriForUpdate(externalEntityIri: SmartIri): Task[Unit] = + if (!externalEntityIri.isKnoraApiV2EntityIri) { + ZIO.fail(BadRequestException(s"Invalid entity IRI for request: $externalEntityIri")) + } else if (!externalEntityIri.getOntologySchema.contains(ApiV2Complex)) { + ZIO.fail(BadRequestException(s"Invalid ontology schema for request: $externalEntityIri")) + } else { + ZIO.succeed(()) + } /** * Constructs a map of property IRIs to [[ReadPropertyInfoV2]] instances, based on property definitions loaded from the @@ -1559,617 +1455,3 @@ object OntologyHelpers { ) } } - -final case class OntologyHelpersLive( - triplestore: TriplestoreService, - ontologyCache: OntologyCache, -)(implicit val stringFormatter: StringFormatter) - extends OntologyHelpers { - - /** - * Reads an ontology's metadata. - * - * @param internalOntologyIri the ontology's internal IRI. - * @return an [[OntologyMetadataV2]], or [[None]] if the ontology is not found. - */ - override def loadOntologyMetadata(internalOntologyIri: SmartIri): Task[Option[OntologyMetadataV2]] = { - for { - _ <- ZIO.when(!internalOntologyIri.getOntologySchema.contains(InternalSchema)) { - ZIO.fail(AssertionException(s"Expected an internal ontology IRI: $internalOntologyIri")) - } - getOntologyInfoResponse <- triplestore.query(Construct(sparql.v2.txt.getOntologyInfo(internalOntologyIri))) - - metadata: Option[OntologyMetadataV2] = - if (getOntologyInfoResponse.statements.isEmpty) { - None - } else { - getOntologyInfoResponse.statements.get( - internalOntologyIri.toString, - ) match { - case Some(statements: Seq[(IRI, String)]) => - val statementMap: Map[IRI, Seq[String]] = statements.groupBy { case (pred, _) => - pred - }.map { case (pred, predStatements) => - pred -> predStatements.map { case (_, obj) => - obj - } - } - - val projectIris: Seq[String] = statementMap.getOrElse( - OntologyConstants.KnoraBase.AttachedToProject, - throw InconsistentRepositoryDataException( - 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) { - throw InconsistentRepositoryDataException( - s"Ontology $internalOntologyIri has more than one knora-base:attachedToProject", - ) - } else { - projectIris.head.toSmartIri - } - - if (!internalOntologyIri.isKnoraBuiltInDefinitionIri) { - if (projectIri.toString == KnoraProjectRepo.builtIn.SystemProject.id.value) { - throw InconsistentRepositoryDataException( - s"Ontology $internalOntologyIri cannot be in project ${KnoraProjectRepo.builtIn.SystemProject.id.value}", - ) - } - - if ( - internalOntologyIri.isKnoraSharedDefinitionIri && projectIri.toString != OntologyConstants.KnoraAdmin.DefaultSharedOntologiesProject - ) { - throw InconsistentRepositoryDataException( - s"Shared ontology $internalOntologyIri must be in project ${OntologyConstants.KnoraAdmin.DefaultSharedOntologiesProject}", - ) - } - } - - val label: String = if (labels.size > 1) { - throw InconsistentRepositoryDataException( - s"Ontology $internalOntologyIri has more than one rdfs:label", - ) - } else if (labels.isEmpty) { - internalOntologyIri.getOntologyName - } else { - labels.head - } - - val comment: Option[String] = if (comments.size > 1) { - throw InconsistentRepositoryDataException( - s"Ontology $internalOntologyIri has more than one rdfs:comment", - ) - } else comments.headOption - - val lastModificationDate: Option[Instant] = - if (lastModDates.size > 1) { - throw InconsistentRepositoryDataException( - s"Ontology $internalOntologyIri has more than one ${OntologyConstants.KnoraBase.LastModificationDate}", - ) - } else if (lastModDates.isEmpty) { - None - } else { - val dateStr = lastModDates.head - Some( - ValuesValidator - .xsdDateTimeStampToInstant(dateStr) - .getOrElse( - throw InconsistentRepositoryDataException( - s"Invalid ${OntologyConstants.KnoraBase.LastModificationDate}: $dateStr", - ), - ), - ) - } - - Some( - OntologyMetadataV2( - ontologyIri = internalOntologyIri, - projectIri = Some(projectIri), - label = Some(label), - comment = comment, - lastModificationDate = lastModificationDate, - ), - ) - - case None => None - } - } - } yield metadata - } - - /** - * Gets the set of subjects that refer to an ontology or its entities. - * - * @param ontology the ontology. - * @return the set of subjects that refer to the ontology or its entities. - */ - override def getSubjectsUsingOntology(ontology: ReadOntologyV2): Task[Set[IRI]] = - for { - isOntologyUsedSparql <- ZIO.attempt( - sparql.v2.txt - .isOntologyUsed( - ontologyNamedGraphIri = ontology.ontologyMetadata.ontologyIri, - classIris = ontology.classes.keySet, - propertyIris = ontology.properties.keySet, - ), - ) - - isOntologyUsedResponse <- triplestore.query(Select(isOntologyUsedSparql)) - - subjects = isOntologyUsedResponse.getColOrThrow("s").toSet - } yield subjects - - /** - * Before an update of an ontology entity, checks that the entity's external IRI, and that of its ontology, - * are valid, and checks that the user has permission to update the ontology. - * - * @param externalOntologyIri the external IRI of the ontology. - * @param externalEntityIri the external IRI of the entity. - * @param requestingUser the user making the request. - */ - override def checkOntologyAndEntityIrisForUpdate( - externalOntologyIri: SmartIri, - externalEntityIri: SmartIri, - requestingUser: User, - ): Task[Unit] = - for { - _ <- checkExternalOntologyIriForUpdate(externalOntologyIri) - _ <- checkExternalEntityIriForUpdate(externalEntityIri) - _ <- checkPermissionsForOntologyUpdate( - internalOntologyIri = externalOntologyIri.toOntologySchema(InternalSchema), - requestingUser = requestingUser, - ) - } yield () - - /** - * Loads a property definition from the triplestore and converts it to a [[PropertyInfoContentV2]]. - * - * @param propertyIri the IRI of the property to be loaded. - * @return a [[PropertyInfoContentV2]] representing the property definition. - */ - override def loadPropertyDefinition(propertyIri: SmartIri): Task[PropertyInfoContentV2] = - triplestore - .query(Construct(sparql.v2.txt.getPropertyDefinition(propertyIri))) - .flatMap(_.asExtended) - .map(constructResponse => OntologyHelpers.constructResponseToPropertyDefinition(propertyIri, constructResponse)) - - /** - * Given a list of resource IRIs and a list of property IRIs (ontology entities), returns an [[EntityInfoGetResponseV2]] describing both resource and property entities. - * - * @param classIris the IRIs of the resource entities to be queried. - * @param propertyIris the IRIs of the property entities to be queried. - * @param requestingUser the user making the request. - * @return an [[EntityInfoGetResponseV2]]. - */ - override def getEntityInfoResponseV2( - classIris: Set[SmartIri] = Set.empty[SmartIri], - propertyIris: Set[SmartIri] = Set.empty[SmartIri], - requestingUser: User, - ): Task[EntityInfoGetResponseV2] = { - for { - cacheData <- ontologyCache.getCacheData - - // See if any of the requested entities are not Knora entities. - - nonKnoraEntities = (classIris ++ propertyIris).filter(!_.isKnoraEntityIri) - - _ = if (nonKnoraEntities.nonEmpty) { - throw BadRequestException( - s"Some requested entities are not Knora entities: ${nonKnoraEntities.mkString(", ")}", - ) - } - - // See if any of the requested entities are unavailable in the requested schema. - - classesUnavailableInSchema = classIris.foldLeft(Set.empty[SmartIri]) { case (acc, classIri) => - // Is this class IRI hard-coded in the requested schema? - if ( - KnoraBaseToApiV2SimpleTransformationRules.externalClassesToAdd - .contains(classIri) || - KnoraBaseToApiV2ComplexTransformationRules.externalClassesToAdd - .contains(classIri) - ) { - // Yes, so it's available. - acc - } else { - // No. Is it among the classes removed from the internal ontology in the requested schema? - classIri.getOntologySchema.get match { - case apiV2Schema: ApiV2Schema => - val internalClassIri = - classIri.toOntologySchema(InternalSchema) - val knoraBaseClassesToRemove = OntologyTransformationRules - .getTransformationRules( - apiV2Schema, - ) - .internalClassesToRemove - - if (knoraBaseClassesToRemove.contains(internalClassIri)) { - // Yes. Include it in the set of unavailable classes. - acc + classIri - } else { - // No. It's available. - acc - } - - case InternalSchema => acc - } - } - } - - propertiesUnavailableInSchema = propertyIris.foldLeft(Set.empty[SmartIri]) { case (acc, propertyIri) => - // Is this property IRI hard-coded in the requested schema? - if ( - KnoraBaseToApiV2SimpleTransformationRules.externalPropertiesToAdd - .contains(propertyIri) || - KnoraBaseToApiV2ComplexTransformationRules.externalPropertiesToAdd - .contains(propertyIri) - ) { - // Yes, so it's available. - acc - } else { - // No. See if it's available in the requested schema. - propertyIri.getOntologySchema.get match { - case apiV2Schema: ApiV2Schema => - val internalPropertyIri = - propertyIri.toOntologySchema(InternalSchema) - - // If it's a link value property and it's requested in the simple schema, it's unavailable. - if ( - apiV2Schema == ApiV2Simple && OntologyHelpers - .isLinkValueProp( - internalPropertyIri, - cacheData, - ) - ) { - acc + propertyIri - } else { - // Is it among the properties removed from the internal ontology in the requested schema? - - val knoraBasePropertiesToRemove = - OntologyTransformationRules - .getTransformationRules( - apiV2Schema, - ) - .internalPropertiesToRemove - - if ( - knoraBasePropertiesToRemove.contains( - internalPropertyIri, - ) - ) { - // Yes. Include it in the set of unavailable properties. - acc + propertyIri - } else { - // No. It's available. - acc - } - } - - case InternalSchema => acc - } - } - } - - entitiesUnavailableInSchema = classesUnavailableInSchema ++ propertiesUnavailableInSchema - - _ <- ZIO.when(entitiesUnavailableInSchema.nonEmpty) { - ZIO.fail( - NotFoundException( - s"Some requested entities were not found: ${entitiesUnavailableInSchema.mkString(", ")}", - ), - ) - } - - // See if any of the requested entities are hard-coded for knora-api. - hardCodedExternalClassesAvailable = - KnoraBaseToApiV2SimpleTransformationRules.externalClassesToAdd.view - .filterKeys(classIris) - .toMap ++ - KnoraBaseToApiV2ComplexTransformationRules.externalClassesToAdd.view.filterKeys(classIris).toMap - - hardCodedExternalPropertiesAvailable = - KnoraBaseToApiV2SimpleTransformationRules.externalPropertiesToAdd.view - .filterKeys(propertyIris) - .toMap ++ - KnoraBaseToApiV2ComplexTransformationRules.externalPropertiesToAdd.view.filterKeys(propertyIris) - - // Convert the remaining external entity IRIs to internal ones. - - internalToExternalClassIris = - (classIris -- hardCodedExternalClassesAvailable.keySet) - .map(externalIri => externalIri.toOntologySchema(InternalSchema) -> externalIri) - .toMap - internalToExternalPropertyIris = - (propertyIris -- hardCodedExternalPropertiesAvailable.keySet) - .map(externalIri => externalIri.toOntologySchema(InternalSchema) -> externalIri) - .toMap - - classIrisForCache = internalToExternalClassIris.keySet - propertyIrisForCache = internalToExternalPropertyIris.keySet - - // Get the entities that are available in the ontology cache. - - classOntologiesForCache = - cacheData.ontologies.view.filterKeys(classIrisForCache.map(_.getOntologyFromEntity)).toMap.values - - propertyOntologiesForCache = - cacheData.ontologies.view.filterKeys(propertyIrisForCache.map(_.getOntologyFromEntity)).toMap.values - - classesAvailableFromCache = - classOntologiesForCache.flatMap(ontology => ontology.classes.view.filterKeys(classIrisForCache).toMap).toMap - - propertiesAvailableFromCache = propertyOntologiesForCache.flatMap { ontology => - ontology.properties.view.filterKeys(propertyIrisForCache).toMap - }.toMap - - allClassesAvailable = classesAvailableFromCache ++ hardCodedExternalClassesAvailable - allPropertiesAvailable = propertiesAvailableFromCache ++ hardCodedExternalPropertiesAvailable - - // See if any entities are missing. - - allExternalClassIrisAvailable = allClassesAvailable.keySet.map { classIri => - if (classIri.getOntologySchema.contains(InternalSchema)) { - internalToExternalClassIris(classIri) - } else { - classIri - } - } - - allExternalPropertyIrisAvailable = allPropertiesAvailable.keySet.map { propertyIri => - if (propertyIri.getOntologySchema.contains(InternalSchema)) { - internalToExternalPropertyIris(propertyIri) - } else { - propertyIri - } - } - - missingClasses = classIris -- allExternalClassIrisAvailable - missingProperties = propertyIris -- allExternalPropertyIrisAvailable - - missingEntities = missingClasses ++ missingProperties - - _ <- ZIO.when(missingEntities.nonEmpty) { - ZIO.fail(NotFoundException(s"Some requested entities were not found: ${missingEntities.mkString(", ")}")) - } - - response = EntityInfoGetResponseV2( - classInfoMap = new ErrorHandlingMap(allClassesAvailable, key => s"Resource class $key not found"), - propertyInfoMap = new ErrorHandlingMap(allPropertiesAvailable, key => s"Property $key not found"), - ) - } yield response - } - - /** - * Requests information about OWL classes in a single ontology. - * - * @param classIris the IRIs (internal or external) of the classes to query for. - * @param requestingUser the user making the request. - * @return a [[ReadOntologyV2]]. - */ - override def getClassDefinitionsFromOntologyV2( - classIris: Set[SmartIri], - allLanguages: Boolean, - requestingUser: User, - ): Task[ReadOntologyV2] = - for { - cacheData <- ontologyCache.getCacheData - - ontologyIris = classIris.map(_.getOntologyFromEntity) - - _ = if (ontologyIris.size != 1) { - throw BadRequestException(s"Only one ontology may be queried per request") - } - - classInfoResponse <- getEntityInfoResponseV2(classIris = classIris, requestingUser = requestingUser) - internalOntologyIri = ontologyIris.head.toOntologySchema(InternalSchema) - - // Are we returning data in the user's preferred language, or in all available languages? - userLang = - if (!allLanguages) { - // Just the user's preferred language. - Some(requestingUser.lang) - } else { - // All available languages. - None - } - } yield ReadOntologyV2( - ontologyMetadata = cacheData.ontologies(internalOntologyIri).ontologyMetadata, - classes = classInfoResponse.classInfoMap, - userLang = userLang, - ) - - /** - * Loads a class definition from the triplestore and converts it to a [[ClassInfoContentV2]]. - * - * @param classIri the IRI of the class to be loaded. - * @return a [[ClassInfoContentV2]] representing the class definition. - */ - override def loadClassDefinition(classIri: SmartIri): Task[ClassInfoContentV2] = - triplestore - .query(Construct(sparql.v2.txt.getClassDefinition(classIri))) - .flatMap(_.asExtended) - .map(OntologyHelpers.constructResponseToClassDefinition(classIri, _)) - - /** - * Checks that the last modification date of an ontology is the same as the one we expect it to be. If not, return - * an error message fitting for the "before update" case. - * - * @param internalOntologyIri the internal IRI of the ontology. - * @param expectedLastModificationDate the last modification date that should now be attached to the ontology. - * @return a failed Future if the expected last modification date is not found. - */ - override def checkOntologyLastModificationDateBeforeUpdate( - internalOntologyIri: SmartIri, - expectedLastModificationDate: Instant, - ): Task[Unit] = - checkOntologyLastModificationDate( - internalOntologyIri = internalOntologyIri, - expectedLastModificationDate = expectedLastModificationDate, - errorFun = throw EditConflictException( - s"Ontology ${internalOntologyIri.toOntologySchema(ApiV2Complex)} has been modified by another user, please reload it and try again.", - ), - ) - - /** - * Checks that the last modification date of an ontology is the same as the one we expect it to be. If not, return - * an error message fitting for the "after update" case. - * - * @param internalOntologyIri the internal IRI of the ontology. - * @param expectedLastModificationDate the last modification date that should now be attached to the ontology. - * @return a failed Future if the expected last modification date is not found. - */ - override def checkOntologyLastModificationDateAfterUpdate( - internalOntologyIri: SmartIri, - expectedLastModificationDate: Instant, - ): Task[Unit] = - checkOntologyLastModificationDate( - internalOntologyIri = internalOntologyIri, - expectedLastModificationDate = expectedLastModificationDate, - errorFun = throw UpdateNotPerformedException( - s"Ontology ${internalOntologyIri.toOntologySchema(ApiV2Complex)} was not updated. Please report this as a possible bug.", - ), - ) - - /** - * Checks that the last modification date of an ontology is the same as the one we expect it to be. - * - * @param internalOntologyIri the internal IRI of the ontology. - * @param expectedLastModificationDate the last modification date that the ontology is expected to have. - * @param errorFun a function that throws an exception. It will be called if the expected last modification date is not found. - * @return a failed Future if the expected last modification date is not found. - */ - private def checkOntologyLastModificationDate( - internalOntologyIri: SmartIri, - expectedLastModificationDate: Instant, - errorFun: => Nothing, - ): Task[Unit] = - for { - existingOntologyMetadata <- loadOntologyMetadata(internalOntologyIri) - - _ = existingOntologyMetadata match { - case Some(metadata) => - metadata.lastModificationDate match { - case Some(lastModificationDate) => - if (lastModificationDate != expectedLastModificationDate) { - errorFun - } - - case None => - throw InconsistentRepositoryDataException( - s"Ontology $internalOntologyIri has no ${OntologyConstants.KnoraBase.LastModificationDate}", - ) - } - - case None => - throw NotFoundException( - s"Ontology $internalOntologyIri (corresponding to ${internalOntologyIri.toOntologySchema(ApiV2Complex)}) not found", - ) - } - } yield () - - /** - * Checks whether the requesting user has permission to update an ontology. - * - * @param internalOntologyIri the internal IRI of the ontology. - * @param requestingUser the user making the request. - * @return `true` if the user has permission to update the ontology - */ - override def canUserUpdateOntology(internalOntologyIri: SmartIri, requestingUser: User): Task[Boolean] = - for { - cacheData <- ontologyCache.getCacheData - - projectIri = - cacheData.ontologies - .getOrElse( - internalOntologyIri, - throw NotFoundException(s"Ontology ${internalOntologyIri.toOntologySchema(ApiV2Complex)} not found"), - ) - .ontologyMetadata - .projectIri - .get - } yield requestingUser.permissions.isProjectAdmin(projectIri.toString) || requestingUser.permissions.isSystemAdmin - - /** - * Throws an exception if the requesting user does not have permission to update an ontology. - * - * @param internalOntologyIri the internal IRI of the ontology. - * @param requestingUser the user making the request. - * @return the project IRI. - */ - override def checkPermissionsForOntologyUpdate( - internalOntologyIri: SmartIri, - requestingUser: User, - ): Task[SmartIri] = - for { - cacheData <- ontologyCache.getCacheData - - projectIri = - cacheData.ontologies - .getOrElse( - internalOntologyIri, - throw NotFoundException(s"Ontology ${internalOntologyIri.toOntologySchema(ApiV2Complex)} not found"), - ) - .ontologyMetadata - .projectIri - .get - - _ = if ( - !requestingUser.permissions.isProjectAdmin(projectIri.toString) && !requestingUser.permissions.isSystemAdmin - ) { - // not a project or system admin - throw ForbiddenException("Ontologies can be modified only by a project or system admin.") - } - - } yield projectIri - - /** - * Checks whether an ontology IRI is valid for an update. - * - * @param externalOntologyIri the external IRI of the ontology. - * @return a failed Future if the IRI is not valid for an update. - */ - override def checkExternalOntologyIriForUpdate(externalOntologyIri: SmartIri): Task[Unit] = - if (!externalOntologyIri.isKnoraOntologyIri) { - ZIO.fail(BadRequestException(s"Invalid ontology IRI for request: $externalOntologyIri}")) - } else if (!externalOntologyIri.getOntologySchema.contains(ApiV2Complex)) { - ZIO.fail(BadRequestException(s"Invalid ontology schema for request: $externalOntologyIri")) - } else if (externalOntologyIri.isKnoraBuiltInDefinitionIri) { - ZIO.fail(BadRequestException(s"Ontology $externalOntologyIri cannot be modified via the Knora API")) - } else { - ZIO.succeed(()) - } - - /** - * Checks whether an entity IRI is valid for an update. - * - * @param externalEntityIri the external IRI of the entity. - * @return a failed Future if the entity IRI is not valid for an update, or is not from the specified ontology. - */ - private def checkExternalEntityIriForUpdate(externalEntityIri: SmartIri): Task[Unit] = - if (!externalEntityIri.isKnoraApiV2EntityIri) { - ZIO.fail(BadRequestException(s"Invalid entity IRI for request: $externalEntityIri")) - } else if (!externalEntityIri.getOntologySchema.contains(ApiV2Complex)) { - ZIO.fail(BadRequestException(s"Invalid ontology schema for request: $externalEntityIri")) - } else { - ZIO.succeed(()) - } - -} - -object OntologyHelpersLive { - val layer = ZLayer.derive[OntologyHelpersLive] -} diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/OntologyTriplestoreHelpers.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/OntologyTriplestoreHelpers.scala new file mode 100644 index 0000000000..9f940dc0f3 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/OntologyTriplestoreHelpers.scala @@ -0,0 +1,305 @@ +/* + * Copyright © 2021 - 2024 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.responders.v2.ontology + +import zio.* + +import java.time.Instant +import scala.collection.immutable + +import dsp.errors.* +import org.knora.webapi.* +import org.knora.webapi.messages.IriConversions.* +import org.knora.webapi.messages.OntologyConstants +import org.knora.webapi.messages.SmartIri +import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.messages.ValuesValidator +import org.knora.webapi.messages.store.triplestoremessages.* +import org.knora.webapi.messages.twirl.queries.sparql +import org.knora.webapi.messages.v2.responder.ontologymessages.* +import org.knora.webapi.slice.admin.domain.service.KnoraProjectRepo +import org.knora.webapi.store.triplestore.api.TriplestoreService +import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Construct +import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Select + +trait OntologyTriplestoreHelpers { + + /** + * Loads a class definition from the triplestore and converts it to a [[ClassInfoContentV2]]. + * + * @param classIri the IRI of the class to be loaded. + * @return a [[ClassInfoContentV2]] representing the class definition. + */ + def loadClassDefinition(classIri: SmartIri): Task[ClassInfoContentV2] + + /** + * Loads a property definition from the triplestore and converts it to a [[PropertyInfoContentV2]]. + * + * @param propertyIri the IRI of the property to be loaded. + * @return a [[PropertyInfoContentV2]] representing the property definition. + */ + def loadPropertyDefinition(propertyIri: SmartIri): Task[PropertyInfoContentV2] + + /** + * Reads an ontology's metadata. + * + * @param internalOntologyIri the ontology's internal IRI. + * @return an [[OntologyMetadataV2]], or [[None]] if the ontology is not found. + */ + def loadOntologyMetadata(internalOntologyIri: SmartIri): Task[Option[OntologyMetadataV2]] + + /** + * Checks that the last modification date of an ontology is the same as the one we expect it to be. If not, return + * an error message fitting for the "before update" case. + * + * @param internalOntologyIri the internal IRI of the ontology. + * @param expectedLastModificationDate the last modification date that should now be attached to the ontology. + * @return a failed Future if the expected last modification date is not found. + */ + def checkOntologyLastModificationDateBeforeUpdate( + internalOntologyIri: SmartIri, + expectedLastModificationDate: Instant, + ): Task[Unit] + + /** + * Checks that the last modification date of an ontology is the same as the one we expect it to be. If not, return + * an error message fitting for the "after update" case. + * + * @param internalOntologyIri the internal IRI of the ontology. + * @param expectedLastModificationDate the last modification date that should now be attached to the ontology. + * @return a failed Future if the expected last modification date is not found. + */ + def checkOntologyLastModificationDateAfterUpdate( + internalOntologyIri: SmartIri, + expectedLastModificationDate: Instant, + ): Task[Unit] + + /** + * Gets the set of subjects that refer to an ontology or its entities. + * + * @param ontology the ontology. + * @return the set of subjects that refer to the ontology or its entities. + */ + def getSubjectsUsingOntology(ontology: ReadOntologyV2): Task[Set[IRI]] +} + +final case class OntologyTriplestoreHelpersLive( + triplestore: TriplestoreService, + stringFormatter: StringFormatter, +) extends OntologyTriplestoreHelpers { + + override def loadOntologyMetadata(internalOntologyIri: SmartIri): Task[Option[OntologyMetadataV2]] = { + for { + _ <- ZIO.when(!internalOntologyIri.getOntologySchema.contains(InternalSchema)) { + ZIO.fail(AssertionException(s"Expected an internal ontology IRI: $internalOntologyIri")) + } + getOntologyInfoResponse <- triplestore.query(Construct(sparql.v2.txt.getOntologyInfo(internalOntologyIri))) + + metadata: Option[OntologyMetadataV2] = + if (getOntologyInfoResponse.statements.isEmpty) { + None + } else { + getOntologyInfoResponse.statements.get( + internalOntologyIri.toString, + ) match { + case Some(statements: Seq[(IRI, String)]) => + val statementMap: Map[IRI, Seq[String]] = statements.groupBy { case (pred, _) => + pred + }.map { case (pred, predStatements) => + pred -> predStatements.map { case (_, obj) => + obj + } + } + + val projectIris: Seq[String] = statementMap.getOrElse( + OntologyConstants.KnoraBase.AttachedToProject, + throw InconsistentRepositoryDataException( + 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) { + throw InconsistentRepositoryDataException( + s"Ontology $internalOntologyIri has more than one knora-base:attachedToProject", + ) + } else { + stringFormatter.toSmartIri(projectIris.head) + } + + if (!internalOntologyIri.isKnoraBuiltInDefinitionIri) { + if (projectIri.toString == KnoraProjectRepo.builtIn.SystemProject.id.value) { + throw InconsistentRepositoryDataException( + s"Ontology $internalOntologyIri cannot be in project ${KnoraProjectRepo.builtIn.SystemProject.id.value}", + ) + } + + if ( + internalOntologyIri.isKnoraSharedDefinitionIri && projectIri.toString != OntologyConstants.KnoraAdmin.DefaultSharedOntologiesProject + ) { + throw InconsistentRepositoryDataException( + s"Shared ontology $internalOntologyIri must be in project ${OntologyConstants.KnoraAdmin.DefaultSharedOntologiesProject}", + ) + } + } + + val label: String = if (labels.size > 1) { + throw InconsistentRepositoryDataException( + s"Ontology $internalOntologyIri has more than one rdfs:label", + ) + } else if (labels.isEmpty) { + internalOntologyIri.getOntologyName + } else { + labels.head + } + + val comment: Option[String] = if (comments.size > 1) { + throw InconsistentRepositoryDataException( + s"Ontology $internalOntologyIri has more than one rdfs:comment", + ) + } else comments.headOption + + val lastModificationDate: Option[Instant] = + if (lastModDates.size > 1) { + throw InconsistentRepositoryDataException( + s"Ontology $internalOntologyIri has more than one ${OntologyConstants.KnoraBase.LastModificationDate}", + ) + } else if (lastModDates.isEmpty) { + None + } else { + val dateStr = lastModDates.head + Some( + ValuesValidator + .xsdDateTimeStampToInstant(dateStr) + .getOrElse( + throw InconsistentRepositoryDataException( + s"Invalid ${OntologyConstants.KnoraBase.LastModificationDate}: $dateStr", + ), + ), + ) + } + + Some( + OntologyMetadataV2( + ontologyIri = internalOntologyIri, + projectIri = Some(projectIri), + label = Some(label), + comment = comment, + lastModificationDate = lastModificationDate, + ), + ) + + case None => None + } + } + } yield metadata + } + + /** + * Checks that the last modification date of an ontology is the same as the one we expect it to be. + * + * @param internalOntologyIri the internal IRI of the ontology. + * @param expectedLastModificationDate the last modification date that the ontology is expected to have. + * @param errorFun a function that throws an exception. It will be called if the expected last modification date is not found. + * @return a failed Future if the expected last modification date is not found. + */ + private def checkOntologyLastModificationDate( + internalOntologyIri: SmartIri, + expectedLastModificationDate: Instant, + errorFun: => Nothing, + ): Task[Unit] = + for { + existingOntologyMetadata <- loadOntologyMetadata(internalOntologyIri) + + _ = existingOntologyMetadata match { + case Some(metadata) => + metadata.lastModificationDate match { + case Some(lastModificationDate) => + if (lastModificationDate != expectedLastModificationDate) { + errorFun + } + + case None => + throw InconsistentRepositoryDataException( + s"Ontology $internalOntologyIri has no ${OntologyConstants.KnoraBase.LastModificationDate}", + ) + } + + case None => + throw NotFoundException( + s"Ontology $internalOntologyIri (corresponding to ${internalOntologyIri.toOntologySchema(ApiV2Complex)}) not found", + ) + } + } yield () + + override def checkOntologyLastModificationDateBeforeUpdate( + internalOntologyIri: SmartIri, + expectedLastModificationDate: Instant, + ): Task[Unit] = + checkOntologyLastModificationDate( + internalOntologyIri = internalOntologyIri, + expectedLastModificationDate = expectedLastModificationDate, + errorFun = throw EditConflictException( + s"Ontology ${internalOntologyIri.toOntologySchema(ApiV2Complex)} has been modified by another user, please reload it and try again.", + ), + ) + + override def checkOntologyLastModificationDateAfterUpdate( + internalOntologyIri: SmartIri, + expectedLastModificationDate: Instant, + ): Task[Unit] = + checkOntologyLastModificationDate( + internalOntologyIri = internalOntologyIri, + expectedLastModificationDate = expectedLastModificationDate, + errorFun = throw UpdateNotPerformedException( + s"Ontology ${internalOntologyIri.toOntologySchema(ApiV2Complex)} was not updated. Please report this as a possible bug.", + ), + ) + + override def getSubjectsUsingOntology(ontology: ReadOntologyV2): Task[Set[IRI]] = + for { + isOntologyUsedSparql <- ZIO.attempt( + sparql.v2.txt + .isOntologyUsed( + ontologyNamedGraphIri = ontology.ontologyMetadata.ontologyIri, + classIris = ontology.classes.keySet, + propertyIris = ontology.properties.keySet, + ), + ) + + isOntologyUsedResponse <- triplestore.query(Select(isOntologyUsedSparql)) + + subjects = isOntologyUsedResponse.getColOrThrow("s").toSet + } yield subjects + + override def loadPropertyDefinition(propertyIri: SmartIri): Task[PropertyInfoContentV2] = + triplestore + .query(Construct(sparql.v2.txt.getPropertyDefinition(propertyIri))) + .flatMap(_.asExtended(stringFormatter)) + .map(constructResponse => + OntologyHelpers.constructResponseToPropertyDefinition(propertyIri, constructResponse)(stringFormatter), + ) + + override def loadClassDefinition(classIri: SmartIri): Task[ClassInfoContentV2] = + triplestore + .query(Construct(sparql.v2.txt.getClassDefinition(classIri))) + .flatMap(_.asExtended(stringFormatter)) + .map(OntologyHelpers.constructResponseToClassDefinition(classIri, _)(stringFormatter)) +} + +object OntologyTriplestoreHelpersLive { val layer = ZLayer.derive[OntologyTriplestoreHelpersLive] }