From 140f8978a1e1e3bb3635f5c777c6f8db1f2e30e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 15 Feb 2024 10:55:08 +0100 Subject: [PATCH 1/9] Remove ontology from KnoraProject entity --- .../admin/domain/model/KnoraProject.scala | 2 - .../domain/service/ProjectADMService.scala | 35 +-- .../repo/service/KnoraProjectRepoLive.scala | 203 ++++++++---------- .../sparql/admin/getProjects.scala.txt | 7 - .../org/knora/webapi/TestDataFactory.scala | 1 - .../service/ProjectADMServiceSpec.scala | 1 - 6 files changed, 112 insertions(+), 137 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraProject.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraProject.scala index 0fbc10a124..6d055268a0 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraProject.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraProject.scala @@ -20,7 +20,6 @@ import org.knora.webapi.slice.common.Value import org.knora.webapi.slice.common.Value.BooleanValue import org.knora.webapi.slice.common.Value.StringValue import org.knora.webapi.slice.common.WithFrom -import org.knora.webapi.slice.resourceinfo.domain.InternalIri case class KnoraProject( id: ProjectIri, @@ -32,7 +31,6 @@ case class KnoraProject( logo: Option[Logo], status: Status, selfjoin: SelfJoin, - ontologies: List[InternalIri] ) object KnoraProject { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/ProjectADMService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/ProjectADMService.scala index 132cf555e6..ae7e707505 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/ProjectADMService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/ProjectADMService.scala @@ -31,21 +31,23 @@ final case class ProjectADMService( def findByProjectIdentifier(projectId: ProjectIdentifierADM): Task[Option[ProjectADM]] = projectRepo.findById(projectId).flatMap(ZIO.foreach(_)(toProjectADM)) - private def toProjectADM(knoraProject: KnoraProject): Task[ProjectADM] = - ZIO.attempt( - ProjectADM( - id = knoraProject.id.value, - shortname = knoraProject.shortname.value, - shortcode = knoraProject.shortcode.value, - longname = knoraProject.longname.map(_.value), - description = knoraProject.description.map(_.value), - keywords = knoraProject.keywords.map(_.value), - logo = knoraProject.logo.map(_.value), - status = knoraProject.status.value, - selfjoin = knoraProject.selfjoin.value, - ontologies = knoraProject.ontologies.map(_.value) - ).unescape - ) + private def toProjectADM(knoraProject: KnoraProject): Task[ProjectADM] = for { + ontologies <- ontologyRepo.findByProject(knoraProject).map(_.map(_.ontologyMetadata.ontologyIri.toIri)) + prj <- ZIO.attempt( + ProjectADM( + id = knoraProject.id.value, + shortname = knoraProject.shortname.value, + shortcode = knoraProject.shortcode.value, + longname = knoraProject.longname.map(_.value), + description = knoraProject.description.map(_.value), + keywords = knoraProject.keywords.map(_.value), + logo = knoraProject.logo.map(_.value), + status = knoraProject.status.value, + selfjoin = knoraProject.selfjoin.value, + ontologies = ontologies + ).unescape + ) + } yield prj private def toKnoraProject(project: ProjectADM): KnoraProject = KnoraProject( @@ -59,8 +61,7 @@ final case class ProjectADMService( keywords = project.keywords.map(Keyword.unsafeFrom).toList, logo = project.logo.map(Logo.unsafeFrom), status = Status.from(project.status), - selfjoin = SelfJoin.from(project.selfjoin), - ontologies = project.ontologies.map(InternalIri.apply).toList + selfjoin = SelfJoin.from(project.selfjoin) ) def findAllProjectsKeywords: Task[ProjectsKeywordsGetResponseADM] = diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala index 976fafced4..798d5c9722 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala @@ -5,15 +5,11 @@ package org.knora.webapi.slice.admin.repo.service +import dsp.errors.InconsistentRepositoryDataException import org.eclipse.rdf4j.model.vocabulary.OWL import org.eclipse.rdf4j.sparqlbuilder.core.SparqlBuilder.`var` as variable import org.eclipse.rdf4j.sparqlbuilder.core.query.Queries import org.eclipse.rdf4j.sparqlbuilder.graphpattern.GraphPatterns.tp -import org.eclipse.rdf4j.sparqlbuilder.rdf.Iri -import org.eclipse.rdf4j.sparqlbuilder.rdf.Rdf -import zio.* - -import dsp.errors.InconsistentRepositoryDataException import org.knora.webapi.messages.OntologyConstants.KnoraAdmin.* import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM import org.knora.webapi.slice.admin.domain.model.KnoraProject @@ -23,110 +19,24 @@ import org.knora.webapi.slice.admin.domain.model.RestrictedViewSize import org.knora.webapi.slice.admin.domain.service.KnoraProjectRepo import org.knora.webapi.slice.admin.repo.rdf.RdfConversions.* import org.knora.webapi.slice.admin.repo.rdf.Vocabulary -import org.knora.webapi.slice.admin.repo.service.KnoraProjectQueries.getProjectByIri -import org.knora.webapi.slice.admin.repo.service.KnoraProjectQueries.getProjectByShortcode -import org.knora.webapi.slice.admin.repo.service.KnoraProjectQueries.getProjectByShortname +import org.knora.webapi.slice.admin.repo.service.KnoraProjectRepoLive.ProjectQueries import org.knora.webapi.slice.common.repo.rdf.Errors.RdfError import org.knora.webapi.slice.common.repo.rdf.RdfResource 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.Update - -object KnoraProjectQueries { - - // property does not exist in ontology, therefor this is not declared in Vocabulary - private val belongsToOntology: Iri = Rdf.iri(KnoraAdminPrefixExpansion, "belongsToOntology") - - private[service] def getProjectByIri(iri: ProjectIri): Construct = - Construct( - s"""|PREFIX knora-admin: - |PREFIX knora-base: - |PREFIX owl: - |CONSTRUCT { - | ?project ?p ?o . - | ?project knora-admin:belongsToOntology ?ontology . - |} WHERE { - | BIND(IRI("${iri.value}") as ?project) - | ?project a knora-admin:knoraProject . - | OPTIONAL { - | ?ontology a owl:Ontology . - | ?ontology knora-base:attachedToProject ?project . - | } - | ?project ?p ?o . - |}""".stripMargin - ) - - private[service] def getProjectByShortcode(shortcode: Shortcode): Construct = - Construct( - s"""|PREFIX xsd: - |PREFIX rdf: - |PREFIX knora-admin: - |PREFIX knora-base: - |PREFIX owl: - |CONSTRUCT { - | ?project ?p ?o . - | ?project knora-admin:belongsToOntology ?ontology . - |} WHERE { - | ?project knora-admin:projectShortcode "${shortcode.value}"^^xsd:string . - | ?project a knora-admin:knoraProject . - | OPTIONAL{ - | ?ontology a owl:Ontology . - | ?ontology knora-base:attachedToProject ?project . - | } - | ?project ?p ?o . - |}""".stripMargin - ) - - private[service] def getProjectByShortname(shortname: Shortname): Construct = - Construct( - s"""|PREFIX xsd: - |PREFIX rdf: - |PREFIX knora-admin: - |PREFIX knora-base: - |PREFIX owl: - |CONSTRUCT { - | ?project ?p ?o . - | ?project knora-admin:belongsToOntology ?ontology . - |} WHERE { - | ?project knora-admin:projectShortname "${shortname.value}"^^xsd:string . - | ?project a knora-admin:knoraProject . - | OPTIONAL{ - | ?ontology a owl:Ontology . - | ?ontology knora-base:attachedToProject ?project . - | } - | ?project ?p ?o . - |}""".stripMargin - ) - - private[service] def getAllProjects: Construct = { - val (project, p, o, ontology) = (variable("project"), variable("p"), variable("o"), variable("ontology")) - def projectPo = tp(project, p, o) - val query = - Queries - .CONSTRUCT(projectPo.andHas(belongsToOntology, ontology)) - .prefix(Vocabulary.KnoraAdmin.NS, Vocabulary.KnoraBase.NS, OWL.NS) - .where( - project - .isA(Vocabulary.KnoraAdmin.KnoraProject) - .and(ontology.isA(OWL.ONTOLOGY).andHas(Vocabulary.KnoraBase.attachedToProject, project).optional) - .and(projectPo) - ) - Construct(query.getQueryString) - } -} +import zio.* final case class KnoraProjectRepoLive( private val triplestore: TriplestoreService ) extends KnoraProjectRepo { - private val belongsToOntology = "http://www.knora.org/ontology/knora-admin#belongsToOntology" - override def findAll(): Task[List[KnoraProject]] = for { - model <- triplestore.queryRdfModel(KnoraProjectQueries.getAllProjects) + model <- triplestore.queryRdfModel(ProjectQueries.getAllProjects) resources <- model.getSubjectResources projects <- ZIO.foreach(resources)(res => - toKnoraProjectNew(res).orElseFail( + toKnoraProject(res).orElseFail( InconsistentRepositoryDataException(s"Failed to convert $res to KnoraProject") ) ) @@ -143,26 +53,26 @@ final case class KnoraProjectRepoLive( private def findOneByIri(iri: ProjectIri): Task[Option[KnoraProject]] = for { - model <- triplestore.queryRdfModel(getProjectByIri(iri)) + model <- triplestore.queryRdfModel(ProjectQueries.getProjectByIri(iri)) resource <- model.getResource(iri.value) - project <- ZIO.foreach(resource)(toKnoraProjectNew).orElse(ZIO.none) + project <- ZIO.foreach(resource)(toKnoraProject).orElse(ZIO.none) } yield project private def findOneByShortcode(shortcode: Shortcode): Task[Option[KnoraProject]] = for { - model <- triplestore.queryRdfModel(getProjectByShortcode(shortcode)) + model <- triplestore.queryRdfModel(ProjectQueries.getProjectByShortcode(shortcode)) resource <- model.getResourceByPropertyStringValue(ProjectShortcode, shortcode.value) - project <- ZIO.foreach(resource)(toKnoraProjectNew).orElse(ZIO.none) + project <- ZIO.foreach(resource)(toKnoraProject).orElse(ZIO.none) } yield project private def findOneByShortname(shortname: Shortname): Task[Option[KnoraProject]] = for { - model <- triplestore.queryRdfModel(getProjectByShortname(shortname)) + model <- triplestore.queryRdfModel(ProjectQueries.getProjectByShortname(shortname)) resource <- model.getResourceByPropertyStringValue(ProjectShortname, shortname.value) - project <- ZIO.foreach(resource)(toKnoraProjectNew).orElse(ZIO.none) + project <- ZIO.foreach(resource)(toKnoraProject).orElse(ZIO.none) } yield project - private def toKnoraProjectNew(resource: RdfResource): IO[RdfError, KnoraProject] = + private def toKnoraProject(resource: RdfResource): IO[RdfError, KnoraProject] = for { iri <- resource.getSubjectIri shortcode <- resource.getStringLiteralOrFail[Shortcode](ProjectShortcode) @@ -173,7 +83,6 @@ final case class KnoraProjectRepoLive( logo <- resource.getStringLiteral[Logo](ProjectLogo) status <- resource.getBooleanLiteralOrFail[Status](StatusProp) selfjoin <- resource.getBooleanLiteralOrFail[SelfJoin](HasSelfJoinEnabled) - ontologies <- resource.getObjectIris(belongsToOntology) } yield KnoraProject( id = ProjectIri.unsafeFrom(iri.value), shortcode = shortcode, @@ -183,17 +92,95 @@ final case class KnoraProjectRepoLive( keywords = keywords.toList.sortBy(_.value), logo = logo, status = status, - selfjoin = selfjoin, - ontologies = ontologies.toList + selfjoin = selfjoin ) override def setProjectRestrictedView( project: KnoraProject, settings: RestrictedView ): Task[Unit] = - triplestore.query(Update(Queries.setRestrictedView(project.id, settings.size, settings.watermark))) + triplestore.query(Update(ProjectQueries.setRestrictedView(project.id, settings.size, settings.watermark))) + +} + +object KnoraProjectRepoLive { + + private object ProjectQueries { + + def getProjectByIri(iri: ProjectIri): Construct = + Construct( + s"""|PREFIX knora-admin: + |PREFIX knora-base: + |PREFIX owl: + |CONSTRUCT { + | ?project ?p ?o . + |} WHERE { + | BIND(IRI("${iri.value}") as ?project) + | ?project a knora-admin:knoraProject . + | OPTIONAL { + | ?ontology a owl:Ontology . + | ?ontology knora-base:attachedToProject ?project . + | } + | ?project ?p ?o . + |}""".stripMargin + ) + + def getProjectByShortcode(shortcode: Shortcode): Construct = + Construct( + s"""|PREFIX xsd: + |PREFIX rdf: + |PREFIX knora-admin: + |PREFIX knora-base: + |PREFIX owl: + |CONSTRUCT { + | ?project ?p ?o . + |} WHERE { + | ?project knora-admin:projectShortcode "${shortcode.value}"^^xsd:string . + | ?project a knora-admin:knoraProject . + | OPTIONAL{ + | ?ontology a owl:Ontology . + | ?ontology knora-base:attachedToProject ?project . + | } + | ?project ?p ?o . + |}""".stripMargin + ) + + def getProjectByShortname(shortname: Shortname): Construct = + Construct( + s"""|PREFIX xsd: + |PREFIX rdf: + |PREFIX knora-admin: + |PREFIX knora-base: + |PREFIX owl: + |CONSTRUCT { + | ?project ?p ?o . + |} WHERE { + | ?project knora-admin:projectShortname "${shortname.value}"^^xsd:string . + | ?project a knora-admin:knoraProject . + | OPTIONAL{ + | ?ontology a owl:Ontology . + | ?ontology knora-base:attachedToProject ?project . + | } + | ?project ?p ?o . + |}""".stripMargin + ) + + def getAllProjects: Construct = { + val (project, p, o, ontology) = (variable("project"), variable("p"), variable("o"), variable("ontology")) + def projectPo = tp(project, p, o) + val query = + Queries + .CONSTRUCT(projectPo) + .prefix(Vocabulary.KnoraAdmin.NS, Vocabulary.KnoraBase.NS, OWL.NS) + .where( + project + .isA(Vocabulary.KnoraAdmin.KnoraProject) + .and(ontology.isA(OWL.ONTOLOGY).andHas(Vocabulary.KnoraBase.attachedToProject, project).optional) + .and(projectPo) + ) + Construct(query.getQueryString) + } - object Queries { def setRestrictedView(projectIri: ProjectIri, size: RestrictedViewSize, watermark: Boolean): String = s""" |PREFIX rdf: @@ -220,8 +207,6 @@ final case class KnoraProjectRepoLive( |} |""".stripMargin } -} -object KnoraProjectRepoLive { val layer = ZLayer.derive[KnoraProjectRepoLive] } diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/admin/getProjects.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/admin/getProjects.scala.txt index 4c84905b57..225571a955 100644 --- a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/admin/getProjects.scala.txt +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/admin/getProjects.scala.txt @@ -26,7 +26,6 @@ PREFIX owl: CONSTRUCT { ?project ?p ?o . - ?project knora-admin:belongsToOntology ?ontology . } WHERE { @@ -41,11 +40,5 @@ WHERE { @if(maybeShortcode.nonEmpty) { ?project knora-admin:projectShortcode "@maybeShortcode.get"^^xsd:string . } - - ?project a knora-admin:knoraProject . - OPTIONAL{ - ?ontology a owl:Ontology . - ?ontology knora-base:attachedToProject ?project . - } ?project ?p ?o . } diff --git a/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala b/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala index b8ba2a547e..284b6a35ad 100644 --- a/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala +++ b/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala @@ -73,7 +73,6 @@ object TestDataFactory { None, Status.Active, SelfJoin.CannotJoin, - List.empty ) def projectShortcodeIdentifier(shortcode: String): ShortcodeIdentifier = diff --git a/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/ProjectADMServiceSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/ProjectADMServiceSpec.scala index fa4847328b..10422d119e 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/ProjectADMServiceSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/ProjectADMServiceSpec.scala @@ -52,7 +52,6 @@ object ProjectADMServiceSpec extends ZIOSpecDefault { logo = None, status = Status.Active, selfjoin = SelfJoin.CanJoin, - List.empty ) assertTrue( ProjectADMService From 69c03956184964e1485b2dc04f4dfabbc3a79dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Mon, 26 Feb 2024 17:26:59 +0100 Subject: [PATCH 2/9] rename queries to match repo method names --- .../repo/service/KnoraProjectRepoLive.scala | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala index 798d5c9722..63ba8c0a0d 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala @@ -33,7 +33,7 @@ final case class KnoraProjectRepoLive( override def findAll(): Task[List[KnoraProject]] = for { - model <- triplestore.queryRdfModel(ProjectQueries.getAllProjects) + model <- triplestore.queryRdfModel(ProjectQueries.findAll) resources <- model.getSubjectResources projects <- ZIO.foreach(resources)(res => toKnoraProject(res).orElseFail( @@ -53,21 +53,21 @@ final case class KnoraProjectRepoLive( private def findOneByIri(iri: ProjectIri): Task[Option[KnoraProject]] = for { - model <- triplestore.queryRdfModel(ProjectQueries.getProjectByIri(iri)) + model <- triplestore.queryRdfModel(ProjectQueries.findOneByIri(iri)) resource <- model.getResource(iri.value) project <- ZIO.foreach(resource)(toKnoraProject).orElse(ZIO.none) } yield project private def findOneByShortcode(shortcode: Shortcode): Task[Option[KnoraProject]] = for { - model <- triplestore.queryRdfModel(ProjectQueries.getProjectByShortcode(shortcode)) + model <- triplestore.queryRdfModel(ProjectQueries.findOneByShortcode(shortcode)) resource <- model.getResourceByPropertyStringValue(ProjectShortcode, shortcode.value) project <- ZIO.foreach(resource)(toKnoraProject).orElse(ZIO.none) } yield project private def findOneByShortname(shortname: Shortname): Task[Option[KnoraProject]] = for { - model <- triplestore.queryRdfModel(ProjectQueries.getProjectByShortname(shortname)) + model <- triplestore.queryRdfModel(ProjectQueries.findOneByShortname(shortname)) resource <- model.getResourceByPropertyStringValue(ProjectShortname, shortname.value) project <- ZIO.foreach(resource)(toKnoraProject).orElse(ZIO.none) } yield project @@ -99,7 +99,7 @@ final case class KnoraProjectRepoLive( project: KnoraProject, settings: RestrictedView ): Task[Unit] = - triplestore.query(Update(ProjectQueries.setRestrictedView(project.id, settings.size, settings.watermark))) + triplestore.query(Update(ProjectQueries.setProjectRestrictedView(project.id, settings.size, settings.watermark))) } @@ -107,7 +107,7 @@ object KnoraProjectRepoLive { private object ProjectQueries { - def getProjectByIri(iri: ProjectIri): Construct = + def findOneByIri(iri: ProjectIri): Construct = Construct( s"""|PREFIX knora-admin: |PREFIX knora-base: @@ -125,7 +125,7 @@ object KnoraProjectRepoLive { |}""".stripMargin ) - def getProjectByShortcode(shortcode: Shortcode): Construct = + def findOneByShortcode(shortcode: Shortcode): Construct = Construct( s"""|PREFIX xsd: |PREFIX rdf: @@ -145,7 +145,7 @@ object KnoraProjectRepoLive { |}""".stripMargin ) - def getProjectByShortname(shortname: Shortname): Construct = + def findOneByShortname(shortname: Shortname): Construct = Construct( s"""|PREFIX xsd: |PREFIX rdf: @@ -165,7 +165,7 @@ object KnoraProjectRepoLive { |}""".stripMargin ) - def getAllProjects: Construct = { + def findAll: Construct = { val (project, p, o, ontology) = (variable("project"), variable("p"), variable("o"), variable("ontology")) def projectPo = tp(project, p, o) val query = @@ -181,7 +181,7 @@ object KnoraProjectRepoLive { Construct(query.getQueryString) } - def setRestrictedView(projectIri: ProjectIri, size: RestrictedViewSize, watermark: Boolean): String = + def setProjectRestrictedView(projectIri: ProjectIri, size: RestrictedViewSize, watermark: Boolean): String = s""" |PREFIX rdf: |PREFIX xsd: From ca8c7ef4fdfc31f1923251f6c2c1e2d997bbdc9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Mon, 26 Feb 2024 17:32:59 +0100 Subject: [PATCH 3/9] Fix test and fmt --- .../webapi/slice/admin/domain/model/KnoraProject.scala | 2 +- .../slice/admin/repo/service/KnoraProjectRepoLive.scala | 5 +++-- .../src/test/scala/org/knora/webapi/TestDataFactory.scala | 2 +- .../slice/admin/domain/service/ProjectADMServiceSpec.scala | 2 +- .../admin/repo/service/KnoraProjectRepoLiveSpec.scala | 7 +------ 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraProject.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraProject.scala index 6d055268a0..31f44db793 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraProject.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraProject.scala @@ -30,7 +30,7 @@ case class KnoraProject( keywords: List[Keyword], logo: Option[Logo], status: Status, - selfjoin: SelfJoin, + selfjoin: SelfJoin ) object KnoraProject { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala index 63ba8c0a0d..ee00d8a778 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala @@ -5,11 +5,13 @@ package org.knora.webapi.slice.admin.repo.service -import dsp.errors.InconsistentRepositoryDataException import org.eclipse.rdf4j.model.vocabulary.OWL import org.eclipse.rdf4j.sparqlbuilder.core.SparqlBuilder.`var` as variable import org.eclipse.rdf4j.sparqlbuilder.core.query.Queries import org.eclipse.rdf4j.sparqlbuilder.graphpattern.GraphPatterns.tp +import zio.* + +import dsp.errors.InconsistentRepositoryDataException import org.knora.webapi.messages.OntologyConstants.KnoraAdmin.* import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM import org.knora.webapi.slice.admin.domain.model.KnoraProject @@ -25,7 +27,6 @@ import org.knora.webapi.slice.common.repo.rdf.RdfResource 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.Update -import zio.* final case class KnoraProjectRepoLive( private val triplestore: TriplestoreService diff --git a/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala b/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala index 284b6a35ad..ff73c2f1c0 100644 --- a/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala +++ b/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala @@ -72,7 +72,7 @@ object TestDataFactory { List.empty, None, Status.Active, - SelfJoin.CannotJoin, + SelfJoin.CannotJoin ) def projectShortcodeIdentifier(shortcode: String): ShortcodeIdentifier = diff --git a/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/ProjectADMServiceSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/ProjectADMServiceSpec.scala index 10422d119e..7e7ea2ce6f 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/ProjectADMServiceSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/ProjectADMServiceSpec.scala @@ -51,7 +51,7 @@ object ProjectADMServiceSpec extends ZIOSpecDefault { keywords = List.empty, logo = None, status = Status.Active, - selfjoin = SelfJoin.CanJoin, + selfjoin = SelfJoin.CanJoin ) assertTrue( ProjectADMService diff --git a/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLiveSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLiveSpec.scala index 10ba43a984..7aedca7bf9 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLiveSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLiveSpec.scala @@ -23,7 +23,6 @@ import org.knora.webapi.slice.admin.domain.model.KnoraProject.SelfJoin import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortname import org.knora.webapi.slice.admin.domain.model.KnoraProject.Status -import org.knora.webapi.slice.resourceinfo.domain.InternalIri import org.knora.webapi.store.triplestore.api.TriplestoreServiceInMemory object KnoraProjectRepoLiveSpec extends ZIOSpecDefault { @@ -37,8 +36,7 @@ object KnoraProjectRepoLiveSpec extends ZIOSpecDefault { List(Keyword.unsafeFrom("project1")), Some(Logo.unsafeFrom("logo.png")), Status.Active, - SelfJoin.CannotJoin, - List(InternalIri("http://rdfh.ch/projects/1234/onto1")) + SelfJoin.CannotJoin ) private val someProjectTrig = @@ -56,9 +54,6 @@ object KnoraProjectRepoLiveSpec extends ZIOSpecDefault { | knora-admin:projectLogo "logo.png" ; | knora-admin:status true ; | knora-admin:hasSelfJoinEnabled false . - | - | a owl:Ontology ; - | knora-base:attachedToProject . |} |""".stripMargin From e07bc6292fa22e450d85523f88333ee7c542fa39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Mon, 26 Feb 2024 17:39:05 +0100 Subject: [PATCH 4/9] Readd ?project a knora-admin:knoraProject . triple to getProjects query --- .../messages/twirl/queries/sparql/admin/getProjects.scala.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/admin/getProjects.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/admin/getProjects.scala.txt index 225571a955..b00a79be9a 100644 --- a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/admin/getProjects.scala.txt +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/admin/getProjects.scala.txt @@ -40,5 +40,7 @@ WHERE { @if(maybeShortcode.nonEmpty) { ?project knora-admin:projectShortcode "@maybeShortcode.get"^^xsd:string . } + + ?project a knora-admin:knoraProject . ?project ?p ?o . } From f26f56f35447b932d5beb4a56ed263eda4320702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Mon, 26 Feb 2024 21:40:56 +0100 Subject: [PATCH 5/9] Remove ontology from and clean up queries --- .../repo/service/KnoraProjectRepoLive.scala | 34 ++++--------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala index ee00d8a778..f85cdc5589 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala @@ -5,7 +5,6 @@ package org.knora.webapi.slice.admin.repo.service -import org.eclipse.rdf4j.model.vocabulary.OWL import org.eclipse.rdf4j.sparqlbuilder.core.SparqlBuilder.`var` as variable import org.eclipse.rdf4j.sparqlbuilder.core.query.Queries import org.eclipse.rdf4j.sparqlbuilder.graphpattern.GraphPatterns.tp @@ -112,16 +111,11 @@ object KnoraProjectRepoLive { Construct( s"""|PREFIX knora-admin: |PREFIX knora-base: - |PREFIX owl: |CONSTRUCT { | ?project ?p ?o . |} WHERE { | BIND(IRI("${iri.value}") as ?project) | ?project a knora-admin:knoraProject . - | OPTIONAL { - | ?ontology a owl:Ontology . - | ?ontology knora-base:attachedToProject ?project . - | } | ?project ?p ?o . |}""".stripMargin ) @@ -132,17 +126,12 @@ object KnoraProjectRepoLive { |PREFIX rdf: |PREFIX knora-admin: |PREFIX knora-base: - |PREFIX owl: |CONSTRUCT { | ?project ?p ?o . |} WHERE { | ?project knora-admin:projectShortcode "${shortcode.value}"^^xsd:string . | ?project a knora-admin:knoraProject . - | OPTIONAL{ - | ?ontology a owl:Ontology . - | ?ontology knora-base:attachedToProject ?project . - | } - | ?project ?p ?o . + | ?project ?p ?o . |}""".stripMargin ) @@ -151,34 +140,23 @@ object KnoraProjectRepoLive { s"""|PREFIX xsd: |PREFIX rdf: |PREFIX knora-admin: - |PREFIX knora-base: - |PREFIX owl: |CONSTRUCT { | ?project ?p ?o . |} WHERE { | ?project knora-admin:projectShortname "${shortname.value}"^^xsd:string . | ?project a knora-admin:knoraProject . - | OPTIONAL{ - | ?ontology a owl:Ontology . - | ?ontology knora-base:attachedToProject ?project . - | } - | ?project ?p ?o . + | ?project ?p ?o . |}""".stripMargin ) def findAll: Construct = { - val (project, p, o, ontology) = (variable("project"), variable("p"), variable("o"), variable("ontology")) - def projectPo = tp(project, p, o) + val (project, p, o) = (variable("project"), variable("p"), variable("o")) + def projectPo = tp(project, p, o) val query = Queries .CONSTRUCT(projectPo) - .prefix(Vocabulary.KnoraAdmin.NS, Vocabulary.KnoraBase.NS, OWL.NS) - .where( - project - .isA(Vocabulary.KnoraAdmin.KnoraProject) - .and(ontology.isA(OWL.ONTOLOGY).andHas(Vocabulary.KnoraBase.attachedToProject, project).optional) - .and(projectPo) - ) + .prefix(Vocabulary.KnoraAdmin.NS) + .where(project.isA(Vocabulary.KnoraAdmin.KnoraProject).and(projectPo)) Construct(query.getQueryString) } From 0295e4e352d1c43d7a309f8dc115842b453266e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 27 Feb 2024 10:30:21 +0100 Subject: [PATCH 6/9] Move findAllUsers from Responder to RestService --- .../org/knora/webapi/core/LayersTest.scala | 7 +++-- .../responders/admin/UsersResponderSpec.scala | 30 ++++++++++--------- .../org/knora/webapi/core/LayersLive.scala | 2 +- .../responders/admin/UsersResponder.scala | 15 ---------- .../admin/api/UsersEndpointsHandler.scala | 2 +- .../admin/api/service/UsersRestService.scala | 11 +++++-- 6 files changed, 30 insertions(+), 37 deletions(-) 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 9e954a7ae1..33142b670d 100644 --- a/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala +++ b/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala @@ -87,9 +87,10 @@ object LayersTest { type CommonR0 = ActorSystem with AppConfigurationsTest with JwtService with SipiService with StringFormatter type CommonR = ApiRoutes - with ApiV2Endpoints with AdminApiEndpoints + with ApiV2Endpoints with AppRouter + with AssetPermissionsResponder with Authenticator with AuthorizationRestService with CacheService @@ -136,7 +137,6 @@ object LayersTest { with RestResourceInfoService with SearchApiRoutes with SearchResponderV2 - with AssetPermissionsResponder with StandoffResponderV2 with StandoffTagUtilV2 with State @@ -144,6 +144,7 @@ object LayersTest { with TestClientService with TriplestoreService with UsersResponder + with UsersRestService with ValuesResponderV2 private val commonLayersForAllIntegrationTests = @@ -179,8 +180,8 @@ object LayersTest { IriService.layer, KnoraProjectRepoLive.layer, KnoraResponseRenderer.layer, - KnoraUserRepoLive.layer, KnoraUserGroupRepoLive.layer, + KnoraUserRepoLive.layer, ListRestService.layer, ListsEndpoints.layer, ListsEndpointsHandlers.layer, diff --git a/integration/src/test/scala/org/knora/webapi/responders/admin/UsersResponderSpec.scala b/integration/src/test/scala/org/knora/webapi/responders/admin/UsersResponderSpec.scala index cb1184fac2..b7a992021b 100644 --- a/integration/src/test/scala/org/knora/webapi/responders/admin/UsersResponderSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/responders/admin/UsersResponderSpec.scala @@ -6,11 +6,13 @@ package org.knora.webapi.responders.admin import org.apache.pekko.testkit.ImplicitSender +import zio.ZIO import java.util.UUID import dsp.errors.BadRequestException import dsp.errors.DuplicateValueException +import dsp.errors.ForbiddenException import dsp.valueobjects.LanguageCode import org.knora.webapi.* import org.knora.webapi.messages.StringFormatter @@ -26,6 +28,7 @@ import org.knora.webapi.sharedtestdata.SharedTestDataADM import org.knora.webapi.slice.admin.api.UsersEndpoints.Requests.BasicUserInformationChangeRequest import org.knora.webapi.slice.admin.api.UsersEndpoints.Requests.PasswordChangeRequest import org.knora.webapi.slice.admin.api.UsersEndpoints.Requests.UserCreateRequest +import org.knora.webapi.slice.admin.api.service.UsersRestService import org.knora.webapi.slice.admin.domain.model.* import org.knora.webapi.util.ZioScalaTestUtil.assertFailsWithA @@ -43,29 +46,28 @@ class UsersResponderSpec extends CoreSpec with ImplicitSender { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - "The UsersResponder " when { - "asked about all users" should { - "return a list if asked by SystemAdmin" in { - val response = UnsafeZioRun.runOrThrow(UsersResponder.findAllUsers()) - response.users.nonEmpty should be(true) - response.users.size should be(18) - } + "The UsersRestService" when { + "calling getAllUsers" should { - "return a list if asked by ProjectAdmin" in { - val response = UnsafeZioRun.runOrThrow(UsersResponder.findAllUsers()) - response.users.nonEmpty should be(true) - response.users.size should be(18) - } + def getAllUsers(requestingUser: User): ZIO[UsersRestService, Throwable, UsersGetResponseADM] = + ZIO.serviceWithZIO[UsersRestService](_.getAllUsers(requestingUser)) - "not return the system and anonymous users" in { - val response = UnsafeZioRun.runOrThrow(UsersResponder.findAllUsers()) + "with a SystemAdmin should return all real users" in { + val response = UnsafeZioRun.runOrThrow(getAllUsers(rootUser)) response.users.nonEmpty should be(true) response.users.size should be(18) response.users.count(_.id == KnoraSystemInstances.Users.AnonymousUser.id) should be(0) response.users.count(_.id == KnoraSystemInstances.Users.SystemUser.id) should be(0) } + + "fail with unauthorized when asked by an anonymous user" in { + val exit = UnsafeZioRun.run(getAllUsers(SharedTestDataADM.anonymousUser)) + assertFailsWithA[ForbiddenException](exit) + } } + } + "The UsersResponder " when { "asked about an user identified by 'iri' " should { "return a profile if the user (root user) is known" in { val actual = UnsafeZioRun.runOrThrow( 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 1f0c0c2e51..46f13f142e 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala @@ -84,7 +84,7 @@ object LayersLive { ProjectExportStorageService & ProjectImportService & ProjectsResponderADM & QueryTraverser & RepositoryUpdater & ResourcesResponderV2 & ResourceUtilV2 & ResourceUtilV2 & RestCardinalityService & RestResourceInfoService & SearchApiRoutes & SearchResponderV2 & AssetPermissionsResponder & SipiService & StandoffResponderV2 & StandoffTagUtilV2 & - State & StoreRestService & StringFormatter & TriplestoreService & UsersResponder & ValuesResponderV2 + State & StoreRestService & StringFormatter & TriplestoreService & UsersResponder & ValuesResponderV2 & UsersRestService /** * All effect layers needed to provide the `Environment` diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponder.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponder.scala index cad157ce14..cf259e4df2 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponder.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponder.scala @@ -78,17 +78,6 @@ final case class UsersResponder( case other => Responder.handleUnexpectedMessage(other, this.getClass.getName) } - /** - * Gets all the users and returns them as a [[UsersGetResponseADM]]. - * - * @return all the users as a [[UsersGetResponseADM]]. - * [[NotFoundException]] if no users are found. - */ - def findAllUsers(): Task[UsersGetResponseADM] = - userService.findAll - .filterOrFail(_.nonEmpty)(NotFoundException("No users found")) - .map(users => UsersGetResponseADM(users.sorted)) - /** * ~ CACHED ~ * Gets information about a Knora user, and returns it as a [[User]]. @@ -722,10 +711,6 @@ final case class UsersResponder( } object UsersResponder { - - def findAllUsers(): ZIO[UsersResponder, Throwable, UsersGetResponseADM] = - ZIO.serviceWithZIO[UsersResponder](_.findAllUsers()) - def changeUserStatus( userIri: UserIri, status: UserStatus, diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/UsersEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/UsersEndpointsHandler.scala index a16f277c6b..157ee275a4 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/UsersEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/UsersEndpointsHandler.scala @@ -33,7 +33,7 @@ case class UsersEndpointsHandler( private val getUsersHandler = SecuredEndpointHandler[Unit, UsersGetResponseADM]( usersEndpoints.get.users, - requestingUser => _ => restService.listAllUsers(requestingUser) + requestingUser => _ => restService.getAllUsers(requestingUser) ) private val getUserByIriHandler = SecuredEndpointHandler[UserIri, UserResponseADM]( diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/UsersRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/UsersRestService.scala index c15c5784c7..318546c9bd 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/UsersRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/UsersRestService.scala @@ -29,18 +29,23 @@ import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.admin.domain.model.UserIri import org.knora.webapi.slice.admin.domain.model.UserStatus import org.knora.webapi.slice.admin.domain.model.Username +import org.knora.webapi.slice.admin.domain.service.UserService import org.knora.webapi.slice.common.api.AuthorizationRestService import org.knora.webapi.slice.common.api.KnoraResponseRenderer final case class UsersRestService( auth: AuthorizationRestService, + userService: UserService, responder: UsersResponder, format: KnoraResponseRenderer ) { - def listAllUsers(requestingUser: User): Task[UsersGetResponseADM] = for { - _ <- auth.ensureSystemAdminSystemUserOrProjectAdminInAnyProject(requestingUser) - internal <- responder.findAllUsers() + def getAllUsers(requestingUser: User): Task[UsersGetResponseADM] = for { + _ <- auth.ensureSystemAdminSystemUserOrProjectAdminInAnyProject(requestingUser) + internal <- userService.findAll + .filterOrFail(_.nonEmpty)(NotFoundException("No users found")) + .map(_.sorted) + .map(UsersGetResponseADM.apply) external <- format.toExternal(internal) } yield external From 1e4a9a9d8ed4664800a486012268536d8c2c74e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 27 Feb 2024 11:33:16 +0100 Subject: [PATCH 7/9] Move caching of `User`s into `UserService` invalidate user on every `save` in the `UserRepo` --- .../org/knora/webapi/core/LayersTest.scala | 4 +- .../store/cache/CacheServiceManagerSpec.scala | 25 --- ...ZSpec.scala => CacheServiceLiveSpec.scala} | 83 ++++---- .../org/knora/webapi/core/LayersLive.scala | 4 +- .../CacheServiceMessages.scala | 23 --- .../responders/admin/UsersResponder.scala | 178 ++---------------- .../admin/domain/service/UserService.scala | 24 ++- .../repo/service/KnoraUserRepoLive.scala | 5 +- .../CacheServiceRequestMessageHandler.scala | 10 - .../webapi/store/cache/api/CacheService.scala | 28 ++- ...InMemImpl.scala => CacheServiceLive.scala} | 47 +++-- .../repo/service/KnoraUserRepoLiveSpec.scala | 8 +- 12 files changed, 137 insertions(+), 302 deletions(-) rename integration/src/test/scala/org/knora/webapi/store/cache/impl/{CacheInMemImplZSpec.scala => CacheServiceLiveSpec.scala} (54%) rename webapi/src/main/scala/org/knora/webapi/store/cache/impl/{CacheServiceInMemImpl.scala => CacheServiceLive.scala} (85%) 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 33142b670d..0299378ce2 100644 --- a/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala +++ b/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala @@ -58,7 +58,7 @@ import org.knora.webapi.slice.search.api.SearchEndpoints import org.knora.webapi.store.cache.CacheServiceRequestMessageHandler import org.knora.webapi.store.cache.CacheServiceRequestMessageHandlerLive import org.knora.webapi.store.cache.api.CacheService -import org.knora.webapi.store.cache.impl.CacheServiceInMemImpl +import org.knora.webapi.store.cache.impl.CacheServiceLive import org.knora.webapi.store.iiif.IIIFRequestMessageHandler import org.knora.webapi.store.iiif.IIIFRequestMessageHandlerLive import org.knora.webapi.store.iiif.api.SipiService @@ -158,7 +158,7 @@ object LayersTest { AuthenticatorLive.layer, AuthorizationRestServiceLive.layer, BaseEndpoints.layer, - CacheServiceInMemImpl.layer, + CacheServiceLive.layer, CacheServiceRequestMessageHandlerLive.layer, CardinalityHandlerLive.layer, CardinalityService.layer, diff --git a/integration/src/test/scala/org/knora/webapi/store/cache/CacheServiceManagerSpec.scala b/integration/src/test/scala/org/knora/webapi/store/cache/CacheServiceManagerSpec.scala index 977ddf5063..c142a2e109 100644 --- a/integration/src/test/scala/org/knora/webapi/store/cache/CacheServiceManagerSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/store/cache/CacheServiceManagerSpec.scala @@ -7,44 +7,19 @@ package org.knora.webapi.store.cache import dsp.errors.BadRequestException import org.knora.webapi.* -import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.* import org.knora.webapi.messages.store.cacheservicemessages.* import org.knora.webapi.sharedtestdata.SharedTestDataADM -import org.knora.webapi.slice.admin.domain.model.* /** * This spec is used to test [[org.knora.webapi.store.cache.serialization.CacheSerialization]]. */ class CacheServiceManagerSpec extends CoreSpec { - implicit protected val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - - val user = SharedTestDataADM.imagesUser01 val project = SharedTestDataADM.imagesProject "The CacheManager" should { - "successfully store a user" in { - appActor ! CacheServicePutUserADM(user) - expectMsg(()) - } - - "successfully retrieve a user by IRI" in { - appActor ! CacheServiceGetUserByIriADM(UserIri.unsafeFrom(user.id)) - expectMsg(Some(user)) - } - - "successfully retrieve a user by USERNAME" in { - appActor ! CacheServiceGetUserByUsernameADM(Username.unsafeFrom(user.username)) - expectMsg(Some(user)) - } - - "successfully retrieve a user by EMAIL" in { - appActor ! CacheServiceGetUserByEmailADM(Email.unsafeFrom(user.email)) - expectMsg(Some(user)) - } - "successfully store a project" in { appActor ! CacheServicePutProjectADM(project) expectMsg(()) diff --git a/integration/src/test/scala/org/knora/webapi/store/cache/impl/CacheInMemImplZSpec.scala b/integration/src/test/scala/org/knora/webapi/store/cache/impl/CacheServiceLiveSpec.scala similarity index 54% rename from integration/src/test/scala/org/knora/webapi/store/cache/impl/CacheInMemImplZSpec.scala rename to integration/src/test/scala/org/knora/webapi/store/cache/impl/CacheServiceLiveSpec.scala index db4df73a8d..73b05a9576 100644 --- a/integration/src/test/scala/org/knora/webapi/store/cache/impl/CacheInMemImplZSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/store/cache/impl/CacheServiceLiveSpec.scala @@ -5,27 +5,20 @@ package org.knora.webapi.store.cache.impl -import zio.ZLayer -import zio.test.Assertion.* import zio.test.* import dsp.errors.BadRequestException -import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.* import org.knora.webapi.sharedtestdata.SharedTestDataADM import org.knora.webapi.slice.admin.domain.model.* import org.knora.webapi.store.cache.api.CacheService -/** - * This spec is used to test [[org.knora.webapi.store.cache.impl.CacheServiceInMemImpl]]. - */ -object CacheInMemImplZSpec extends ZIOSpecDefault { +object CacheServiceLiveSpec extends ZIOSpecDefault { - implicit val stringFormatter: StringFormatter = StringFormatter.getInitializedTestInstance + private val user: User = SharedTestDataADM.imagesUser01 - val user: User = SharedTestDataADM.imagesUser01 - val userWithApostrophe = User( + private val userWithApostrophe = User( id = "http://rdfh.ch/users/aaaaaab71e7b0e01", username = "user_with_apostrophe", email = "userWithApostrophe@example.org", @@ -37,43 +30,43 @@ object CacheInMemImplZSpec extends ZIOSpecDefault { val project: ProjectADM = SharedTestDataADM.imagesProject - /** - * Defines a layer which encompases all dependencies that are needed for - * running the tests. - */ - val testLayers = ZLayer.make[CacheService](CacheServiceInMemImpl.layer) - - def spec: Spec[Any, Throwable] = - (userTests + projectTests + otherTests).provideLayerShared(testLayers) @@ TestAspect.sequential - - val userTests = suite("CacheInMemImplZSpec - user")( - test("successfully store a user and retrieve by IRI") { + private val userTests = suite("User")( + test("successfully store a user and retrieve by UserIri") { for { - _ <- CacheService.putUserADM(user) - retrievedUser <- CacheService.getUserByIriADM(UserIri.unsafeFrom(user.id)) - } yield assertTrue(retrievedUser == Some(user)) + _ <- CacheService.putUser(user) + retrievedUser <- CacheService.getUserByIri(UserIri.unsafeFrom(user.id)) + } yield assertTrue(retrievedUser.contains(user)) }, - test("successfully store a user and retrieve by USERNAME")( + test("successfully store a user and retrieve by Username")( for { - _ <- CacheService.putUserADM(user) - retrievedUser <- CacheService.getUserByUsernameADM(Username.unsafeFrom(user.username)) - } yield assert(retrievedUser)(equalTo(Some(user))) + _ <- CacheService.putUser(user) + retrievedUser <- CacheService.getUserByUsername(Username.unsafeFrom(user.username)) + } yield assertTrue(retrievedUser.contains(user)) ), - test("successfully store a user and retrieve by EMAIL")( + test("successfully store a user and retrieve by Email")( for { - _ <- CacheService.putUserADM(user) - retrievedUser <- CacheService.getUserByEmailADM(Email.unsafeFrom(user.email)) - } yield assert(retrievedUser)(equalTo(Some(user))) + _ <- CacheService.putUser(user) + retrievedUser <- CacheService.getUserByEmail(Email.unsafeFrom(user.email)) + } yield assertTrue(retrievedUser.contains(user)) ), - test("successfully store and retrieve a user with special characters in his name")( + test("successfully store and retrieve a user with special characters in their name")( for { - _ <- CacheService.putUserADM(userWithApostrophe) - retrievedUser <- CacheService.getUserByIriADM(UserIri.unsafeFrom(userWithApostrophe.id)) - } yield assert(retrievedUser)(equalTo(Some(userWithApostrophe))) + _ <- CacheService.putUser(userWithApostrophe) + retrievedUser <- CacheService.getUserByIri(userWithApostrophe.userIri) + } yield assertTrue(retrievedUser.contains(userWithApostrophe)) + ), + test("given when successfully stored and invalidated a user then the cache should not contain the user")( + for { + _ <- CacheService.putUser(userWithApostrophe) + _ <- CacheService.invalidateUser(userWithApostrophe.userIri) + userByIri <- CacheService.getUserByIri(userWithApostrophe.userIri) + userByEmail <- CacheService.getUserByEmail(Email.unsafeFrom(userWithApostrophe.email)) + userByUsername <- CacheService.getUserByUsername(Username.unsafeFrom(userWithApostrophe.username)) + } yield assertTrue(userByIri.isEmpty, userByEmail.isEmpty, userByUsername.isEmpty) ) ) - val projectTests = suite("CacheInMemImplZSpec - project")( + private val projectTests = suite("ProjectADM")( test("successfully store a project and retrieve by IRI")( for { _ <- CacheService.putProjectADM(project) @@ -82,7 +75,7 @@ object CacheInMemImplZSpec extends ZIOSpecDefault { .fromString(project.id) .getOrElseWith(e => throw BadRequestException(e.head.getMessage)) ) - } yield assert(retrievedProject)(equalTo(Some(project))) + } yield assertTrue(retrievedProject.contains(project)) ), test("successfully store a project and retrieve by SHORTCODE")( for { @@ -93,7 +86,7 @@ object CacheInMemImplZSpec extends ZIOSpecDefault { .fromString(project.shortcode) .getOrElseWith(e => throw BadRequestException(e.head.getMessage)) ) - } yield assert(retrievedProject)(equalTo(Some(project))) + } yield assertTrue(retrievedProject.contains(project)) ), test("successfully store a project and retrieve by SHORTNAME")( for { @@ -104,23 +97,27 @@ object CacheInMemImplZSpec extends ZIOSpecDefault { .fromString(project.shortname) .getOrElseWith(e => throw BadRequestException(e.head.getMessage)) ) - } yield assert(retrievedProject)(equalTo(Some(project))) + } yield assertTrue(retrievedProject.contains(project)) ) ) - val otherTests = suite("CacheInMemImplZSpec - other")( + private val otherTests = suite("Other")( test("successfully store string value")( for { _ <- CacheService.putStringValue("my-new-key", "my-new-value") retrievedValue <- CacheService.getStringValue("my-new-key") - } yield assert(retrievedValue)(equalTo(Some("my-new-value"))) + } yield assertTrue(retrievedValue.contains("my-new-value")) ), test("successfully delete stored value")( for { _ <- CacheService.putStringValue("my-new-key", "my-new-value") _ <- CacheService.removeValues(Set("my-new-key")) retrievedValue <- CacheService.getStringValue("my-new-key") - } yield assert(retrievedValue)(equalTo(None)) + } yield assertTrue(retrievedValue.isEmpty) ) ) + + def spec: Spec[Any, Throwable] = + suite("CacheServiceLive")(userTests, projectTests, otherTests).provide(CacheServiceLive.layer) + } diff --git a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala index 46f13f142e..3438be999c 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala @@ -58,7 +58,7 @@ import org.knora.webapi.slice.search.api.SearchEndpoints import org.knora.webapi.store.cache.CacheServiceRequestMessageHandler import org.knora.webapi.store.cache.CacheServiceRequestMessageHandlerLive import org.knora.webapi.store.cache.api.CacheService -import org.knora.webapi.store.cache.impl.CacheServiceInMemImpl +import org.knora.webapi.store.cache.impl.CacheServiceLive import org.knora.webapi.store.iiif.IIIFRequestMessageHandler import org.knora.webapi.store.iiif.IIIFRequestMessageHandlerLive import org.knora.webapi.store.iiif.api.SipiService @@ -102,7 +102,7 @@ object LayersLive { AuthenticatorLive.layer, AuthorizationRestServiceLive.layer, BaseEndpoints.layer, - CacheServiceInMemImpl.layer, + CacheServiceLive.layer, CacheServiceRequestMessageHandlerLive.layer, CardinalityHandlerLive.layer, CardinalityService.layer, diff --git a/webapi/src/main/scala/org/knora/webapi/messages/store/cacheservicemessages/CacheServiceMessages.scala b/webapi/src/main/scala/org/knora/webapi/messages/store/cacheservicemessages/CacheServiceMessages.scala index 70c9a133c2..166af95eb5 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/store/cacheservicemessages/CacheServiceMessages.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/store/cacheservicemessages/CacheServiceMessages.scala @@ -9,10 +9,7 @@ import org.knora.webapi.core.RelayedMessage import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM import org.knora.webapi.messages.store.StoreRequest -import org.knora.webapi.slice.admin.domain.model.Email import org.knora.webapi.slice.admin.domain.model.User -import org.knora.webapi.slice.admin.domain.model.UserIri -import org.knora.webapi.slice.admin.domain.model.Username sealed trait CacheServiceRequest extends StoreRequest with RelayedMessage @@ -26,26 +23,6 @@ case class CacheServicePutProjectADM(value: ProjectADM) extends CacheServiceRequ */ case class CacheServiceGetProjectADM(identifier: ProjectIdentifierADM) extends CacheServiceRequest -/** - * Message requesting to write user to cache. - */ -case class CacheServicePutUserADM(value: User) extends CacheServiceRequest - -/** - * Message requesting to retrieve user from cache. - */ -case class CacheServiceGetUserByIriADM(userIri: UserIri) extends CacheServiceRequest - -/** - * Message requesting to retrieve user from cache. - */ -case class CacheServiceGetUserByEmailADM(email: Email) extends CacheServiceRequest - -/** - * Message requesting to retrieve user from cache. - */ -case class CacheServiceGetUserByUsernameADM(username: Username) extends CacheServiceRequest - /** * Message requesting to store a simple string under the supplied key. */ diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponder.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponder.scala index cf259e4df2..df854a1622 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponder.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponder.scala @@ -28,11 +28,6 @@ import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetADM import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.* import org.knora.webapi.messages.admin.responder.usersmessages.UserOperationResponseADM import org.knora.webapi.messages.admin.responder.usersmessages.* -import org.knora.webapi.messages.store.cacheservicemessages.CacheServiceGetUserByEmailADM -import org.knora.webapi.messages.store.cacheservicemessages.CacheServiceGetUserByIriADM -import org.knora.webapi.messages.store.cacheservicemessages.CacheServiceGetUserByUsernameADM -import org.knora.webapi.messages.store.cacheservicemessages.CacheServicePutUserADM -import org.knora.webapi.messages.store.cacheservicemessages.CacheServiceRemoveValues import org.knora.webapi.messages.util.KnoraSystemInstances.Users import org.knora.webapi.responders.IriLocker import org.knora.webapi.responders.IriService @@ -79,7 +74,6 @@ final case class UsersResponder( } /** - * ~ CACHED ~ * Gets information about a Knora user, and returns it as a [[User]]. * If possible, tries to retrieve it from the cache. If not, it retrieves * it from the triplestore, and then writes it to the cache. Writes to the @@ -89,20 +83,14 @@ final case class UsersResponder( * @param userInformationType the type of the requested profile (restricted * of full). * @param requestingUser the user initiating the request. - * @param skipCache the flag denotes to skip the cache and instead - * get data from the triplestore * @return a [[User]] describing the user. */ def findUserByIri( identifier: UserIri, userInformationType: UserInformationTypeADM, - requestingUser: User, - skipCache: Boolean = false + requestingUser: User ): Task[Option[User]] = - for { - maybeUserADM <- if (skipCache) userService.findUserByIri(identifier) - else getUserFromCacheOrTriplestoreByIri(identifier) - } yield maybeUserADM.map(filterUserInformation(_, requestingUser, userInformationType)) + userService.findUserByIri(identifier).map(_.map(filterUserInformation(_, requestingUser, userInformationType))) /** * If the requesting user is a system admin, or is requesting themselves, or is a system user, @@ -118,7 +106,6 @@ final case class UsersResponder( else user.ofType(UserInformationTypeADM.Public) /** - * ~ CACHED ~ * Gets information about a Knora user, and returns it as a [[User]]. * If possible, tries to retrieve it from the cache. If not, it retrieves * it from the triplestore, and then writes it to the cache. Writes to the @@ -128,23 +115,16 @@ final case class UsersResponder( * @param userInformationType the type of the requested profile (restricted * of full). * @param requestingUser the user initiating the request. - * @param skipCache the flag denotes to skip the cache and instead - * get data from the triplestore * @return a [[User]] describing the user. */ def findUserByEmail( email: Email, userInformationType: UserInformationTypeADM, - requestingUser: User, - skipCache: Boolean = false + requestingUser: User ): Task[Option[User]] = - for { - maybeUserADM <- if (skipCache) userService.findUserByEmail(email) - else getUserFromCacheOrTriplestoreByEmail(email) - } yield maybeUserADM.map(filterUserInformation(_, requestingUser, userInformationType)) + userService.findUserByEmail(email).map(_.map(filterUserInformation(_, requestingUser, userInformationType))) /** - * ~ CACHED ~ * Gets information about a Knora user, and returns it as a [[User]]. * If possible, tries to retrieve it from the cache. If not, it retrieves * it from the triplestore, and then writes it to the cache. Writes to the @@ -154,21 +134,14 @@ final case class UsersResponder( * @param userInformationType the type of the requested profile (restricted * of full). * @param requestingUser the user initiating the request. - * @param skipCache the flag denotes to skip the cache and instead - * get data from the triplestore * @return a [[User]] describing the user. */ def findUserByUsername( username: Username, userInformationType: UserInformationTypeADM, - requestingUser: User, - skipCache: Boolean = false + requestingUser: User ): Task[Option[User]] = - for { - maybeUserADM <- - if (skipCache) userService.findUserByUsername(username) - else getUserFromCacheOrTriplestoreByUsername(username) - } yield maybeUserADM.map(filterUserInformation(_, requestingUser, userInformationType)) + userService.findUserByUsername(username).map(_.map(filterUserInformation(_, requestingUser, userInformationType))) /** * Updates an existing user. Only basic user data information (username, email, givenName, familyName, lang) @@ -298,11 +271,8 @@ final case class UsersResponder( * @return a sequence of [[ProjectADM]] */ private def userProjectMembershipsGetADM(userIri: IRI) = - findUserByIri( - UserIri.unsafeFrom(userIri), - UserInformationTypeADM.Full, - Users.SystemUser - ).map(_.map(_.projects).getOrElse(Seq.empty)) + findUserByIri(UserIri.unsafeFrom(userIri), UserInformationTypeADM.Full, Users.SystemUser) + .map(_.map(_.projects).getOrElse(Seq.empty)) /** * Returns the user's project memberships as [[UserProjectMembershipsGetResponseADM]]. @@ -540,13 +510,9 @@ final case class UsersResponder( .findById(userIri) .someOrFail(NotFoundException(s"User '$userIri' not found.")) _ <- userService.updateUser(currentUser, req) - _ <- messageRelay.ask[Unit]( - CacheServiceRemoveValues(Set(currentUser.id.value, currentUser.email.value, currentUser.username.value)) - ) updatedUserADM <- - findUserByIri(userIri, UserInformationTypeADM.Full, Users.SystemUser, skipCache = true) + findUserByIri(userIri, UserInformationTypeADM.Full, Users.SystemUser) .someOrFail(UpdateNotPerformedException("User was not updated. Please report this as a possible bug.")) - _ <- messageRelay.ask[Unit](CacheServicePutUserADM(updatedUserADM)) } yield UserOperationResponseADM(updatedUserADM.ofType(UserInformationTypeADM.Restricted)) /** @@ -595,7 +561,7 @@ final case class UsersResponder( // try to retrieve newly created user (will also add to cache) createdUser <- - findUserByIri(userIri, UserInformationTypeADM.Full, Users.SystemUser, skipCache = true).someOrFail { + findUserByIri(userIri, UserInformationTypeADM.Full, Users.SystemUser).someOrFail { val msg = s"User ${userIri.value} was not created. Please report this as a possible bug." UpdateNotPerformedException(msg) } @@ -603,111 +569,6 @@ final case class UsersResponder( IriLocker.runWithIriLock(apiRequestID, USERS_GLOBAL_LOCK_IRI, createNewUserTask) } - - /** - * Tries to retrieve a [[User]] either from triplestore or cache if caching is enabled. - * If user is not found in cache but in triplestore, then user is written to cache. - */ - private def getUserFromCacheOrTriplestoreByIri( - userIri: UserIri - ): Task[Option[User]] = - if (appConfig.cacheService.enabled) { - // caching enabled - messageRelay.ask[Option[User]](CacheServiceGetUserByIriADM(userIri)).flatMap { - case None => - // none found in cache. getting from triplestore. - userService.findUserByIri(userIri).flatMap { - case None => - // also none found in triplestore. finally returning none. - logger.debug("getUserFromCacheOrTriplestore - not found in cache and in triplestore") - ZIO.none - case Some(user) => - // found a user in the triplestore. need to write to cache. - logger.debug( - "getUserFromCacheOrTriplestore - not found in cache but found in triplestore. need to write to cache." - ) - // writing user to cache and afterwards returning the user found in the triplestore - messageRelay.ask[Unit](CacheServicePutUserADM(user)).as(Some(user)) - } - case Some(user) => - logger.debug("getUserFromCacheOrTriplestore - found in cache. returning user.") - ZIO.some(user) - } - } else { - // caching disabled - logger.debug("getUserFromCacheOrTriplestore - caching disabled. getting from triplestore.") - userService.findUserByIri(userIri) - } - - /** - * Tries to retrieve a [[User]] either from triplestore or cache if caching is enabled. - * If user is not found in cache but in triplestore, then user is written to cache. - */ - private def getUserFromCacheOrTriplestoreByUsername( - username: Username - ): Task[Option[User]] = - if (appConfig.cacheService.enabled) { - // caching enabled - messageRelay.ask[Option[User]](CacheServiceGetUserByUsernameADM(username)).flatMap { - case None => - // none found in cache. getting from triplestore. - userService.findUserByUsername(username).flatMap { - case None => - // also none found in triplestore. finally returning none. - logger.debug("getUserFromCacheOrTriplestore - not found in cache and in triplestore") - ZIO.none - case Some(user) => - // found a user in the triplestore. need to write to cache. - logger.debug( - "getUserFromCacheOrTriplestore - not found in cache but found in triplestore. need to write to cache." - ) - // writing user to cache and afterwards returning the user found in the triplestore - messageRelay.ask[Unit](CacheServicePutUserADM(user)).as(Some(user)) - } - case Some(user) => - logger.debug("getUserFromCacheOrTriplestore - found in cache. returning user.") - ZIO.some(user) - } - } else { - // caching disabled - logger.debug("getUserFromCacheOrTriplestore - caching disabled. getting from triplestore.") - userService.findUserByUsername(username) - } - - /** - * Tries to retrieve a [[User]] either from triplestore or cache if caching is enabled. - * If user is not found in cache but in triplestore, then user is written to cache. - */ - private def getUserFromCacheOrTriplestoreByEmail( - email: Email - ): Task[Option[User]] = - if (appConfig.cacheService.enabled) { - // caching enabled - messageRelay.ask[Option[User]](CacheServiceGetUserByEmailADM(email)).flatMap { - case None => - // none found in cache. getting from triplestore. - userService.findUserByEmail(email).flatMap { - case None => - // also none found in triplestore. finally returning none. - logger.debug("getUserFromCacheOrTriplestore - not found in cache and in triplestore") - ZIO.none - case Some(user) => - // found a user in the triplestore. need to write to cache. - logger.debug( - "getUserFromCacheOrTriplestore - not found in cache but found in triplestore. need to write to cache." - ) - // writing user to cache and afterwards returning the user found in the triplestore - messageRelay.ask[Unit](CacheServicePutUserADM(user)).as(Some(user)) - } - case Some(user) => - logger.debug("getUserFromCacheOrTriplestore - found in cache. returning user.") - ZIO.some(user) - } - } else { - // caching disabled - logger.debug("getUserFromCacheOrTriplestore - caching disabled. getting from triplestore.") - userService.findUserByEmail(email) - } } object UsersResponder { @@ -728,26 +589,23 @@ object UsersResponder { def findUserByIri( identifier: UserIri, userInformationType: UserInformationTypeADM, - requestingUser: User, - skipCache: Boolean = false + requestingUser: User ): ZIO[UsersResponder, Throwable, Option[User]] = - ZIO.serviceWithZIO[UsersResponder](_.findUserByIri(identifier, userInformationType, requestingUser, skipCache)) + ZIO.serviceWithZIO[UsersResponder](_.findUserByIri(identifier, userInformationType, requestingUser)) def findUserByEmail( email: Email, userInformationType: UserInformationTypeADM, - requestingUser: User, - skipCache: Boolean = false + requestingUser: User ): ZIO[UsersResponder, Throwable, Option[User]] = - ZIO.serviceWithZIO[UsersResponder](_.findUserByEmail(email, userInformationType, requestingUser, skipCache)) + ZIO.serviceWithZIO[UsersResponder](_.findUserByEmail(email, userInformationType, requestingUser)) def findUserByUsername( username: Username, userInformationType: UserInformationTypeADM, - requestingUser: User, - skipCache: Boolean = false + requestingUser: User ): ZIO[UsersResponder, Throwable, Option[User]] = - ZIO.serviceWithZIO[UsersResponder](_.findUserByUsername(username, userInformationType, requestingUser, skipCache)) + ZIO.serviceWithZIO[UsersResponder](_.findUserByUsername(username, userInformationType, requestingUser)) def findProjectMemberShipsByIri( userIri: UserIri @@ -766,9 +624,7 @@ object UsersResponder { projectIri: ProjectIri, apiRequestID: UUID ): ZIO[UsersResponder, Throwable, UserOperationResponseADM] = - ZIO.serviceWithZIO[UsersResponder]( - _.addProjectToUserIsInProjectAdminGroup(userIri, projectIri, apiRequestID) - ) + ZIO.serviceWithZIO[UsersResponder](_.addProjectToUserIsInProjectAdminGroup(userIri, projectIri, apiRequestID)) def removeProjectFromUserIsInProjectAndIsInProjectAdminGroup( userIri: UserIri, diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/UserService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/UserService.scala index 41c57bd948..c91d6bf7f2 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/UserService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/UserService.scala @@ -25,6 +25,7 @@ import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.admin.domain.model.UserIri import org.knora.webapi.slice.admin.domain.model.UserStatus import org.knora.webapi.slice.admin.domain.model.Username +import org.knora.webapi.store.cache.api.CacheService final case class UserChangeRequest( username: Option[Username] = None, @@ -44,17 +45,32 @@ case class UserService( private val userRepo: KnoraUserRepo, private val projectsService: ProjectADMService, private val groupsService: GroupsResponderADM, - private val permissionService: PermissionsResponderADM + private val permissionService: PermissionsResponderADM, + private val cacheService: CacheService ) { def findUserByIri(iri: UserIri): Task[Option[User]] = - userRepo.findById(iri).flatMap(ZIO.foreach(_)(toUser)) + fromCacheOrRepo(iri, cacheService.getUserByIri, userRepo.findById) def findUserByEmail(email: Email): Task[Option[User]] = - userRepo.findByEmail(email).flatMap(ZIO.foreach(_)(toUser)) + fromCacheOrRepo(email, cacheService.getUserByEmail, userRepo.findByEmail) def findUserByUsername(username: Username): Task[Option[User]] = - userRepo.findByUsername(username).flatMap(ZIO.foreach(_)(toUser)) + fromCacheOrRepo(username, cacheService.getUserByUsername, userRepo.findByUsername) + + private def fromCacheOrRepo[A]( + id: A, + fromCache: A => Task[Option[User]], + fromRepo: A => Task[Option[KnoraUser]] + ): Task[Option[User]] = + fromCache(id).flatMap { + case Some(user) => ZIO.some(user) + case None => + fromRepo(id).flatMap(ZIO.foreach(_)(toUser)).tap { + case Some(user) => cacheService.putUser(user) + case None => ZIO.unit + } + } def findAll: Task[Seq[User]] = userRepo.findAll().flatMap(ZIO.foreach(_)(toUser)) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserRepoLive.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserRepoLive.scala index 8a9ee5bb68..4304547a4d 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserRepoLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserRepoLive.scala @@ -39,12 +39,13 @@ import org.knora.webapi.slice.admin.repo.rdf.Vocabulary import org.knora.webapi.slice.admin.repo.service.KnoraUserRepoLive.UserQueries import org.knora.webapi.slice.common.repo.rdf.Errors.ConversionError import org.knora.webapi.slice.common.repo.rdf.RdfResource +import org.knora.webapi.store.cache.api.CacheService 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.Update import org.knora.webapi.store.triplestore.errors.TriplestoreResponseException -final case class KnoraUserRepoLive(triplestore: TriplestoreService) extends KnoraUserRepo { +final case class KnoraUserRepoLive(triplestore: TriplestoreService, cacheService: CacheService) extends KnoraUserRepo { override def findById(id: UserIri): Task[Option[KnoraUser]] = { val construct = UserQueries.findById(id) @@ -120,7 +121,7 @@ final case class KnoraUserRepoLive(triplestore: TriplestoreService) extends Knor } yield users.toList override def save(user: KnoraUser): Task[KnoraUser] = - triplestore.query(UserQueries.save(user)).as(user) + cacheService.invalidateUser(user.id) *> triplestore.query(UserQueries.save(user)).as(user) } object KnoraUserRepoLive { diff --git a/webapi/src/main/scala/org/knora/webapi/store/cache/CacheServiceRequestMessageHandler.scala b/webapi/src/main/scala/org/knora/webapi/store/cache/CacheServiceRequestMessageHandler.scala index a2bf1e4962..1126a7f005 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/cache/CacheServiceRequestMessageHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/cache/CacheServiceRequestMessageHandler.scala @@ -21,12 +21,6 @@ trait CacheServiceRequestMessageHandler extends MessageHandler final case class CacheServiceRequestMessageHandlerLive(cacheService: CacheService) extends CacheServiceRequestMessageHandler { - private val cacheServiceWriteUserTimer = Metric - .timer( - name = "cache-service-write-user", - chronoUnit = ChronoUnit.NANOS - ) - private val cacheServiceWriteProjectTimer = Metric .timer( name = "cache-service-write-project", @@ -41,10 +35,6 @@ final case class CacheServiceRequestMessageHandlerLive(cacheService: CacheServic override def isResponsibleFor(message: ResponderRequest): Boolean = message.isInstanceOf[CacheServiceRequest] override def handle(message: ResponderRequest): Task[Any] = message match { - case CacheServicePutUserADM(value) => cacheService.putUserADM(value) @@ cacheServiceWriteUserTimer.trackDuration - case CacheServiceGetUserByIriADM(iri) => cacheService.getUserByIriADM(iri) - case CacheServiceGetUserByEmailADM(email) => cacheService.getUserByEmailADM(email) - case CacheServiceGetUserByUsernameADM(username) => cacheService.getUserByUsernameADM(username) case CacheServicePutProjectADM(value) => cacheService.putProjectADM(value) @@ cacheServiceWriteProjectTimer.trackDuration case CacheServiceGetProjectADM(identifier) => diff --git a/webapi/src/main/scala/org/knora/webapi/store/cache/api/CacheService.scala b/webapi/src/main/scala/org/knora/webapi/store/cache/api/CacheService.scala index cbd4a920b3..ae5cc07139 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/cache/api/CacheService.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/cache/api/CacheService.scala @@ -22,16 +22,36 @@ import org.knora.webapi.slice.admin.domain.model.Username */ @accessible trait CacheService { - def putUserADM(value: User): Task[Unit] - def getUserByIriADM(iri: UserIri): Task[Option[User]] - def getUserByUsernameADM(username: Username): Task[Option[User]] - def getUserByEmailADM(email: Email): Task[Option[User]] + + def putUser(value: User): Task[Unit] + + def getUserByIri(iri: UserIri): Task[Option[User]] + + def getUserByUsername(username: Username): Task[Option[User]] + + def getUserByEmail(email: Email): Task[Option[User]] + + /** + * Invalidates the user stored under the IRI. + * + * @param iri the user's IRI. + */ + def invalidateUser(iri: UserIri): UIO[Unit] + def putProjectADM(value: ProjectADM): Task[Unit] + def getProjectADM(identifier: ProjectIdentifierADM): Task[Option[ProjectADM]] + def invalidateProjectADM(identifier: KnoraProject.ProjectIri): UIO[Unit] + def putStringValue(key: String, value: String): Task[Unit] + def getStringValue(key: String): Task[Option[String]] + def removeValues(keys: Set[String]): Task[Unit] + def flushDB(requestingUser: User): Task[Unit] + val getStatus: UIO[CacheServiceStatusResponse] + } diff --git a/webapi/src/main/scala/org/knora/webapi/store/cache/impl/CacheServiceInMemImpl.scala b/webapi/src/main/scala/org/knora/webapi/store/cache/impl/CacheServiceLive.scala similarity index 85% rename from webapi/src/main/scala/org/knora/webapi/store/cache/impl/CacheServiceInMemImpl.scala rename to webapi/src/main/scala/org/knora/webapi/store/cache/impl/CacheServiceLive.scala index a85f1fa45d..d38b16a18f 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/cache/impl/CacheServiceInMemImpl.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/cache/impl/CacheServiceLive.scala @@ -35,10 +35,10 @@ import org.knora.webapi.store.cache.api.EmptyValue * @param projects a map of projects. * @param lut a lookup table of username/email to IRI. */ -case class CacheServiceInMemImpl( +case class CacheServiceLive( users: TMap[String, User], projects: TMap[String, ProjectADM], - lut: TMap[String, String] // sealed trait for key type + lut: TMap[String, String] ) extends CacheService { /** @@ -51,39 +51,36 @@ case class CacheServiceInMemImpl( * * @param value the value to be stored */ - def putUserADM(value: User): Task[Unit] = + def putUser(value: User): Task[Unit] = (for { _ <- users.put(value.id, value) _ <- lut.put(value.username, value.id) _ <- lut.put(value.email, value.id) - } yield ()).commit.tap(_ => ZIO.logDebug(s"Stored UserADM to Cache: ${value.id}")) + } yield ()).commit - override def getUserByIriADM(iri: UserIri): Task[Option[User]] = getUserByIri(iri.value) + override def getUserByIri(iri: UserIri): Task[Option[User]] = users.get(iri.value).commit - override def getUserByUsernameADM(username: Username): Task[Option[User]] = getUserByUsernameOrEmail(username.value) + override def getUserByUsername(username: Username): Task[Option[User]] = getUserByLookupKey(username.value) - override def getUserByEmailADM(email: Email): Task[Option[User]] = getUserByUsernameOrEmail(email.value) + override def getUserByEmail(email: Email): Task[Option[User]] = getUserByLookupKey(email.value) - /** - * Retrieves the user stored under the IRI. - * - * @param id the user's IRI. - * @return an optional [[User]]. - */ - def getUserByIri(id: String): UIO[Option[User]] = - users.get(id).commit + private def getUserByLookupKey(key: String): UIO[Option[User]] = + lut.get(key).some.flatMap(users.get(_).some).commit.unsome /** - * Retrieves the user stored under the username or email. - * - * @param usernameOrEmail of the user. - * @return an optional [[User]]. + * Invalidates the user stored under the IRI. + * @param iri the user's IRI. */ - def getUserByUsernameOrEmail(usernameOrEmail: String): UIO[Option[User]] = + override def invalidateUser(iri: UserIri): UIO[Unit] = (for { - iri <- lut.get(usernameOrEmail).some - user <- users.get(iri).some - } yield user).commit.unsome // watch Spartan session about error. post example on Spartan channel + user <- users.get(iri.value).some + _ <- users.delete(iri.value) + _ <- users.delete(user.username) + _ <- users.delete(user.email) + _ <- lut.delete(iri.value) + _ <- lut.delete(user.username) + _ <- lut.delete(user.email) + } yield ()).commit.ignore /** * Stores the project under the IRI and additionally the IRI under the keys @@ -223,13 +220,13 @@ case class CacheServiceInMemImpl( ZIO.succeed(CacheServiceStatusOK) } -object CacheServiceInMemImpl { +object CacheServiceLive { val layer: ZLayer[Any, Nothing, CacheService] = ZLayer { for { users <- TMap.empty[String, User].commit projects <- TMap.empty[String, ProjectADM].commit lut <- TMap.empty[String, String].commit - } yield CacheServiceInMemImpl(users, projects, lut) + } yield CacheServiceLive(users, projects, lut) }.tap(_ => ZIO.logInfo(">>> In-Memory Cache Service Initialized <<<")) } diff --git a/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserRepoLiveSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserRepoLiveSpec.scala index 47627b79d3..697a04b813 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserRepoLiveSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserRepoLiveSpec.scala @@ -28,6 +28,7 @@ import org.knora.webapi.slice.admin.domain.model.UserIri import org.knora.webapi.slice.admin.domain.model.Username import org.knora.webapi.slice.admin.domain.service.KnoraUserRepo import org.knora.webapi.slice.admin.repo.rdf.Vocabulary +import org.knora.webapi.store.cache.impl.CacheServiceLive import org.knora.webapi.store.triplestore.api.TriplestoreService import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Update import org.knora.webapi.store.triplestore.api.TriplestoreServiceInMemory @@ -190,5 +191,10 @@ object KnoraUserRepoLiveSpec extends ZIOSpecDefault { } yield assertTrue(updatedUser.isInGroup.isEmpty) } ) - ).provide(KnoraUserRepoLive.layer, TriplestoreServiceInMemory.emptyLayer, StringFormatter.test) + ).provide( + KnoraUserRepoLive.layer, + TriplestoreServiceInMemory.emptyLayer, + StringFormatter.test, + CacheServiceLive.layer + ) } From 336459f20ec21d9218d72bedfb8ac30b81e127a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 27 Feb 2024 11:38:46 +0100 Subject: [PATCH 8/9] cleanup scaladoc --- .../knora/webapi/responders/admin/UsersResponder.scala | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponder.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponder.scala index df854a1622..a7c8396f0a 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponder.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponder.scala @@ -75,9 +75,6 @@ final case class UsersResponder( /** * Gets information about a Knora user, and returns it as a [[User]]. - * If possible, tries to retrieve it from the cache. If not, it retrieves - * it from the triplestore, and then writes it to the cache. Writes to the - * cache are always `UserInformationTypeADM.FULL`. * * @param identifier the IRI of the user. * @param userInformationType the type of the requested profile (restricted @@ -107,9 +104,6 @@ final case class UsersResponder( /** * Gets information about a Knora user, and returns it as a [[User]]. - * If possible, tries to retrieve it from the cache. If not, it retrieves - * it from the triplestore, and then writes it to the cache. Writes to the - * cache are always `UserInformationTypeADM.FULL`. * * @param email the email of the user. * @param userInformationType the type of the requested profile (restricted @@ -126,9 +120,6 @@ final case class UsersResponder( /** * Gets information about a Knora user, and returns it as a [[User]]. - * If possible, tries to retrieve it from the cache. If not, it retrieves - * it from the triplestore, and then writes it to the cache. Writes to the - * cache are always `UserInformationTypeADM.FULL`. * * @param username the username of the user. * @param userInformationType the type of the requested profile (restricted @@ -559,7 +550,6 @@ final case class UsersResponder( ) _ <- userRepo.save(newUser) - // try to retrieve newly created user (will also add to cache) createdUser <- findUserByIri(userIri, UserInformationTypeADM.Full, Users.SystemUser).someOrFail { val msg = s"User ${userIri.value} was not created. Please report this as a possible bug." From b0360fdc518719845b7610cd4dca2492419ee4b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 27 Feb 2024 15:35:24 +0100 Subject: [PATCH 9/9] Use typed lookup tables in CacheServiceLive and remove unused code --- .../cache/impl/CacheServiceLiveSpec.scala | 18 +- .../ProjectsMessagesADM.scala | 3 + .../CacheServiceMessages.scala | 18 +- .../admin/ProjectsResponderADM.scala | 4 +- .../admin/api/service/StoreRestService.scala | 2 +- .../slice/admin/domain/model/User.scala | 4 +- .../CacheServiceRequestMessageHandler.scala | 9 +- .../webapi/store/cache/api/CacheService.scala | 12 +- .../store/cache/impl/CacheServiceLive.scala | 193 ++++++------------ 9 files changed, 80 insertions(+), 183 deletions(-) diff --git a/integration/src/test/scala/org/knora/webapi/store/cache/impl/CacheServiceLiveSpec.scala b/integration/src/test/scala/org/knora/webapi/store/cache/impl/CacheServiceLiveSpec.scala index 73b05a9576..4d9475d3db 100644 --- a/integration/src/test/scala/org/knora/webapi/store/cache/impl/CacheServiceLiveSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/store/cache/impl/CacheServiceLiveSpec.scala @@ -101,23 +101,7 @@ object CacheServiceLiveSpec extends ZIOSpecDefault { ) ) - private val otherTests = suite("Other")( - test("successfully store string value")( - for { - _ <- CacheService.putStringValue("my-new-key", "my-new-value") - retrievedValue <- CacheService.getStringValue("my-new-key") - } yield assertTrue(retrievedValue.contains("my-new-value")) - ), - test("successfully delete stored value")( - for { - _ <- CacheService.putStringValue("my-new-key", "my-new-value") - _ <- CacheService.removeValues(Set("my-new-key")) - retrievedValue <- CacheService.getStringValue("my-new-key") - } yield assertTrue(retrievedValue.isEmpty) - ) - ) - def spec: Spec[Any, Throwable] = - suite("CacheServiceLive")(userTests, projectTests, otherTests).provide(CacheServiceLive.layer) + suite("CacheServiceLive")(userTests, projectTests).provide(CacheServiceLive.layer) } diff --git a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADM.scala b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADM.scala index 10decdf39f..15c3cd04c2 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADM.scala @@ -206,6 +206,9 @@ case class ProjectADM( def projectIri: ProjectIri = ProjectIri.unsafeFrom(id) + def getShortname: Shortname = Shortname.unsafeFrom(shortname) + def getShortcode: Shortcode = Shortcode.unsafeFrom(shortcode) + if (description.isEmpty) { throw OntologyConstraintException("Project description is a required property.") } diff --git a/webapi/src/main/scala/org/knora/webapi/messages/store/cacheservicemessages/CacheServiceMessages.scala b/webapi/src/main/scala/org/knora/webapi/messages/store/cacheservicemessages/CacheServiceMessages.scala index 166af95eb5..46dbd57752 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/store/cacheservicemessages/CacheServiceMessages.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/store/cacheservicemessages/CacheServiceMessages.scala @@ -9,7 +9,6 @@ import org.knora.webapi.core.RelayedMessage import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM import org.knora.webapi.messages.store.StoreRequest -import org.knora.webapi.slice.admin.domain.model.User sealed trait CacheServiceRequest extends StoreRequest with RelayedMessage @@ -23,25 +22,10 @@ case class CacheServicePutProjectADM(value: ProjectADM) extends CacheServiceRequ */ case class CacheServiceGetProjectADM(identifier: ProjectIdentifierADM) extends CacheServiceRequest -/** - * Message requesting to store a simple string under the supplied key. - */ -case class CacheServicePutString(key: String, value: String) extends CacheServiceRequest - -/** - * Message requesting to retrieve simple string stored under the key. - */ -case class CacheServiceGetString(key: String) extends CacheServiceRequest - -/** - * Message requesting to remove anything stored under the keys. - */ -case class CacheServiceRemoveValues(keys: Set[String]) extends CacheServiceRequest - /** * Message requesting to completely empty the cache (wipe everything). */ -case class CacheServiceFlushDB(requestingUser: User) extends CacheServiceRequest +case object CacheServiceClearCache extends CacheServiceRequest /** * Queries Cache Service status. diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala index de1b4d2221..084c0b7333 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala @@ -23,7 +23,7 @@ import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentif import org.knora.webapi.messages.admin.responder.projectsmessages.* import org.knora.webapi.messages.admin.responder.usersmessages.UserGetByIriADM import org.knora.webapi.messages.admin.responder.usersmessages.UserInformationTypeADM -import org.knora.webapi.messages.store.cacheservicemessages.CacheServiceFlushDB +import org.knora.webapi.messages.store.cacheservicemessages.CacheServiceClearCache import org.knora.webapi.messages.store.triplestoremessages.* import org.knora.webapi.messages.twirl.queries.sparql import org.knora.webapi.messages.util.KnoraSystemInstances @@ -498,7 +498,7 @@ final case class ProjectsResponderADMLive( .someOrFail(NotFoundException(s"Project '${projectIri.value}' not found. Aborting update request.")) // we are changing the project, so lets get rid of the cached copy - _ <- messageRelay.ask[Any](CacheServiceFlushDB(KnoraSystemInstances.Users.SystemUser)) + _ <- messageRelay.ask[Any](CacheServiceClearCache) /* Update project */ updateQuery = sparql.admin.txt.updateProject( diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/StoreRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/StoreRestService.scala index 596e3766ea..916eb1ca57 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/StoreRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/StoreRestService.scala @@ -43,7 +43,7 @@ final case class StoreRestService( _ <- ZIO.logWarning(s"Resetting triplestore content with ${rdfDataObjects.map(_.name).mkString(", ")}") _ <- triplestoreService.resetTripleStoreContent(rdfDataObjects, prependDefaults).logError _ <- ontologyCache.loadOntologies(SystemUser).logError - _ <- cacheService.flushDB(SystemUser).logError + _ <- cacheService.clearCache().logError } yield MessageResponse("success") } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/User.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/User.scala index 985bbb3274..f8f401419b 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/User.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/User.scala @@ -74,7 +74,9 @@ final case class User( permissions: PermissionsDataADM = PermissionsDataADM() ) extends Ordered[User] { self => - def userIri = UserIri.unsafeFrom(id) + def userIri = UserIri.unsafeFrom(id) + def getUsername = Username.unsafeFrom(username) + def getEmail = Email.unsafeFrom(email) /** * Allows to sort collections of UserADM. Sorting is done by the id. diff --git a/webapi/src/main/scala/org/knora/webapi/store/cache/CacheServiceRequestMessageHandler.scala b/webapi/src/main/scala/org/knora/webapi/store/cache/CacheServiceRequestMessageHandler.scala index 1126a7f005..722246ebde 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/cache/CacheServiceRequestMessageHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/cache/CacheServiceRequestMessageHandler.scala @@ -39,12 +39,9 @@ final case class CacheServiceRequestMessageHandlerLive(cacheService: CacheServic cacheService.putProjectADM(value) @@ cacheServiceWriteProjectTimer.trackDuration case CacheServiceGetProjectADM(identifier) => cacheService.getProjectADM(identifier) @@ cacheServiceReadProjectTimer.trackDuration - case CacheServicePutString(key, value) => cacheService.putStringValue(key, value) - case CacheServiceGetString(key) => cacheService.getStringValue(key) - case CacheServiceRemoveValues(keys) => cacheService.removeValues(keys) - case CacheServiceFlushDB(requestingUser) => cacheService.flushDB(requestingUser) - case CacheServiceGetStatus => cacheService.getStatus - case other => ZIO.logError(s"CacheServiceManager received an unexpected message: $other") + case CacheServiceClearCache => cacheService.clearCache() + case CacheServiceGetStatus => cacheService.getStatus + case other => ZIO.logError(s"CacheServiceManager received an unexpected message: $other") } } diff --git a/webapi/src/main/scala/org/knora/webapi/store/cache/api/CacheService.scala b/webapi/src/main/scala/org/knora/webapi/store/cache/api/CacheService.scala index ae5cc07139..cafa1620ee 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/cache/api/CacheService.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/cache/api/CacheService.scala @@ -12,7 +12,7 @@ import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM import org.knora.webapi.messages.store.cacheservicemessages.CacheServiceStatusResponse import org.knora.webapi.slice.admin.domain.model.Email -import org.knora.webapi.slice.admin.domain.model.KnoraProject +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.model.UserIri import org.knora.webapi.slice.admin.domain.model.Username @@ -42,15 +42,9 @@ trait CacheService { def getProjectADM(identifier: ProjectIdentifierADM): Task[Option[ProjectADM]] - def invalidateProjectADM(identifier: KnoraProject.ProjectIri): UIO[Unit] + def invalidateProjectADM(identifier: ProjectIri): UIO[Unit] - def putStringValue(key: String, value: String): Task[Unit] - - def getStringValue(key: String): Task[Option[String]] - - def removeValues(keys: Set[String]): Task[Unit] - - def flushDB(requestingUser: User): Task[Unit] + def clearCache(): Task[Unit] val getStatus: UIO[CacheServiceStatusResponse] diff --git a/webapi/src/main/scala/org/knora/webapi/store/cache/impl/CacheServiceLive.scala b/webapi/src/main/scala/org/knora/webapi/store/cache/impl/CacheServiceLive.scala index d38b16a18f..1e123b35f1 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/cache/impl/CacheServiceLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/cache/impl/CacheServiceLive.scala @@ -14,14 +14,13 @@ import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentif import org.knora.webapi.messages.store.cacheservicemessages.CacheServiceStatusOK import org.knora.webapi.messages.store.cacheservicemessages.CacheServiceStatusResponse import org.knora.webapi.slice.admin.domain.model.Email -import org.knora.webapi.slice.admin.domain.model.KnoraProject import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri +import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode +import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortname import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.admin.domain.model.UserIri import org.knora.webapi.slice.admin.domain.model.Username import org.knora.webapi.store.cache.api.CacheService -import org.knora.webapi.store.cache.api.EmptyKey -import org.knora.webapi.store.cache.api.EmptyValue /** * In-Memory Cache implementation @@ -30,42 +29,42 @@ import org.knora.webapi.store.cache.api.EmptyValue * A ref in itself is fiber (thread) safe, but to keep the cumulative state * consistent, all Refs need to be updated in a single transaction. This * requires STM (Software Transactional Memory) to be used. - * - * @param users a map of users. - * @param projects a map of projects. - * @param lut a lookup table of username/email to IRI. */ case class CacheServiceLive( - users: TMap[String, User], - projects: TMap[String, ProjectADM], - lut: TMap[String, String] + users: TMap[UserIri, User], + projects: TMap[ProjectIri, ProjectADM], + mappingUsernameUserIri: TMap[Username, UserIri], + mappingEmailUserIri: TMap[Email, UserIri], + mappingShortcodeProjectIri: TMap[Shortcode, ProjectIri], + mappingShortnameProjectIri: TMap[Shortname, ProjectIri] ) extends CacheService { /** - * Stores the user under the IRI (inside 'users') and additionally the IRI - * under the keys of USERNAME and EMAIL (inside the 'lut'): + * Stores the user under the IRI and additionally the IRI + * + * users: + * IRI -> byte array * - * IRI -> byte array - * username -> IRI - * email -> IRI + * lookupTableUsers: + * username -> IRI + * email -> IRI * * @param value the value to be stored */ def putUser(value: User): Task[Unit] = (for { - _ <- users.put(value.id, value) - _ <- lut.put(value.username, value.id) - _ <- lut.put(value.email, value.id) + _ <- users.put(value.userIri, value) + _ <- mappingUsernameUserIri.put(value.getUsername, value.userIri) + _ <- mappingEmailUserIri.put(value.getEmail, value.userIri) } yield ()).commit - override def getUserByIri(iri: UserIri): Task[Option[User]] = users.get(iri.value).commit - - override def getUserByUsername(username: Username): Task[Option[User]] = getUserByLookupKey(username.value) + override def getUserByIri(iri: UserIri): Task[Option[User]] = users.get(iri).commit - override def getUserByEmail(email: Email): Task[Option[User]] = getUserByLookupKey(email.value) + override def getUserByUsername(username: Username): Task[Option[User]] = + mappingUsernameUserIri.get(username).some.flatMap(users.get(_).some).commit.unsome - private def getUserByLookupKey(key: String): UIO[Option[User]] = - lut.get(key).some.flatMap(users.get(_).some).commit.unsome + override def getUserByEmail(email: Email): Task[Option[User]] = + mappingEmailUserIri.get(email).some.flatMap(users.get(_).some).commit.unsome /** * Invalidates the user stored under the IRI. @@ -73,48 +72,50 @@ case class CacheServiceLive( */ override def invalidateUser(iri: UserIri): UIO[Unit] = (for { - user <- users.get(iri.value).some - _ <- users.delete(iri.value) - _ <- users.delete(user.username) - _ <- users.delete(user.email) - _ <- lut.delete(iri.value) - _ <- lut.delete(user.username) - _ <- lut.delete(user.email) + user <- users.get(iri).some + _ <- users.delete(iri) + _ <- mappingUsernameUserIri.delete(user.getUsername) + _ <- mappingEmailUserIri.delete(user.getEmail) } yield ()).commit.ignore /** * Stores the project under the IRI and additionally the IRI under the keys - * of SHORTCODE and SHORTNAME: + * of Shortcode and Shortname: * - * IRI -> byte array - * shortname -> IRI - * shortcode -> IRI + * projects: + * IRI -> byte array + * + * lookupTableProjects: + * shortname -> IRI + * shortcode -> IRI * * @param value the stored value * @return [[Unit]] */ def putProjectADM(value: ProjectADM): Task[Unit] = (for { - _ <- projects.put(value.id, value) - _ <- lut.put(value.shortname, value.id) - _ <- lut.put(value.shortcode, value.id) - } yield ()).commit.tap(_ => ZIO.logDebug(s"Stored ProjectADM to Cache: ${value.id}")) + _ <- projects.put(value.projectIri, value) + _ <- mappingShortcodeProjectIri.put(value.getShortcode, value.projectIri) + _ <- mappingShortnameProjectIri.put(value.getShortname, value.projectIri) + } yield ()).commit /** * Retrieves the project stored under the identifier (either iri, shortname, or shortcode). * * The data is stored under the IRI key. - * Additionally, the SHORTCODE and SHORTNAME keys point to the IRI key + * Additionally, the Shortcode and Shortname keys point to the IRI key * * @param identifier the project identifier. * @return an optional [[ProjectADM]] */ def getProjectADM(identifier: ProjectIdentifierADM): Task[Option[ProjectADM]] = - (identifier match { - case IriIdentifier(value) => getProjectByIri(value) - case ShortcodeIdentifier(value) => getProjectByShortcode(value) - case ShortnameIdentifier(value) => getProjectByShortname(value) - }).tap(_ => ZIO.logDebug(s"Retrieved ProjectADM from Cache: $identifier")) + identifier match { + case IriIdentifier(projectIri) => projects.get(projectIri).commit + case ShortcodeIdentifier(code) => + mappingShortcodeProjectIri.get(code).some.flatMap(projects.get(_).some).commit.unsome + case ShortnameIdentifier(name) => + mappingShortnameProjectIri.get(name).some.flatMap(projects.get(_).some).commit.unsome + } /** * Invalidates the project stored under the IRI. @@ -123,95 +124,24 @@ case class CacheServiceLive( */ def invalidateProjectADM(iri: ProjectIri): UIO[Unit] = (for { - project <- projects.get(iri.value).some - shortcode = project.shortcode - shortname = project.shortname - _ <- projects.delete(iri.value) - _ <- projects.delete(shortcode) - _ <- projects.delete(shortname) - _ <- lut.delete(iri.value) - _ <- lut.delete(shortcode) - _ <- lut.delete(shortname) - } yield ()).commit.ignore - - /** - * Retrieves the project by the IRI. - * - * @param iri the project's IRI - * @return an optional [[ProjectADM]]. - */ - def getProjectByIri(iri: ProjectIri) = projects.get(iri.value).commit - - /** - * Retrieves the project by the SHORTNAME. - * - * @param shortname of the project. - * @return an optional [[ProjectADM]] - */ - def getProjectByShortname(shortname: KnoraProject.Shortname): UIO[Option[ProjectADM]] = - (for { - iri <- lut.get(shortname.value).some project <- projects.get(iri).some - } yield project).commit.unsome - - /** - * Retrieves the project by the SHORTCODE. - * - * @param shortcode of the project. - * @return an optional [[ProjectADM]] - */ - def getProjectByShortcode(shortcode: KnoraProject.Shortcode): UIO[Option[ProjectADM]] = - (for { - iri <- lut.get(shortcode.value).some - project <- projects.get(iri).some - } yield project).commit.unsome - - /** - * Store string or byte array value under key. - * - * @param key the key. - * @param value the value. - */ - def putStringValue(key: String, value: String): Task[Unit] = { - - val emptyKeyError = EmptyKey("The key under which the value should be written is empty. Aborting write to cache.") - val emptyValueError = EmptyValue("The string value is empty. Aborting write to cache.") - - (for { - key <- if (key.isEmpty()) ZIO.fail(emptyKeyError) else ZIO.succeed(key) - value <- if (value.isEmpty()) ZIO.fail(emptyValueError) else ZIO.succeed(value) - _ <- lut.put(key, value).commit - } yield ()).tap(_ => ZIO.logDebug(s"Wrote key: $key with value: $value to cache.")) - } - - /** - * Get value stored under the key as a string. - * - * @param maybeKey the key. - * @return an optional [[String]]. - */ - def getStringValue(key: String): Task[Option[String]] = - lut.get(key).commit.tap(value => ZIO.logDebug(s"Retrieved key: $key with value: $value from cache.")) - - /** - * Removes values for the provided keys. Any invalid keys are ignored. - * - * @param keys the keys. - */ - def removeValues(keys: Set[String]): Task[Unit] = - (for { - _ <- ZIO.foreach(keys)(key => lut.delete(key).commit) // FIXME: is this realy thread safe? - } yield ()).tap(_ => ZIO.logDebug(s"Removed keys from cache: $keys")) + _ <- projects.delete(iri) + _ <- mappingShortcodeProjectIri.delete(project.getShortcode) + _ <- mappingShortnameProjectIri.delete(project.getShortname) + } yield ()).commit.ignore /** * Flushes (removes) all stored content from the in-memory cache. */ - def flushDB(requestingUser: User): Task[Unit] = + def clearCache(): Task[Unit] = (for { _ <- users.foreach((k, _) => users.delete(k)) _ <- projects.foreach((k, _) => projects.delete(k)) - _ <- lut.foreach((k, _) => lut.delete(k)) - } yield ()).commit.tap(_ => ZIO.logDebug("Flushed in-memory cache")) + _ <- mappingUsernameUserIri.foreach((k, _) => mappingUsernameUserIri.delete(k)) + _ <- mappingEmailUserIri.foreach((k, _) => mappingEmailUserIri.delete(k)) + _ <- mappingShortcodeProjectIri.foreach((k, _) => mappingShortcodeProjectIri.delete(k)) + _ <- mappingShortnameProjectIri.foreach((k, _) => mappingShortnameProjectIri.delete(k)) + } yield ()).commit /** * Pings the in-memory cache to see if it is available. @@ -224,9 +154,12 @@ object CacheServiceLive { val layer: ZLayer[Any, Nothing, CacheService] = ZLayer { for { - users <- TMap.empty[String, User].commit - projects <- TMap.empty[String, ProjectADM].commit - lut <- TMap.empty[String, String].commit - } yield CacheServiceLive(users, projects, lut) + users <- TMap.empty[UserIri, User].commit + projects <- TMap.empty[ProjectIri, ProjectADM].commit + usernameMapping <- TMap.empty[Username, UserIri].commit + emailMapping <- TMap.empty[Email, UserIri].commit + shortcodeMapping <- TMap.empty[Shortcode, ProjectIri].commit + shortnameMapping <- TMap.empty[Shortname, ProjectIri].commit + } yield CacheServiceLive(users, projects, usernameMapping, emailMapping, shortcodeMapping, shortnameMapping) }.tap(_ => ZIO.logInfo(">>> In-Memory Cache Service Initialized <<<")) }