Skip to content

Commit

Permalink
refactor: Remove KnoraApiCreateValueModel and move code to service …
Browse files Browse the repository at this point in the history
…which is directly assembling the `CreateValueV2` (DEV-4305)  (#3413)
  • Loading branch information
seakayone authored Nov 6, 2024
1 parent cc42d41 commit 34be717
Show file tree
Hide file tree
Showing 10 changed files with 1,002 additions and 766 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import org.knora.webapi.slice.admin.api.service.ProjectRestService
import org.knora.webapi.slice.admin.api.service.UserRestService
import org.knora.webapi.slice.admin.domain.service.*
import org.knora.webapi.slice.admin.domain.service.ProjectExportStorageService
import org.knora.webapi.slice.common.ApiComplexV2JsonLdRequestParser
import org.knora.webapi.slice.common.api.*
import org.knora.webapi.slice.common.repo.service.PredicateObjectMapper
import org.knora.webapi.slice.infrastructure.CacheManager
Expand Down Expand Up @@ -102,6 +103,7 @@ object LayersTest {
// format: off
AdminApiEndpoints &
AdminModule.Provided &
ApiComplexV2JsonLdRequestParser &
ApiRoutes &
AppRouter &
AuthenticationApiModule.Provided &
Expand Down Expand Up @@ -167,6 +169,7 @@ object LayersTest {
ZLayer.makeSome[CommonR0, CommonR](
AdminApiModule.layer,
AdminModule.layer,
ApiComplexV2JsonLdRequestParser.layer,
ApiRoutes.layer,
AppRouter.layer,
AssetPermissionsResponder.layer,
Expand Down
3 changes: 3 additions & 0 deletions webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import org.knora.webapi.slice.admin.api.service.PermissionRestService
import org.knora.webapi.slice.admin.api.service.ProjectRestService
import org.knora.webapi.slice.admin.api.service.UserRestService
import org.knora.webapi.slice.admin.domain.service.*
import org.knora.webapi.slice.common.ApiComplexV2JsonLdRequestParser
import org.knora.webapi.slice.common.api.*
import org.knora.webapi.slice.common.repo.service.PredicateObjectMapper
import org.knora.webapi.slice.infrastructure.InfrastructureModule
Expand Down Expand Up @@ -82,6 +83,7 @@ object LayersLive {
ActorSystem &
AdminApiEndpoints &
AdminModule.Provided &
ApiComplexV2JsonLdRequestParser &
ApiRoutes &
ApiV2Endpoints &
AppConfigurations &
Expand Down Expand Up @@ -139,6 +141,7 @@ object LayersLive {
ZLayer.make[DspEnvironmentLive](
AdminApiModule.layer,
AdminModule.layer,
ApiComplexV2JsonLdRequestParser.layer,
ApiRoutes.layer,
ApiV2Endpoints.layer,
AppConfig.layer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ import org.knora.webapi.slice.admin.api.model.Project
import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode
import org.knora.webapi.slice.admin.domain.model.Permission
import org.knora.webapi.slice.admin.domain.model.User
import org.knora.webapi.slice.common.KnoraApiCreateValueModel
import org.knora.webapi.slice.common.jena.JenaConversions.given
import org.knora.webapi.slice.common.jena.ResourceOps.*
import org.knora.webapi.slice.resourceinfo.domain.InternalIri
Expand Down Expand Up @@ -566,41 +565,6 @@ case class CreateValueV2(
ingestState: AssetIngestState = AssetInTemp,
)

/**
* Constructs [[CreateValueV2]] instances based on JSON-LD input.
*/
object CreateValueV2 {

/**
* Converts JSON-LD input to a [[CreateValueV2]].
*
* @param ingestState indicates the state of the file, either ingested or in temp folder
* @param jsonLdString JSON-LD input as String.
* @return a case class instance representing the input.
*/
def fromJsonLd(
ingestState: AssetIngestState,
jsonLdString: String,
): ZIO[SipiService & IriConverter & MessageRelay, Throwable, CreateValueV2] = ZIO.scoped {
for {
converter <- ZIO.service[IriConverter]
model <- KnoraApiCreateValueModel.fromJsonLd(jsonLdString, converter).mapError(BadRequestException(_))
fileInfo <- ValueContentV2.fileInfoFromExternal(model.valueFileValueFilename, ingestState, model.shortcode)
valueContent <- model.getValueContent(fileInfo).mapError(BadRequestException(_))
} yield CreateValueV2(
resourceIri = model.resourceIri.toString,
resourceClassIri = model.resourceClassIri.smartIri,
propertyIri = model.valuePropertyIri.smartIri,
valueContent = valueContent,
valueIri = model.valueIri.map(_.smartIri),
valueUUID = model.valueUuid,
valueCreationDate = model.valueCreationDate,
permissions = model.valuePermissions,
ingestState = ingestState,
)
}
}

/** A trait for classes representing information to be updated in a value. */
sealed trait UpdateValueV2 {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import org.knora.webapi.routing.v2.*
import org.knora.webapi.slice.admin.api.AdminApiRoutes
import org.knora.webapi.slice.admin.domain.service.ProjectService
import org.knora.webapi.slice.admin.domain.service.UserService
import org.knora.webapi.slice.common.ApiComplexV2JsonLdRequestParser
import org.knora.webapi.slice.common.api.AuthorizationRestService
import org.knora.webapi.slice.infrastructure.api.ManagementRoutes
import org.knora.webapi.slice.lists.api.ListsApiV2Routes
Expand Down Expand Up @@ -98,9 +99,9 @@ final case class ApiRoutes(
object ApiRoutes {

private type ApiRoutesRuntime =
AppConfig & AuthenticationApiRoutes & AuthorizationRestService & core.State & IriConverter & MessageRelay &
ProjectService & RestCardinalityService & WebApiAuthenticator & SearchApiRoutes & SearchResponderV2 &
SipiService & StringFormatter & UserService & ValuesResponderV2 & ListsApiV2Routes
ApiComplexV2JsonLdRequestParser & AppConfig & AuthenticationApiRoutes & AuthorizationRestService & core.State &
IriConverter & MessageRelay & ProjectService & RestCardinalityService & WebApiAuthenticator & SearchApiRoutes &
SearchResponderV2 & SipiService & StringFormatter & UserService & ValuesResponderV2 & ListsApiV2Routes

/**
* All routes composed together.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import org.knora.webapi.messages.v2.responder.valuemessages.*
import org.knora.webapi.responders.v2.ValuesResponderV2
import org.knora.webapi.routing.RouteUtilV2
import org.knora.webapi.routing.RouteUtilZ
import org.knora.webapi.slice.common.ApiComplexV2JsonLdRequestParser
import org.knora.webapi.slice.resourceinfo.domain.IriConverter
import org.knora.webapi.slice.security.Authenticator
import org.knora.webapi.store.iiif.api.SipiService
Expand All @@ -30,7 +31,8 @@ import org.knora.webapi.store.iiif.api.SipiService
*/
final case class ValuesRouteV2()(
private implicit val runtime: Runtime[
AppConfig & Authenticator & IriConverter & SipiService & StringFormatter & MessageRelay & ValuesResponderV2,
ApiComplexV2JsonLdRequestParser & AppConfig & Authenticator & IriConverter & SipiService & StringFormatter &
MessageRelay & ValuesResponderV2,
],
) {

Expand Down Expand Up @@ -84,7 +86,9 @@ final case class ValuesRouteV2()(
requestingUser <- ZIO.serviceWithZIO[Authenticator](_.getUserADM(ctx))
apiRequestId <- Random.nextUUID
ingestState = AssetIngestState.headerAssetIngestState(ctx.request.headers)
valueToCreate <- CreateValueV2.fromJsonLd(ingestState, jsonLdString)
valueToCreate <- ZIO.serviceWithZIO[ApiComplexV2JsonLdRequestParser](
_.createValueV2FromJsonLd(jsonLdString, ingestState).mapError(BadRequestException(_)),
)
response <-
ZIO.serviceWithZIO[ValuesResponderV2](_.createValueV2(valueToCreate, requestingUser, apiRequestId))
} yield response,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/*
* Copyright © 2021 - 2024 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors.
* SPDX-License-Identifier: Apache-2.0
*/

package org.knora.webapi.slice.common
import org.apache.jena.rdf.model.*
import org.apache.jena.vocabulary.RDF
import zio.*
import zio.ZIO
import zio.ZLayer

import java.time.Instant
import java.util.UUID
import scala.jdk.CollectionConverters.*
import scala.language.implicitConversions

import dsp.valueobjects.UuidUtil
import org.knora.webapi.core.MessageRelay
import org.knora.webapi.messages.OntologyConstants
import org.knora.webapi.messages.OntologyConstants.KnoraApiV2Complex.*
import org.knora.webapi.messages.OntologyConstants.Xsd
import org.knora.webapi.messages.ValuesValidator
import org.knora.webapi.messages.v2.responder.valuemessages.*
import org.knora.webapi.messages.v2.responder.valuemessages.ValueContentV2.FileInfo
import org.knora.webapi.routing.v2.AssetIngestState
import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode
import org.knora.webapi.slice.common.KnoraIris.*
import org.knora.webapi.slice.common.KnoraIris.ResourceClassIri as KResourceClassIri
import org.knora.webapi.slice.common.KnoraIris.ResourceIri
import org.knora.webapi.slice.common.KnoraIris.ResourceIri as KResourceIri
import org.knora.webapi.slice.common.jena.JenaConversions.given
import org.knora.webapi.slice.common.jena.ModelOps
import org.knora.webapi.slice.common.jena.ModelOps.*
import org.knora.webapi.slice.common.jena.ResourceOps.*
import org.knora.webapi.slice.common.jena.StatementOps.*
import org.knora.webapi.slice.resourceinfo.domain.IriConverter
import org.knora.webapi.store.iiif.api.SipiService

final case class ApiComplexV2JsonLdRequestParser(
converter: IriConverter,
messageRelay: MessageRelay,
sipiService: SipiService,
) {

def createValueV2FromJsonLd(str: String, ingestState: AssetIngestState): IO[String, CreateValueV2] =
ZIO.scoped {
for {
model <- ModelOps.fromJsonLd(str)
resourceAndIri <- resourceAndIri(model)
(resource, resourceIri) = resourceAndIri
resourceClassIri <- resourceClassIri(resource)
valueStatement <- valueStatement(resource)
valuePropertyIri <- valuePropertyIri(valueStatement)
valueType <- valueType(valueStatement)
valueResource = valueStatement.getObject.asResource()
valueIri <- valueIri(valueResource)
valueUuid <- ZIO.fromEither(valueHasUuid(valueResource))
valueCreationDate <- ZIO.fromEither(valueCreationDate(valueResource))
valuePermissions <- ZIO.fromEither(valuePermissions(valueResource))
valueFileValueFilename <- ZIO.fromEither(valueFileValueFilename(valueResource))
valueContent <-
getValueContent(valueType.toString, valueResource, valueFileValueFilename, resourceIri.shortcode, ingestState)
} yield CreateValueV2(
resourceIri.toString,
resourceClassIri.smartIri,
valuePropertyIri.smartIri,
valueContent,
valueIri.map(_.smartIri),
valueUuid,
valueCreationDate,
valuePermissions,
ingestState,
)
}

private def resourceAndIri(model: Model): IO[String, (Resource, ResourceIri)] =
ZIO.fromEither(model.singleRootResource).flatMap { (r: Resource) =>
converter
.asSmartIri(r.uri.getOrElse(""))
.mapError(_.getMessage)
.flatMap(iri => ZIO.fromEither(KResourceIri.from(iri)))
.map((r, _))
}

private def valueStatement(rootResource: Resource): IO[String, Statement] = ZIO
.succeed(rootResource.listProperties().asScala.filter(_.getPredicate != RDF.`type`).toList)
.filterOrFail(_.nonEmpty)("No value property found in root resource")
.filterOrFail(_.size == 1)("Multiple value properties found in root resource")
.map(_.head)

private def valuePropertyIri(valueStatement: Statement) =
converter
.asSmartIri(valueStatement.predicateUri)
.mapError(_.getMessage)
.flatMap(iri => ZIO.fromEither(PropertyIri.from(iri)))

private def valueType(stmt: Statement) = ZIO
.fromEither(stmt.objectAsResource().flatMap(_.rdfsType.toRight("No rdf:type found for value.")))
.orElseFail(s"No value type found for value.")
.flatMap(converter.asSmartIri(_).mapError(_.getMessage))

private def valueIri(valueResource: Resource): IO[String, Option[ValueIri]] = ZIO
.fromOption(valueResource.uri)
.flatMap(converter.asSmartIri(_).mapError(_.getMessage).asSomeError)
.flatMap(iri => ZIO.fromEither(ValueIri.from(iri)).asSomeError)
.unsome

private def valueHasUuid(valueResource: Resource): Either[String, Option[UUID]] =
valueResource.objectStringOption(ValueHasUUID).flatMap {
case Some(str) =>
UuidUtil.base64Decode(str).map(Some(_)).toEither.left.map(e => s"Invalid UUID '$str': ${e.getMessage}")
case None => Right(None)
}

private def valueCreationDate(valueResource: Resource): Either[String, Option[Instant]] =
valueResource.objectDataTypeOption(ValueCreationDate, Xsd.DateTimeStamp).flatMap {
case Some(str) => ValuesValidator.parseXsdDateTimeStamp(str).map(Some(_))
case None => Right(None)
}

private def valuePermissions(valueResource: Resource): Either[String, Option[String]] =
valueResource.objectStringOption(HasPermissions)

private def valueFileValueFilename(valueResource: Resource): Either[String, Option[String]] =
valueResource.objectStringOption(FileValueHasFilename)

private def resourceClassIri(rootResource: Resource): IO[String, KResourceClassIri] = ZIO
.fromOption(rootResource.rdfsType)
.orElseFail("No root resource class IRI found")
.flatMap(converter.asSmartIri(_).mapError(_.getMessage))
.flatMap(iri => ZIO.fromEither(KResourceClassIri.from(iri)))

private def getValueContent(
valueType: String,
valueResource: Resource,
maybeFileName: Option[String],
shortcode: Shortcode,
ingestState: AssetIngestState,
): IO[String, ValueContentV2] =
def withFileInfo[T](fileInfo: Option[FileInfo], f: FileInfo => Either[String, T]): IO[String, T] =
fileInfo match
case None => ZIO.fail("FileInfo is missing")
case Some(info) => ZIO.fromEither(f(info))
for {
i <-
ValueContentV2
.fileInfoFromExternal(maybeFileName, ingestState, shortcode)
.provide(ZLayer.succeed(sipiService))
.mapError(_.getMessage)
content <-
valueType match
case AudioFileValue => withFileInfo(i, AudioFileValueContentV2.from(valueResource, _))
case ArchiveFileValue => withFileInfo(i, ArchiveFileValueContentV2.from(valueResource, _))
case BooleanValue => ZIO.fromEither(BooleanValueContentV2.from(valueResource))
case ColorValue => ZIO.fromEither(ColorValueContentV2.from(valueResource))
case DateValue => ZIO.fromEither(DateValueContentV2.from(valueResource))
case DecimalValue => ZIO.fromEither(DecimalValueContentV2.from(valueResource))
case DocumentFileValue => withFileInfo(i, DocumentFileValueContentV2.from(valueResource, _))
case GeomValue => ZIO.fromEither(GeomValueContentV2.from(valueResource))
case GeonameValue => ZIO.fromEither(GeonameValueContentV2.from(valueResource))
case IntValue => ZIO.fromEither(IntegerValueContentV2.from(valueResource))
case IntervalValue => ZIO.fromEither(IntervalValueContentV2.from(valueResource))
case ListValue => HierarchicalListValueContentV2.from(valueResource, converter)
case LinkValue => LinkValueContentV2.from(valueResource, converter)
case MovingImageFileValue => withFileInfo(i, MovingImageFileValueContentV2.from(valueResource, _))
case StillImageExternalFileValue => ZIO.fromEither(StillImageExternalFileValueContentV2.from(valueResource))
case StillImageFileValue => withFileInfo(i, StillImageFileValueContentV2.from(valueResource, _))
case TextValue => TextValueContentV2.from(valueResource).provide(ZLayer.succeed(messageRelay))
case TextFileValue => withFileInfo(i, TextFileValueContentV2.from(valueResource, _))
case TimeValue => ZIO.fromEither(TimeValueContentV2.from(valueResource))
case UriValue => ZIO.fromEither(UriValueContentV2.from(valueResource))
case _ => ZIO.fail(s"Unsupported value type: $valueType")
} yield content
}

object ApiComplexV2JsonLdRequestParser {
val layer = ZLayer.derive[ApiComplexV2JsonLdRequestParser]
}
Loading

0 comments on commit 34be717

Please sign in to comment.