Skip to content

Commit

Permalink
refactor: Load ontologies when querying for KnoraProjects (#2916)
Browse files Browse the repository at this point in the history
  • Loading branch information
seakayone committed Nov 6, 2023
1 parent e4c587a commit 21550ce
Show file tree
Hide file tree
Showing 14 changed files with 101 additions and 155 deletions.
32 changes: 13 additions & 19 deletions webapi/src/main/scala/dsp/valueobjects/Project.scala
Expand Up @@ -11,7 +11,6 @@ import zio.prelude.Validation
import scala.util.matching.Regex

import dsp.errors.ValidationException
import dsp.valueobjects.Iri

object Project {
// A regex sub-pattern for project IDs, which must consist of 4 hexadecimal digits.
Expand All @@ -28,17 +27,6 @@ object Project {
*/
private val shortnameRegex: Regex = "^[a-zA-Z][a-zA-Z0-9_-]{2,19}$".r

/**
* Check that the string represents a valid project shortname.
*
* @param shortname string to be checked.
* @return the same string.
*/
private def validateAndEscapeProjectShortname(shortname: String): Option[String] =
shortnameRegex
.findFirstIn(shortname)
.flatMap(Iri.toSparqlEncodedString)

object ErrorMessages {
val ShortcodeMissing = "Shortcode cannot be empty."
val ShortcodeInvalid = (v: String) => s"Invalid project shortcode: $v"
Expand All @@ -64,8 +52,7 @@ object Project {
implicit val encoder: JsonEncoder[Shortcode] =
JsonEncoder[String].contramap((shortcode: Shortcode) => shortcode.value)

def unsafeFrom(str: String) = make(str)
.getOrElse(throw new IllegalArgumentException(ErrorMessages.ShortcodeInvalid(str)))
def unsafeFrom(str: String): Shortcode = make(str).fold(e => throw e.head, identity)

def make(value: String): Validation[ValidationException, Shortcode] =
if (value.isEmpty) Validation.fail(ValidationException(ErrorMessages.ShortcodeMissing))
Expand All @@ -87,19 +74,26 @@ object Project {
implicit val encoder: JsonEncoder[Shortname] =
JsonEncoder[String].contramap((shortname: Shortname) => shortname.value)

def unsafeFrom(str: String): Shortname = make(str).fold(e => throw e.head, identity)

def make(value: String): Validation[ValidationException, Shortname] =
if (value.isEmpty) Validation.fail(ValidationException(ErrorMessages.ShortnameMissing))
else
Validation
.fromOption(validateAndEscapeProjectShortname(value))
.fromOption(validateAndEscape(value))
.mapError(_ => ValidationException(ErrorMessages.ShortnameInvalid(value)))
.map(new Shortname(_) {})

def make(value: Option[String]): Validation[ValidationException, Option[Shortname]] =
value match {
case Some(v) => self.make(v).map(Some(_))
case None => Validation.succeed(None)
private def validateAndEscape(shortname: String): Option[String] = {
val defaultSharedOntologiesProject = "DefaultSharedOntologiesProject"
if (shortname == defaultSharedOntologiesProject) {
Some(defaultSharedOntologiesProject)
} else {
shortnameRegex
.findFirstIn(shortname)
.flatMap(Iri.toSparqlEncodedString)
}
}
}

/**
Expand Down
Expand Up @@ -8,17 +8,19 @@ package org.knora.webapi.slice.admin.domain.model
import zio.NonEmptyChunk

import dsp.valueobjects.Project.Shortcode
import dsp.valueobjects.Project.Shortname
import dsp.valueobjects.V2.StringLiteralV2
import org.knora.webapi.slice.resourceinfo.domain.InternalIri

case class KnoraProject(
id: InternalIri,
shortname: String,
shortname: Shortname,
shortcode: Shortcode,
longname: Option[String],
description: NonEmptyChunk[StringLiteralV2],
keywords: List[String],
logo: Option[String],
status: Boolean,
selfjoin: Boolean
selfjoin: Boolean,
ontologies: List[InternalIri]
)
Expand Up @@ -9,14 +9,13 @@ import zio.Task
import dsp.valueobjects.Project.Shortcode
import dsp.valueobjects.RestrictedViewSize
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.ShortcodeIdentifier
import org.knora.webapi.slice.admin.domain.model.KnoraProject
import org.knora.webapi.slice.common.repo.service.Repository
import org.knora.webapi.slice.resourceinfo.domain.InternalIri

trait KnoraProjectRepo extends Repository[KnoraProject, InternalIri] {
def findById(id: ProjectIdentifierADM): Task[Option[KnoraProject]]
def findByShortcode(shortcode: Shortcode): Task[Option[KnoraProject]] = findById(ShortcodeIdentifier(shortcode))
def findOntologies(project: KnoraProject): Task[List[InternalIri]]
def findByShortcode(code: Shortcode): Task[Option[KnoraProject]] =
findById(ProjectIdentifierADM.ShortcodeIdentifier(code))
def setProjectRestrictedViewSize(project: KnoraProject, size: RestrictedViewSize): Task[Unit]
}
Expand Up @@ -8,6 +8,7 @@ package org.knora.webapi.slice.admin.domain.service
import zio._

import dsp.valueobjects.Project.Shortcode
import dsp.valueobjects.Project.Shortname
import dsp.valueobjects.RestrictedViewSize
import org.knora.webapi.messages.OntologyConstants
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM
Expand Down Expand Up @@ -38,10 +39,9 @@ object ProjectADMService {
* @return the [[InternalIri]] of the project's data named graph.
*/
def projectDataNamedGraphV2(project: ProjectADM): InternalIri = {
val shortcode = Shortcode
.make(project.shortcode)
.getOrElse(throw new IllegalArgumentException(s"Invalid project shortcode: ${project.shortcode}"))
projectDataNamedGraphV2(shortcode, project.shortname)
val shortcode = Shortcode.unsafeFrom(project.shortcode)
val shortname = Shortname.unsafeFrom(project.shortname)
projectDataNamedGraphV2(shortcode, shortname)
}

/**
Expand All @@ -53,8 +53,8 @@ object ProjectADMService {
def projectDataNamedGraphV2(project: KnoraProject): InternalIri =
projectDataNamedGraphV2(project.shortcode, project.shortname)

private def projectDataNamedGraphV2(shortcode: Shortcode, projectShortname: String) =
InternalIri(s"${OntologyConstants.NamedGraphs.DataNamedGraphStart}/${shortcode.value}/$projectShortname")
private def projectDataNamedGraphV2(shortcode: Shortcode, shortname: Shortname) =
InternalIri(s"${OntologyConstants.NamedGraphs.DataNamedGraphStart}/${shortcode.value}/${shortname.value}")
}

final case class ProjectADMServiceLive(
Expand All @@ -67,33 +67,33 @@ final case class ProjectADMServiceLive(
projectRepo.findById(projectId).flatMap(ZIO.foreach(_)(toProjectADM))

private def toProjectADM(knoraProject: KnoraProject): Task[ProjectADM] =
for {
ontologyIris <- projectRepo.findOntologies(knoraProject)
} yield ProjectADM(
id = knoraProject.id.value,
shortname = knoraProject.shortname,
shortcode = knoraProject.shortcode.value,
longname = knoraProject.longname,
description = knoraProject.description,
keywords = knoraProject.keywords,
logo = knoraProject.logo,
status = knoraProject.status,
selfjoin = knoraProject.selfjoin,
ontologies = ontologyIris.map(_.value)
).unescape
ZIO.attempt(
ProjectADM(
id = knoraProject.id.value,
shortname = knoraProject.shortname.value,
shortcode = knoraProject.shortcode.value,
longname = knoraProject.longname,
description = knoraProject.description,
keywords = knoraProject.keywords,
logo = knoraProject.logo,
status = knoraProject.status,
selfjoin = knoraProject.selfjoin,
ontologies = knoraProject.ontologies.map(_.value)
).unescape
)

private def toKnoraProject(project: ProjectADM): KnoraProject =
KnoraProject(
id = InternalIri.apply(project.id),
shortname = project.shortname,
shortcode =
Shortcode.make(project.shortcode).getOrElse(throw new IllegalArgumentException("Should not happened.")),
shortname = Shortname.unsafeFrom(project.shortname),
shortcode = Shortcode.unsafeFrom(project.shortcode),
longname = project.longname,
description = NonEmptyChunk.fromIterable(project.description.head, project.description.tail),
keywords = project.keywords.toList,
logo = project.logo,
status = project.status,
selfjoin = project.selfjoin
selfjoin = project.selfjoin,
ontologies = project.ontologies.map(InternalIri.apply).toList
)

override def findAllProjectsKeywords: Task[ProjectsKeywordsGetResponseADM] =
Expand Down
Expand Up @@ -131,13 +131,13 @@ final case class ProjectExportServiceLive(

override def exportProjectTriples(project: KnoraProject): Task[Path] =
Files
.createTempDirectory(Some(project.shortname), fileAttributes = Nil)
.createTempDirectory(Some(project.shortname.value), fileAttributes = Nil)
.map(trigExportFilePath(project, _))
.flatMap(exportProjectTriples(project, _))

override def exportProjectTriples(project: KnoraProject, targetFile: Path): Task[Path] = ZIO.scoped {
for {
tempDir <- Files.createTempDirectoryScoped(Some(project.shortname), fileAttributes = Nil)
tempDir <- Files.createTempDirectoryScoped(Some(project.shortname.value), fileAttributes = Nil)
ontologyAndData <- downloadOntologyAndData(project, tempDir)
adminData <- downloadProjectAdminData(project, tempDir)
permissionData <- downloadPermissionData(project, tempDir)
Expand Down
Expand Up @@ -11,36 +11,28 @@ import zio._
import dsp.valueobjects.Project
import dsp.valueobjects.RestrictedViewSize
import dsp.valueobjects.V2
import org.knora.webapi.messages.OntologyConstants.KnoraAdmin.HasSelfJoinEnabled
import org.knora.webapi.messages.OntologyConstants.KnoraAdmin.ProjectDescription
import org.knora.webapi.messages.OntologyConstants.KnoraAdmin.ProjectKeyword
import org.knora.webapi.messages.OntologyConstants.KnoraAdmin.ProjectLogo
import org.knora.webapi.messages.OntologyConstants.KnoraAdmin.ProjectLongname
import org.knora.webapi.messages.OntologyConstants.KnoraAdmin.ProjectShortcode
import org.knora.webapi.messages.OntologyConstants.KnoraAdmin.ProjectShortname
import org.knora.webapi.messages.OntologyConstants.KnoraAdmin.Status
import org.knora.webapi.messages.OntologyConstants.KnoraAdmin._
import org.knora.webapi.messages.StringFormatter
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM
import org.knora.webapi.messages.store.triplestoremessages.BooleanLiteralV2
import org.knora.webapi.messages.store.triplestoremessages.IriLiteralV2
import org.knora.webapi.messages.store.triplestoremessages.SparqlExtendedConstructResponse.ConstructPredicateObjects
import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2
import org.knora.webapi.messages.store.triplestoremessages.SubjectV2
import org.knora.webapi.messages.twirl.queries.sparql
import org.knora.webapi.slice.admin.domain.model.KnoraProject
import org.knora.webapi.slice.admin.domain.service.KnoraProjectRepo
import org.knora.webapi.slice.common.repo.service.PredicateObjectMapper
import org.knora.webapi.slice.ontology.domain.service.OntologyRepo
import org.knora.webapi.slice.resourceinfo.domain.InternalIri
import org.knora.webapi.store.triplestore.api.TriplestoreService
import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Construct
import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Select
import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Update

final case class KnoraProjectRepoLive(
private val triplestore: TriplestoreService,
private val mapper: PredicateObjectMapper
private val mapper: PredicateObjectMapper,
private implicit val sf: StringFormatter
) extends KnoraProjectRepo {
implicit val sf: StringFormatter = StringFormatter.getGeneralInstance

override def findById(id: InternalIri): Task[Option[KnoraProject]] =
findOneByQuery(sparql.admin.txt.getProjects(maybeIri = Some(id.value), None, None))
Expand Down Expand Up @@ -73,7 +65,9 @@ final case class KnoraProjectRepoLive(
val projectIri = InternalIri(subjectPropsTuple._1.toString)
val propsMap = subjectPropsTuple._2
for {
shortname <- mapper.getSingleOrFail[StringLiteralV2](ProjectShortname, propsMap).map(_.value)
shortname <- mapper
.getSingleOrFail[StringLiteralV2](ProjectShortname, propsMap)
.flatMap(it => Project.Shortname.make(it.value).toZIO)
shortcode <- mapper
.getSingleOrFail[StringLiteralV2](ProjectShortcode, propsMap)
.flatMap(it => Project.Shortcode.make(it.value).toZIO)
Expand All @@ -85,26 +79,22 @@ final case class KnoraProjectRepoLive(
logo <- mapper.getSingleOption[StringLiteralV2](ProjectLogo, propsMap).map(_.map(_.value))
status <- mapper.getSingleOrFail[BooleanLiteralV2](Status, propsMap).map(_.value)
selfjoin <- mapper.getSingleOrFail[BooleanLiteralV2](HasSelfJoinEnabled, propsMap).map(_.value)
} yield KnoraProject(projectIri, shortname, shortcode, longname, description, keywords, logo, status, selfjoin)
}

override def findOntologies(project: KnoraProject): Task[List[InternalIri]] = {
val query =
s"""
|PREFIX owl: <http://www.w3.org/2002/07/owl#>
|PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
|PREFIX knora-admin: <http://www.knora.org/ontology/knora-admin#>
|PREFIX knora-base: <http://www.knora.org/ontology/knora-base#>
|
|SELECT ?ontologyIri WHERE {
| BIND(<${project.id.value}> AS ?projectIri)
| ?ontologyIri a owl:Ontology .
| ?ontologyIri knora-base:attachedToProject ?projectIri .
| ?projectIri a knora-admin:knoraProject .
|} order by ?projectIri""".stripMargin
triplestore
.query(Select(query))
.map(_.results.bindings.flatMap(_.rowMap.get("ontologyIri")).map(InternalIri).toList)
ontologies <-
mapper
.getListOption[IriLiteralV2]("http://www.knora.org/ontology/knora-admin#belongsToOntology", propsMap)
.map(_.getOrElse(List.empty).map(literal => InternalIri(literal.value)))
} yield KnoraProject(
projectIri,
shortname,
shortcode,
longname,
description,
keywords,
logo,
status,
selfjoin,
ontologies
)
}

override def setProjectRestrictedViewSize(
Expand All @@ -118,6 +108,5 @@ final case class KnoraProjectRepoLive(
}

object KnoraProjectRepoLive {
val layer: URLayer[TriplestoreService with OntologyRepo with PredicateObjectMapper, KnoraProjectRepoLive] =
ZLayer.fromFunction(KnoraProjectRepoLive.apply _)
val layer = ZLayer.derive[KnoraProjectRepoLive]
}
Expand Up @@ -21,23 +21,31 @@
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX knora-admin: <http://www.knora.org/ontology/knora-admin#>
PREFIX knora-base: <http://www.knora.org/ontology/knora-base#>
PREFIX owl: <http://www.w3.org/2002/07/owl#>

CONSTRUCT { ?s ?p ?o . }

CONSTRUCT {
?project ?p ?o .
?project knora-admin:belongsToOntology ?ontology .
}
WHERE {

@if(maybeIri.nonEmpty) {
BIND(IRI("@maybeIri") as ?s)
BIND(IRI("@maybeIri") as ?project)
}

@if(maybeShortname.nonEmpty) {
?s knora-admin:projectShortname "@maybeShortname.get"^^xsd:string .
?project knora-admin:projectShortname "@maybeShortname.get"^^xsd:string .
}

@if(maybeShortcode.nonEmpty) {
?s knora-admin:projectShortcode "@maybeShortcode.get"^^xsd:string .
?project knora-admin:projectShortcode "@maybeShortcode.get"^^xsd:string .
}

?s rdf:type knora-admin:knoraProject .
?s ?p ?o .
?project a knora-admin:knoraProject .
OPTIONAL{
?ontology a owl:Ontology .
?ontology knora-base:attachedToProject ?project .
}
?project ?p ?o .
}
5 changes: 0 additions & 5 deletions webapi/src/test/scala/dsp/valueobjects/ProjectSpec.scala
Expand Up @@ -101,11 +101,6 @@ object ProjectSpec extends ZIOSpecDefault {
Shortname.make(param).map(_.value) == Validation.succeed(param)
) && assert(Shortname.make(param).toOption)(isSome(isSubtype[Shortname](Assertion.anything)))
}
},
test("successfully validate passing None") {
assertTrue(
Shortname.make(None) == Validation.succeed(None)
)
}
)

Expand Down
5 changes: 3 additions & 2 deletions webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala
Expand Up @@ -21,14 +21,15 @@ object TestDataFactory {

val someProject = KnoraProject(
InternalIri("http://rdfh.ch/projects/0001"),
"shortname",
Shortname.unsafeFrom("shortname"),
Shortcode.unsafeFrom("0001"),
None,
NonEmptyChunk(V2.StringLiteralV2("Some description", None)),
List.empty,
None,
true,
false
false,
List.empty
)

def projectShortcodeIdentifier(shortcode: String): ShortcodeIdentifier =
Expand Down
Expand Up @@ -21,6 +21,7 @@ import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectC
import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectUpdateRequest
import org.knora.webapi.slice.admin.api.service.ProjectADMRestService
import org.knora.webapi.slice.admin.api.service.ProjectsADMRestServiceLive
import org.knora.webapi.slice.admin.domain.repo.KnoraProjectRepoInMemory
import org.knora.webapi.slice.admin.domain.service.DspIngestClientMock
import org.knora.webapi.slice.admin.domain.service.ProjectExportServiceStub
import org.knora.webapi.slice.admin.domain.service.ProjectExportStorageServiceLive
Expand Down Expand Up @@ -65,7 +66,7 @@ object ProjectsServiceLiveSpec extends ZIOSpecDefault {
exp.toLayer,
org.knora.webapi.slice.common.api.RestPermissionServiceLive.layer,
ProjectExportServiceStub.layer,
org.knora.webapi.slice.admin.domain.service.KnoraProjectRepoInMemory.layer,
KnoraProjectRepoInMemory.layer,
ProjectImportServiceLive.layer,
ProjectExportStorageServiceLive.layer,
AppConfig.layer,
Expand Down

0 comments on commit 21550ce

Please sign in to comment.