Skip to content

Commit

Permalink
copy values from project when creating values or resources
Browse files Browse the repository at this point in the history
  • Loading branch information
seakayone committed Nov 22, 2024
1 parent 953279b commit f97e3e5
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import scala.jdk.CollectionConverters.IteratorHasAsScala
import scala.language.implicitConversions

import org.knora.webapi.E2EZSpec
import org.knora.webapi.it.v2.CopyrightAndLicensesSpec.addCopyrightAttributionAndLicenseToProject
import org.knora.webapi.messages.OntologyConstants
import org.knora.webapi.messages.OntologyConstants.KnoraApiV2Complex.HasCopyrightAttribution
import org.knora.webapi.messages.OntologyConstants.KnoraApiV2Complex.HasLicense
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@

package org.knora.webapi.messages.v2.responder.resourcemessages

import monocle.Optional

import java.time.Instant
import java.util.UUID

Expand All @@ -26,15 +24,11 @@ import org.knora.webapi.messages.util.rdf.*
import org.knora.webapi.messages.util.standoff.StandoffTagUtilV2
import org.knora.webapi.messages.util.standoff.XMLUtil
import org.knora.webapi.messages.v2.responder.*
import org.knora.webapi.messages.v2.responder.resourcemessages.ResourceMessagesV2Optics.ReadResourceV2Optics
import org.knora.webapi.messages.v2.responder.standoffmessages.MappingXMLtoStandoff
import org.knora.webapi.messages.v2.responder.valuemessages.*
import org.knora.webapi.messages.v2.responder.valuemessages.ValueMessagesV2Optics.*
import org.knora.webapi.routing.v2.AssetIngestState
import org.knora.webapi.routing.v2.AssetIngestState.*
import org.knora.webapi.slice.admin.api.model.Project
import org.knora.webapi.slice.admin.domain.model.KnoraProject.CopyrightAttribution
import org.knora.webapi.slice.admin.domain.model.KnoraProject.License
import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri
import org.knora.webapi.slice.admin.domain.model.Permission
import org.knora.webapi.slice.admin.domain.model.User
Expand Down Expand Up @@ -573,65 +567,6 @@ case class ReadResourceV2(
values = valuesWithDeletedValues,
)
}

}

object ReadResourceV2 {

def setCopyrightAndLicenceIfMissing(
copyright: Option[CopyrightAttribution],
license: Option[License],
): ReadResourceV2 => ReadResourceV2 =
setCopyrightAndLicenceIfMissingResourceValues(copyright, license)
.andThen(setCopyrightAndLicenceIfMissingOnLinkedResources(copyright, license))

private def setCopyrightAndLicenceIfMissingResourceValues(
copyright: Option[CopyrightAttribution],
license: Option[License],
): ReadResourceV2 => ReadResourceV2 = {
def readValueWith(predicate: FileValueV2 => Boolean): ReadValueV2 => Boolean = rv =>
ReadValueV2Optics.fileValueV2.getOption(rv).exists(predicate)

def readValuesWith(predicate: FileValueV2 => Boolean): Optional[Seq[ReadValueV2], FileValueV2] =
ReadValueV2Optics
.elements(readValueWith(predicate))
.andThen(ReadValueV2Optics.fileValueV2)

def readValuesWithPred(predicate: FileValueV2 => Boolean): Seq[ReadValueV2] => Boolean =
readValuesWith(predicate).getOption(_).isDefined

def readResourcesWith(predicate: FileValueV2 => Boolean): Optional[ReadResourceV2, Seq[ReadValueV2]] =
ReadResourceV2Optics.values(readValuesWithPred(predicate))

def fileValueWith(predicate: FileValueV2 => Boolean): Optional[ReadResourceV2, FileValueV2] =
readResourcesWith(predicate).andThen(readValuesWith(predicate))

def setIfMissing[T](opt: Optional[FileValueV2, Option[T]], newValue: Option[T]): ReadResourceV2 => ReadResourceV2 =
r => fileValueWith(v => opt.getOption(v).flatten.isEmpty).andThen(opt).modifyOption(_ => newValue)(r).getOrElse(r)

setIfMissing(FileValueV2Optics.licenseOption, license).andThen(
setIfMissing(FileValueV2Optics.copyrightAttributionOption, copyright),
)
}

private def setCopyrightAndLicenceIfMissingOnLinkedResources(
copyright: Option[CopyrightAttribution],
license: Option[License],
): ReadResourceV2 => ReadResourceV2 =
rr => rr.copy(values = rr.values.map(linkValueIfMissingMapper(copyright, license)(_, _)))

private def linkValueIfMissingMapper(copyright: Option[CopyrightAttribution], license: Option[License]) =
(iri: SmartIri, seq: Seq[ReadValueV2]) =>
(
iri,
seq.map {
case lv: ReadLinkValueV2 =>
ReadValueV2Optics.nestedResourceOfLinkValueContent
.modifyOption(setCopyrightAndLicenceIfMissingResourceValues(copyright, license))(lv)
.getOrElse(lv)
case other => other
},
)
}

/**
Expand Down Expand Up @@ -847,16 +782,6 @@ case class ReadResourcesSequenceV2(
with KnoraReadV2[ReadResourcesSequenceV2]
with UpdateResultInProject { self =>

private def updateCopyRightAndLicenseDeep(): ReadResourcesSequenceV2 = {
val newResources = self.resources.map { resource =>
ReadResourceV2.setCopyrightAndLicenceIfMissing(
self.projectADM.copyrightAttribution,
self.projectADM.license,
)(resource)
}
self.copy(resources = newResources)
}

override def toOntologySchema(targetSchema: ApiV2Schema): ReadResourcesSequenceV2 =
copy(
resources = resources.map(_.toOntologySchema(targetSchema)),
Expand Down Expand Up @@ -938,8 +863,7 @@ case class ReadResourcesSequenceV2(
appConfig: AppConfig,
schemaOptions: Set[Rendering] = Set.empty,
): JsonLDDocument =
updateCopyRightAndLicenseDeep()
.toOntologySchema(targetSchema)
toOntologySchema(targetSchema)
.generateJsonLD(
targetSchema = targetSchema,
appConfig = appConfig,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,56 @@ package org.knora.webapi.messages.v2.responder.resourcemessages

import monocle.*
import monocle.macros.*

import org.knora.webapi.messages.SmartIri
import org.knora.webapi.messages.v2.responder.valuemessages.ReadValueV2
import org.knora.webapi.messages.v2.responder.valuemessages.FileValueContentV2
import org.knora.webapi.messages.v2.responder.valuemessages.FileValueV2
import org.knora.webapi.messages.v2.responder.valuemessages.ValueContentV2
import org.knora.webapi.messages.v2.responder.valuemessages.ValueMessagesV2Optics.FileValueContentV2Optics

object ResourceMessagesV2Optics {

type ReadResourceV2Values = Map[SmartIri, Seq[ReadValueV2]]

object ReadResourceV2Optics {
object CreateResourceV2Optics {
type CreateResourceV2Values = Map[SmartIri, Seq[CreateValueInNewResourceV2]]

val values: Lens[ReadResourceV2, ReadResourceV2Values] = GenLens[ReadResourceV2](_.values)
val values: Lens[CreateResourceV2, CreateResourceV2Values] = GenLens[CreateResourceV2](_.values)

private def inValues(predicate: Seq[ReadValueV2] => Boolean): Optional[ReadResourceV2Values, Seq[ReadValueV2]] =
Optional[ReadResourceV2Values, Seq[ReadValueV2]](_.values.find(predicate))(newValue =>
private def inValues(predicate: Seq[CreateValueInNewResourceV2] => Boolean) =
Optional[CreateResourceV2Values, Seq[CreateValueInNewResourceV2]](_.values.find(predicate))(newValue =>
values =>
values.map {
case (k, v) if predicate(v) => (k, newValue)
case other => other
},
)

def values(predicate: Seq[ReadValueV2] => Boolean): Optional[ReadResourceV2, Seq[ReadValueV2]] =
def values(
predicate: Seq[CreateValueInNewResourceV2] => Boolean,
): Optional[CreateResourceV2, Seq[CreateValueInNewResourceV2]] =
values.andThen(inValues(predicate))
}

object CreateValueInNewResourceV2Optics {

val valueContent: Lens[CreateValueInNewResourceV2, ValueContentV2] =
GenLens[CreateValueInNewResourceV2](_.valueContent)

val fileValueContentV2: Optional[CreateValueInNewResourceV2, FileValueContentV2] =
Optional[CreateValueInNewResourceV2, FileValueContentV2](_.valueContent.asOpt[FileValueContentV2])(fc =>
_.copy(valueContent = fc),
)

val fileValue: Optional[CreateValueInNewResourceV2, FileValueV2] =
CreateValueInNewResourceV2Optics.fileValueContentV2.andThen(FileValueContentV2Optics.fileValueV2)

def elements(
predicate: CreateValueInNewResourceV2 => Boolean,
): Optional[Seq[CreateValueInNewResourceV2], CreateValueInNewResourceV2] =
Optional[Seq[CreateValueInNewResourceV2], CreateValueInNewResourceV2](_.find(predicate))(newValue =>
values =>
values.map {
case v if predicate(v) => newValue
case other => other
},
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ object ValueMessagesV2Optics {

object FileValueContentV2Optics {


val fileValueV2: Lens[FileValueContentV2, FileValueV2] =
Lens[FileValueContentV2, FileValueV2](_.fileValue)(fv => {
case vc: MovingImageFileValueContentV2 => vc.copy(fileValue = fv)
Expand All @@ -37,7 +38,10 @@ object ValueMessagesV2Optics {
case vc: ArchiveFileValueContentV2 => vc.copy(fileValue = fv)
case vc: TextFileValueContentV2 => vc.copy(fileValue = fv)
})

val copyRightAttributionOption: Lens[FileValueContentV2, Option[CopyrightAttribution]] =
fileValueV2.andThen(FileValueV2Optics.copyrightAttributionOption)
val licenseOption: Lens[FileValueContentV2, Option[License]] =
fileValueV2.andThen(FileValueV2Optics.licenseOption)
}

object LinkValueContentV2Optics {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package org.knora.webapi.responders.v2

import monocle.PLens
import zio.*

import java.time.Instant
Expand Down Expand Up @@ -32,24 +33,32 @@ import org.knora.webapi.messages.v2.responder.SuccessResponseV2
import org.knora.webapi.messages.v2.responder.ontologymessages.*
import org.knora.webapi.messages.v2.responder.resourcemessages.*
import org.knora.webapi.messages.v2.responder.valuemessages.*
import org.knora.webapi.messages.v2.responder.valuemessages.FileValueContentV2
import org.knora.webapi.messages.v2.responder.valuemessages.ValueMessagesV2Optics.FileValueContentV2Optics
import org.knora.webapi.responders.IriLocker
import org.knora.webapi.responders.IriService
import org.knora.webapi.responders.admin.PermissionsResponder
import org.knora.webapi.slice.admin.domain.model.KnoraProject
import org.knora.webapi.slice.admin.domain.model.Permission
import org.knora.webapi.slice.admin.domain.model.User
import org.knora.webapi.slice.admin.domain.service.KnoraGroupRepo
import org.knora.webapi.slice.admin.domain.service.KnoraProjectService
import org.knora.webapi.slice.admin.domain.service.KnoraUserRepo
import org.knora.webapi.slice.admin.domain.service.ProjectService
import org.knora.webapi.slice.common.KnoraIris.ResourceIri
import org.knora.webapi.slice.ontology.domain.model.Cardinality.AtLeastOne
import org.knora.webapi.slice.ontology.domain.model.Cardinality.ExactlyOne
import org.knora.webapi.slice.ontology.domain.model.Cardinality.ZeroOrOne
import org.knora.webapi.slice.resourceinfo.domain.IriConverter
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

final case class ValuesResponderV2(
appConfig: AppConfig,
iriService: IriService,
iriConverter: IriConverter,
projectService: KnoraProjectService,
messageRelay: MessageRelay,
permissionUtilADM: PermissionUtilADM,
resourceUtilV2: ResourceUtilV2,
Expand All @@ -58,6 +67,16 @@ final case class ValuesResponderV2(
permissionsResponder: PermissionsResponder,
)(implicit val stringFormatter: StringFormatter) {

private def setCopyrightAndLicenceIfMissing(project: KnoraProject): FileValueContentV2 => FileValueContentV2 =
FileValueContentV2Optics.licenseOption
.filter(_.isEmpty)
.replace(project.license)
.andThen(
FileValueContentV2Optics.copyRightAttributionOption
.filter(_.isEmpty)
.replace(project.copyrightAttribution),
)

/**
* Creates a new value in an existing resource.
*
Expand All @@ -73,13 +92,22 @@ final case class ValuesResponderV2(
): Task[CreateValueResponseV2] = {
def taskZio: Task[CreateValueResponseV2] = {
for {
resourceIri <-
iriConverter
.asSmartIri(valueToCreate.resourceIri)
.flatMap(iri => ZIO.fromEither(ResourceIri.from(iri)).mapError(BadRequestException.apply))
project <- projectService
.findByShortcode(resourceIri.shortcode)
.someOrFail(NotFoundException(s"Project not found for resource IRI: $resourceIri"))

// Convert the submitted value to the internal schema.
submittedInternalPropertyIri <-
ZIO.attempt(valueToCreate.propertyIri.toOntologySchema(InternalSchema))

submittedInternalValueContent: ValueContentV2 =
valueToCreate.valueContent
.toOntologySchema(InternalSchema)
valueToCreate.valueContent.toOntologySchema(InternalSchema) match
case fileValueContent: FileValueContentV2 => setCopyrightAndLicenceIfMissing(project)(fileValueContent)
case other => other

// Get ontology information about the submitted property.
propertyInfoRequestForSubmittedProperty =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
package org.knora.webapi.responders.v2.resources

import com.typesafe.scalalogging.LazyLogging
import monocle.Optional
import zio.*

import java.time.Instant

import scala.language.postfixOps
import dsp.errors.*
import dsp.valueobjects.UuidUtil
import org.knora.webapi.*
Expand All @@ -28,8 +29,11 @@ import org.knora.webapi.messages.v2.responder.ontologymessages.EntityInfoGetResp
import org.knora.webapi.messages.v2.responder.ontologymessages.OwlCardinality.*
import org.knora.webapi.messages.v2.responder.ontologymessages.ReadClassInfoV2
import org.knora.webapi.messages.v2.responder.resourcemessages.*
import org.knora.webapi.messages.v2.responder.resourcemessages.ResourceMessagesV2Optics.CreateResourceV2Optics
import org.knora.webapi.messages.v2.responder.resourcemessages.ResourceMessagesV2Optics.CreateValueInNewResourceV2Optics
import org.knora.webapi.messages.v2.responder.standoffmessages.*
import org.knora.webapi.messages.v2.responder.valuemessages.*
import org.knora.webapi.messages.v2.responder.valuemessages.ValueMessagesV2Optics.FileValueV2Optics
import org.knora.webapi.responders.IriLocker
import org.knora.webapi.responders.IriService
import org.knora.webapi.responders.admin.PermissionsResponder
Expand Down Expand Up @@ -90,6 +94,27 @@ final case class CreateResourceV2Handler(
createResourceRequestV2.requestingUser,
)

private def setIfMissing(project: Project): CreateResourceV2 => CreateResourceV2 = {
def createValuesWith(
pred: FileValueV2 => Boolean,
): Optional[Seq[CreateValueInNewResourceV2], CreateValueInNewResourceV2] =
CreateValueInNewResourceV2Optics.elements(cv =>
CreateValueInNewResourceV2Optics.fileValue.getOption(cv).exists(pred),
)

def fileValueWith(pred: FileValueV2 => Boolean): Optional[CreateResourceV2, FileValueV2] =
CreateResourceV2Optics
.values(createValuesWith(pred).getOption(_).isDefined)
.andThen(createValuesWith(pred))
.andThen(CreateValueInNewResourceV2Optics.fileValue)

def replaceIfEmpty[T](newValue: Option[T], opt: Optional[FileValueV2, Option[T]]) =
fileValueWith(opt.getOption(_).flatten.isEmpty).andThen(opt).replace(newValue)

replaceIfEmpty(project.license, FileValueV2Optics.licenseOption)
.andThen(replaceIfEmpty(project.copyrightAttribution, FileValueV2Optics.copyrightAttributionOption))
}

private def triplestoreUpdate(
createResourceRequestV2: CreateResourceRequestV2,
): Task[ReadResourcesSequenceV2] =
Expand Down Expand Up @@ -171,9 +196,11 @@ final case class CreateResourceV2Handler(
ZIO
.fail(DuplicateValueException(s"Resource IRI: '$resourceIri' already exists."))
.whenZIO(iriService.checkIriExists(resourceIri))
project = createResourceRequestV2.createResource.projectADM

// Convert the resource to the internal ontology schema.
internalCreateResource <- ZIO.attempt(createResourceRequestV2.createResource.toOntologySchema(InternalSchema))
internalCreateResource <-
ZIO.attempt(setIfMissing(project)(createResourceRequestV2.createResource).toOntologySchema(InternalSchema))

// Check link targets and list nodes that should exist.
_ <- checkStandoffLinkTargets(
Expand Down

0 comments on commit f97e3e5

Please sign in to comment.