Skip to content

Commit

Permalink
feat: Add maintenance service for fixing top-left dimension values DE…
Browse files Browse the repository at this point in the history
  • Loading branch information
seakayone authored Oct 11, 2023
1 parent dd2dfe6 commit 82b715a
Show file tree
Hide file tree
Showing 9 changed files with 368 additions and 19 deletions.
8 changes: 7 additions & 1 deletion project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ object Dependencies {
val zioPrelude = "dev.zio" %% "zio-prelude" % ZioPreludeVersion
val zioSttp = "com.softwaremill.sttp.client3" %% "zio" % "3.9.0"

// refined
val refined = Seq(
"eu.timepit" %% "refined" % "0.11.0",
"dev.zio" %% "zio-json-interop-refined" % "0.6.2"
)

// zio-test and friends
val zioTest = "dev.zio" %% "zio-test" % ZioVersion
val zioTestSbt = "dev.zio" %% "zio-test-sbt" % ZioVersion
Expand Down Expand Up @@ -135,7 +141,7 @@ object Dependencies {

val webapiTestDependencies = Seq(zioTest, zioTestSbt, zioMock, wiremock).map(_ % Test)

val webapiDependencies = Seq(
val webapiDependencies = refined ++ Seq(
pekkoActor,
pekkoHttp,
pekkoHttpCors,
Expand Down
3 changes: 3 additions & 0 deletions webapi/src/main/scala/dsp/valueobjects/Project.scala
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ object Project {
implicit val encoder: JsonEncoder[Shortcode] =
JsonEncoder[String].contramap((shortcode: Shortcode) => shortcode.value)

def unsafeFrom(str: String) = make(str)
.getOrElse(throw new IllegalArgumentException(s"Invalid project shortcode: $str"))

def make(value: String): Validation[ValidationException, Shortcode] =
if (value.isEmpty) {
Validation.fail(ValidationException(ProjectErrorMessages.ShortcodeMissing))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright © 2021 - 2023 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors.
* SPDX-License-Identifier: Apache-2.0
*/

package org.knora.webapi.slice.admin.api.model

import eu.timepit.refined.api.Refined
import eu.timepit.refined.api.RefinedTypeOps
import eu.timepit.refined.numeric.Positive
import eu.timepit.refined.refineV
import eu.timepit.refined.string.MatchesRegex
import zio.Chunk
import zio.json.DeriveJsonCodec
import zio.json.JsonCodec
import zio.json.interop.refined._

import dsp.valueobjects.Project.Shortcode

object MaintenanceRequests {

type AssetId = String Refined MatchesRegex["^[a-zA-Z0-9-_]{4,}$"]
object AssetId extends RefinedTypeOps[AssetId, String] {
implicit val codec: JsonCodec[AssetId] = JsonCodec[String].transformOrFail(AssetId.from, _.toString)
}

final case class Dimensions(width: Int Refined Positive, height: Int Refined Positive)

object Dimensions {
def from(width: Int, height: Int): Either[String, Dimensions] = for {
widthRefined <- refineV[Positive](width)
heightRefined <- refineV[Positive](height)
} yield Dimensions(widthRefined, heightRefined)

implicit val codec: JsonCodec[Dimensions] = DeriveJsonCodec.gen[Dimensions]
}

case class ReportAsset(id: AssetId, dimensions: Dimensions)

object ReportAsset {
implicit val codec: JsonCodec[ReportAsset] = DeriveJsonCodec.gen[ReportAsset]
}

case class ProjectWithBakFiles(id: Shortcode, assetIds: Chunk[ReportAsset])

object ProjectWithBakFiles {
implicit val codec: JsonCodec[ProjectWithBakFiles] = DeriveJsonCodec.gen[ProjectWithBakFiles]
}

case class ProjectsWithBakfilesReport(projects: Chunk[ProjectWithBakFiles])

object ProjectsWithBakfilesReport {
implicit val codec: JsonCodec[ProjectsWithBakfilesReport] = DeriveJsonCodec.gen[ProjectsWithBakfilesReport]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* Copyright © 2021 - 2023 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors.
* SPDX-License-Identifier: Apache-2.0
*/

package org.knora.webapi.slice.admin.api.service

import zio.Task
import zio.ZIO
import zio.ZLayer
import zio.macros.accessible
import zio.stream.ZStream

import dsp.valueobjects.Project.Shortcode
import org.knora.webapi.slice.admin.api.model.MaintenanceRequests.AssetId
import org.knora.webapi.slice.admin.api.model.MaintenanceRequests.Dimensions
import org.knora.webapi.slice.admin.api.model.MaintenanceRequests.ProjectsWithBakfilesReport
import org.knora.webapi.slice.admin.api.model.MaintenanceRequests.ReportAsset
import org.knora.webapi.slice.admin.domain.model.KnoraProject
import org.knora.webapi.slice.admin.domain.service.KnoraProjectRepo
import org.knora.webapi.slice.admin.domain.service.ProjectADMService
import org.knora.webapi.slice.common.repo.service.PredicateObjectMapper
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.Select
import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Update

@accessible
trait MaintenanceService {
def fixTopLeftDimensions(report: ProjectsWithBakfilesReport): Task[Unit]

def doNothing: Task[Unit]
}

final case class MaintenanceServiceLive(
projectRepo: KnoraProjectRepo,
triplestoreService: TriplestoreService,
mapper: PredicateObjectMapper
) extends MaintenanceService {
override def doNothing: Task[Unit] = ZIO.unit
override def fixTopLeftDimensions(report: ProjectsWithBakfilesReport): Task[Unit] =
ZStream
.fromIterable(report.projects)
.flatMap { project =>
ZStream
.fromIterable(project.assetIds)
.flatMapPar(5)(assetId =>
ZStream.fromZIOOption(
fixAsset(project.id, assetId)
// None.type errors are just a sign that the assetId should be ignored. Some.type errors are real errors.
.tapSomeError { case Some(e) => ZIO.logError(s"Error while processing ${project.id}, $assetId: $e") }
// We have logged real errors above, from here on out ignore all errors so that the stream can continue.
.orElseFail(None)
)
)
}
.runDrain

private def fixAsset(shortcode: Shortcode, asset: ReportAsset): ZIO[Any, Option[Throwable], Unit] =
for {
project <- projectRepo.findByShortcode(shortcode).some
stillImageFileValueIri <- checkDimensions(project, asset)
_ <- transposeImageDimensions(project, stillImageFileValueIri)
} yield ()

private def checkDimensions(
project: KnoraProject,
asset: ReportAsset
): ZIO[Any, Option[Throwable], InternalIri] =
for {
res <- getDimensionAndStillImageValueIri(project, asset)
(dim, iri) = res
_ <- ZIO.when(dim == asset.dimensions)(ZIO.fail(None))
} yield iri

private def getDimensionAndStillImageValueIri(
project: KnoraProject,
asset: ReportAsset
): ZIO[Any, Option[Throwable], (Dimensions, InternalIri)] =
for {
result <- triplestoreService.query(checkDimensionsQuery(project, asset.id)).asSomeError
rowMap <- ZIO.fromOption(result.results.bindings.headOption.map(_.rowMap))
iri <- ZIO.fromOption(rowMap.get("valueIri")).map(InternalIri)
width <- ZIO.fromOption(rowMap.get("dimX").flatMap(_.toIntOption))
height <- ZIO.fromOption(rowMap.get("dimY").flatMap(_.toIntOption))
dim <- ZIO.fromOption(Dimensions.from(width, height).toOption)
} yield (dim, iri)

private def checkDimensionsQuery(project: KnoraProject, assetId: AssetId) = {
val projectGraph = ProjectADMService.projectDataNamedGraphV2(project)
Select(s"""
|PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|PREFIX knora-base: <http://www.knora.org/ontology/knora-base#>
|
|SELECT ?valueIri ?dimX ?dimY
| FROM <${projectGraph.value}>
|WHERE {
| ?valueIri a knora-base:StillImageFileValue .
| ?valueIri knora-base:internalFilename ?filename .
| FILTER (strstarts(str(?filename), "$assetId"))
| ?valueIri knora-base:dimX ?dimX .
| ?valueIri knora-base:dimY ?dimY .
|}
|""".stripMargin)
}

private def transposeImageDimensions(
project: KnoraProject,
stillImageFileValueIri: InternalIri
): ZIO[Any, Option[Throwable], Unit] =
triplestoreService.query(transposeUpdate(project, stillImageFileValueIri)).asSomeError

private def transposeUpdate(project: KnoraProject, stillImageFileValueIri: InternalIri) = {
val projectGraph = ProjectADMService.projectDataNamedGraphV2(project)
Update(
s"""
|PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|PREFIX knora-base: <http://www.knora.org/ontology/knora-base#>
|
|WITH <${projectGraph.value}>
|DELETE
|{
| ?r knora-base:dimX ?oldX .
| ?r knora-base:dimY ?oldY .
|}
|INSERT
|{
| ?r knora-base:dimX ?oldY .
| ?r knora-base:dimY ?oldX .
|}
|WHERE
|{
| BIND (<${stillImageFileValueIri.value}> AS ?r)
| ?r knora-base:dimX ?oldX .
| ?r knora-base:dimY ?oldY .
|}
|""".stripMargin
)
}
}

object MaintenanceServiceLive {

val layer = ZLayer.derive[MaintenanceServiceLive]
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@
*/

package org.knora.webapi.store.triplestore.api
import org.apache.jena.query.Dataset
import org.apache.jena.query.QueryExecution
import org.apache.jena.query.QueryExecutionFactory
import org.apache.jena.query.QuerySolution
import org.apache.jena.query.ReadWrite
import org.apache.jena.query.ResultSet
import org.apache.jena.query._
import org.apache.jena.rdf.model.Model
import org.apache.jena.rdf.model.ModelFactory
import org.apache.jena.tdb2.TDB2Factory
Expand All @@ -19,9 +14,11 @@ import zio.Ref
import zio.Scope
import zio.Task
import zio.UIO
import zio.ULayer
import zio.URIO
import zio.ZIO
import zio.ZLayer
import zio.macros.accessible

import java.nio.charset.StandardCharsets
import java.nio.file.Path
Expand All @@ -33,15 +30,7 @@ import org.knora.webapi.IRI
import org.knora.webapi.messages.StringFormatter
import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject
import org.knora.webapi.messages.store.triplestoremessages.SparqlConstructResponse
import org.knora.webapi.messages.util.rdf.QuadFormat
import org.knora.webapi.messages.util.rdf.RdfFeatureFactory
import org.knora.webapi.messages.util.rdf.RdfFormatUtil
import org.knora.webapi.messages.util.rdf.RdfStringSource
import org.knora.webapi.messages.util.rdf.SparqlSelectResult
import org.knora.webapi.messages.util.rdf.SparqlSelectResultBody
import org.knora.webapi.messages.util.rdf.SparqlSelectResultHeader
import org.knora.webapi.messages.util.rdf.Turtle
import org.knora.webapi.messages.util.rdf.VariableResultsRow
import org.knora.webapi.messages.util.rdf._
import org.knora.webapi.messages.util.rdf.jenaimpl.JenaFormatUtil
import org.knora.webapi.slice.resourceinfo.domain.InternalIri
import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Ask
Expand All @@ -59,8 +48,14 @@ import org.knora.webapi.util.ZScopedJavaIoStreams.byteArrayOutputStream
import org.knora.webapi.util.ZScopedJavaIoStreams.fileInputStream
import org.knora.webapi.util.ZScopedJavaIoStreams.fileOutputStream

@accessible
trait TestTripleStore extends TriplestoreService {
def setDataset(ds: Dataset): UIO[Unit]
}

final case class TriplestoreServiceInMemory(datasetRef: Ref[Dataset], implicit val sf: StringFormatter)
extends TriplestoreService {
extends TriplestoreService
with TestTripleStore {
private val rdfFormatUtil: RdfFormatUtil = RdfFeatureFactory.getRdfFormatUtil()

override def query(query: Select): Task[SparqlSelectResult] = {
Expand Down Expand Up @@ -240,6 +235,9 @@ final case class TriplestoreServiceInMemory(datasetRef: Ref[Dataset], implicit v

override def uploadRepository(inputFile: Path): Task[Unit] =
ZIO.fail(new UnsupportedOperationException("Not implemented in TriplestoreServiceInMemory."))

override def setDataset(ds: Dataset): UIO[Unit] =
datasetRef.set(ds)
}

object TriplestoreServiceInMemory {
Expand All @@ -252,6 +250,8 @@ object TriplestoreServiceInMemory {
*/
val createEmptyDataset: UIO[Dataset] = ZIO.succeed(TDB2Factory.createDataset())

val layer: ZLayer[Ref[Dataset] with StringFormatter, Nothing, TriplestoreService] =
val emptyDatasetRefLayer: ULayer[Ref[Dataset]] = ZLayer.fromZIO(createEmptyDataset.flatMap(Ref.make(_)))

val layer: ZLayer[Ref[Dataset] with StringFormatter, Nothing, TestTripleStore with TriplestoreService] =
ZLayer.fromFunction(TriplestoreServiceInMemory.apply _)
}
17 changes: 17 additions & 0 deletions webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,32 @@

package org.knora.webapi

import zio.NonEmptyChunk

import dsp.valueobjects.Iri._
import dsp.valueobjects.Project._
import dsp.valueobjects.V2
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM._
import org.knora.webapi.slice.admin.domain.model.KnoraProject
import org.knora.webapi.slice.resourceinfo.domain.InternalIri

/**
* Helps in creating value objects for tests.
*/
object TestDataFactory {

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

def projectShortcodeIdentifier(shortcode: String): ShortcodeIdentifier =
ShortcodeIdentifier
.fromString(shortcode)
Expand Down
Loading

0 comments on commit 82b715a

Please sign in to comment.