From 1bfa7085f8e5a83e0626a5ba92b9a3365149c104 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 20 Nov 2024 09:44:45 +0100 Subject: [PATCH 01/31] add copyright fallback to resources --- .../responders/v2/ResourcesResponderV2.scala | 70 ++++++++++++++++++- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index e3ae11382c..f4f69985a8 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -11,7 +11,6 @@ import zio.ZIO import java.time.Instant import java.util.UUID - import dsp.errors.* import dsp.valueobjects.Iri import dsp.valueobjects.UuidUtil @@ -42,6 +41,8 @@ import org.knora.webapi.responders.IriService import org.knora.webapi.responders.Responder import org.knora.webapi.responders.admin.PermissionsResponder import org.knora.webapi.responders.v2.resources.CreateResourceV2Handler +import org.knora.webapi.slice.admin.api.model.Project +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.Permission import org.knora.webapi.slice.admin.domain.model.User @@ -652,10 +653,73 @@ final case class ResourcesResponderV2( } } responseWithDeletedResourcesReplaced = apiResponse.copy(resources = deletedResourcesReplaced) - } yield responseWithDeletedResourcesReplaced - + response <- addCopyrightAttributionAndLicense(responseWithDeletedResourcesReplaced) + } yield response } + private def addCopyrightAttributionAndLicense(seq: ReadResourcesSequenceV2): Task[ReadResourcesSequenceV2] = + ZIO + .foreach(seq.resources)(addCopyrightAttributionAndLicense(seq.projectADM.projectIri)) + .map(newResources => seq.copy(resources = newResources)) + + private def addCopyrightAttributionAndLicense( + projectIri: ProjectIri, + )(resource: ReadResourceV2): Task[ReadResourceV2] = + for { + project <- + knoraProjectService.findById(projectIri).someOrFail(NotFoundException(s"Project $projectIri not found")) + newValues <- addCopyrightAttributionAndLicense(resource.values, project) + } yield resource.copy(values = newValues) + + private def addCopyrightAttributionAndLicense( + valuesMap: Map[SmartIri, Seq[ReadValueV2]], + project: KnoraProject, + ): UIO[Map[SmartIri, Seq[ReadValueV2]]] = + ZIO.foreach(valuesMap)((smartIri, values) => + ZIO + .foreach(values)(value => addCopyrightAttributionAndLicense(project, value)) + .map(newValue => (smartIri, newValue)), + ) + + private def addCopyrightAttributionAndLicense(project: KnoraProject, value: ReadValueV2): UIO[ReadValueV2] = + value match + case rov: ReadOtherValueV2 => + ZIO.succeed(rov.copy(valueContent = addCopyrightAttributionAndLicense(project, rov.valueContent))) + case lv: ReadLinkValueV2 => + val oldNested = lv.valueContent.nestedResource + oldNested match + case Some(nested) => + addCopyrightAttributionAndLicense(nested.values, project).map(newNestedValues => + lv.copy(valueContent = + lv.valueContent.copy(nestedResource = oldNested.map(_.copy(values = newNestedValues))), + ), + ) + case None => ZIO.succeed(lv) + case other => ZIO.succeed(other) + + private def addCopyrightAttributionAndLicense(project: KnoraProject, content: ValueContentV2): ValueContentV2 = + content match + case tfvc: FileValueContentV2 => + val newFv = addCopyrightAttributionAndLicenseFileValue(project, tfvc.fileValue) + tfvc match + case fvc: MovingImageFileValueContentV2 => fvc.copy(fileValue = newFv) + case fvc: StillImageFileValueContentV2 => fvc.copy(fileValue = newFv) + case fvc: AudioFileValueContentV2 => fvc.copy(fileValue = newFv) + case fvc: DocumentFileValueContentV2 => fvc.copy(fileValue = newFv) + case fvc: StillImageExternalFileValueContentV2 => fvc.copy(fileValue = newFv) + case fvc: ArchiveFileValueContentV2 => fvc.copy(fileValue = newFv) + case fvc: TextFileValueContentV2 => fvc.copy(fileValue = newFv) + case other => other + + private def addCopyrightAttributionAndLicenseFileValue(project: KnoraProject, fileValue: FileValueV2): FileValueV2 = + fileValue match + case fv if fv.copyrightAttribution.isEmpty || fv.license.isEmpty => + fv.copy( + copyrightAttribution = fv.copyrightAttribution.orElse(project.copyrightAttribution), + license = fv.license.orElse(project.license), + ) + case other => other + /** * Get the preview of a resource. * From 7068231bf1ba90c57a0f42f5a18c9eac10ee3e1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 20 Nov 2024 10:02:56 +0100 Subject: [PATCH 02/31] fmt --- .../org/knora/webapi/responders/v2/ResourcesResponderV2.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index f4f69985a8..831c0ed2fc 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -11,6 +11,7 @@ import zio.ZIO import java.time.Instant import java.util.UUID + import dsp.errors.* import dsp.valueobjects.Iri import dsp.valueobjects.UuidUtil From 0ac89b9cb8128fd71547609bb9b5cd54321a3166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 20 Nov 2024 12:03:13 +0100 Subject: [PATCH 03/31] add monocle --- project/Dependencies.scala | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 7699bb8d5c..abf84db62c 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -19,6 +19,8 @@ object Dependencies { val PekkoActorVersion = "1.1.2" val PekkoHttpVersion = "1.1.0" + val MonocleVersion = "3.3.0" + // rdf and graph libraries // topbraid/shacl is not yet compatible with jena 5 so we need to use jena 4 for now // see: https://github.com/TopQuadrant/shacl/pull/177 @@ -53,6 +55,13 @@ object Dependencies { "dev.zio" %% "zio-json-interop-refined" % "0.7.3", ) + // monocle + val monocle = Seq( + "dev.optics" %% "monocle-core" % MonocleVersion, + "dev.optics" %% "monocle-macro" % MonocleVersion, + "dev.optics" %% "monocle-refined" % MonocleVersion, + ) + // zio-test and friends val zioTest = "dev.zio" %% "zio-test" % ZioVersion val zioTestSbt = "dev.zio" %% "zio-test-sbt" % ZioVersion @@ -149,7 +158,7 @@ object Dependencies { val webapiTestDependencies = Seq(zioTest, zioTestSbt, wiremock).map(_ % Test) - val webapiDependencies = refined ++ Seq( + val webapiDependencies = monocle ++ refined ++ Seq( pekkoActor, pekkoHttp, pekkoHttpCors, From 5e82b63a3b29e6cc85619b3a3ed76f44de5b7484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 20 Nov 2024 14:54:48 +0100 Subject: [PATCH 04/31] wip --- .../v2/ReadResourceV2LensLearningSpec.scala | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 webapi/src/test/scala/org/knora/webapi/responders/v2/ReadResourceV2LensLearningSpec.scala diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/ReadResourceV2LensLearningSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/ReadResourceV2LensLearningSpec.scala new file mode 100644 index 0000000000..c76859388e --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/ReadResourceV2LensLearningSpec.scala @@ -0,0 +1,102 @@ +/* + * 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.responders.v2 +import org.knora.webapi.messages.SmartIri +import monocle.macros.* +import monocle.* +import monocle.Optional +import org.knora.webapi.ApiV2Complex +import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.messages.v2.responder.resourcemessages.ReadResourceV2 +import org.knora.webapi.messages.v2.responder.valuemessages.* +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.Permission +import zio.test.* + +import java.time.Instant +import java.util.UUID + +object ReadResourceV2LensLearningSpec extends ZIOSpecDefault { + + type ReadValues = Map[SmartIri, Seq[ReadValueV2]] + val valuesLens: Lens[ReadResourceV2, ReadValues] = + GenLens[ReadResourceV2](_.values) + + val fileValueContentOptional: Optional[ReadValueV2, FileValueContentV2] = + Optional[ReadValueV2, FileValueContentV2](_.valueContent.asOpt[FileValueContentV2])(fc => { + case rv: ReadLinkValueV2 => rv + case rv: ReadTextValueV2 => rv + case ov: ReadOtherValueV2 => ov.copy(valueContent = fc) + }) + + val fileValueContentPrism: Prism[ValueContentV2, FileValueContentV2] = + GenPrism[ValueContentV2, FileValueContentV2] + + val fileValueLens: Lens[FileValueContentV2, FileValueV2] = + Lens[FileValueContentV2, FileValueV2](_.fileValue)(fv => { + case vc: MovingImageFileValueContentV2 => vc.copy(fileValue = fv) + case vc: StillImageFileValueContentV2 => vc.copy(fileValue = fv) + case vc: AudioFileValueContentV2 => vc.copy(fileValue = fv) + case vc: DocumentFileValueContentV2 => vc.copy(fileValue = fv) + case vc: StillImageExternalFileValueContentV2 => vc.copy(fileValue = fv) + case vc: ArchiveFileValueContentV2 => vc.copy(fileValue = fv) + case vc: TextFileValueContentV2 => vc.copy(fileValue = fv) + }) + + val copyrightAttributionLens: Lens[FileValueV2, Option[CopyrightAttribution]] = + GenLens[FileValueV2](_.copyrightAttribution) + val licenseLens: Lens[FileValueV2, Option[License]] = GenLens[FileValueV2](_.license) + + val sf = StringFormatter.getInitializedTestInstance + + val aLicense = License.unsafeFrom("CC-BY-4.0") + val anotherLicense = License.unsafeFrom("Apache-2.0") + + val aCopyrightAttribution = CopyrightAttribution.unsafeFrom("2020, John Doe") + val anotherCopyrightAttribution = CopyrightAttribution.unsafeFrom("2024, Jane Doe") + + val fileValueWithoutLicenseOrCopyright = + FileValueV2("internalFilename", "internalMimeType", None, None, None, None) + val fileValueWithALicense = + FileValueV2("internalFilename", "internalMimeType", None, None, None, Some(aLicense)) + val fileValueWithACopyrightAttribution = + FileValueV2("internalFilename", "internalMimeType", None, None, Some(aCopyrightAttribution), None) + + val setLicenseIfMissing: Option[License] => ValueContentV2 => ValueContentV2 = newLicense => + value => { + val composed = fileValueContentPrism.andThen(fileValueLens).andThen(licenseLens) + composed.getOption(value).flatten match { + case Some(_) => value + case None => composed.replace(newLicense)(value) + } + } + + private val fileValueLensesSuite = suite("FileValueV2 lenses")( + test("licenseLens should replace an empty license with a new one") { + val actual = licenseLens.replace(Some(aLicense))(fileValueWithoutLicenseOrCopyright) + assertTrue(actual.license == Some(aLicense)) + }, + test("licenseLens should replace an existing license with a new one") { + val actual = licenseLens.replace(Some(anotherLicense))(fileValueWithALicense) + assertTrue(actual.license == Some(anotherLicense)) + }, + test("copyrightAttributionLens should replace an empty copyrightAttribution with a new one") { + val aCopyrightAttribution = CopyrightAttribution.unsafeFrom("2020, John Doe") + val actual = copyrightAttributionLens.replace(Some(aCopyrightAttribution))(fileValueWithoutLicenseOrCopyright) + assertTrue(actual.copyrightAttribution == Some(aCopyrightAttribution)) + }, + test("copyrightAttributionLens should replace an existing copyrightAttribution with a new one") { + val actual = + copyrightAttributionLens.replace(Some(anotherCopyrightAttribution))(fileValueWithACopyrightAttribution) + assertTrue(actual.copyrightAttribution == Some(anotherCopyrightAttribution)) + }, + ) + + val spec = suite("ReadResourceV2LensLearning")( + fileValueLensesSuite, + ) +} From 498de35065dc51ef31809405e41b3538736a7d09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 20 Nov 2024 15:01:25 +0100 Subject: [PATCH 05/31] wip --- .../v2/ReadResourceV2LensLearningSpec.scala | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/ReadResourceV2LensLearningSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/ReadResourceV2LensLearningSpec.scala index c76859388e..1e4631665a 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/ReadResourceV2LensLearningSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/ReadResourceV2LensLearningSpec.scala @@ -7,6 +7,7 @@ package org.knora.webapi.responders.v2 import org.knora.webapi.messages.SmartIri import monocle.macros.* import monocle.* +import monocle.Lens import monocle.Optional import org.knora.webapi.ApiV2Complex import org.knora.webapi.messages.StringFormatter @@ -26,10 +27,18 @@ object ReadResourceV2LensLearningSpec extends ZIOSpecDefault { val valuesLens: Lens[ReadResourceV2, ReadValues] = GenLens[ReadResourceV2](_.values) - val fileValueContentOptional: Optional[ReadValueV2, FileValueContentV2] = - Optional[ReadValueV2, FileValueContentV2](_.valueContent.asOpt[FileValueContentV2])(fc => { - case rv: ReadLinkValueV2 => rv - case rv: ReadTextValueV2 => rv + val fileValueContentLens: Lens[ReadValueV2, ValueContentV2] = + Lens[ReadValueV2, ValueContentV2](_.valueContent)(fc => { + case rv: ReadLinkValueV2 => + fc match { + case lv: LinkValueContentV2 => rv.copy(valueContent = lv) + case _ => rv + } + case rv: ReadTextValueV2 => + fc match { + case tv: TextValueContentV2 => rv.copy(valueContent = tv) + case _ => rv + } case ov: ReadOtherValueV2 => ov.copy(valueContent = fc) }) @@ -66,10 +75,11 @@ object ReadResourceV2LensLearningSpec extends ZIOSpecDefault { val fileValueWithACopyrightAttribution = FileValueV2("internalFilename", "internalMimeType", None, None, Some(aCopyrightAttribution), None) - val setLicenseIfMissing: Option[License] => ValueContentV2 => ValueContentV2 = newLicense => + val setLicenseIfMissing: Option[License] => ReadValueV2 => ReadValueV2 = newLicense => value => { - val composed = fileValueContentPrism.andThen(fileValueLens).andThen(licenseLens) - composed.getOption(value).flatten match { + val composed = fileValueContentLens.andThen(fileValueContentPrism).andThen(fileValueLens).andThen(licenseLens) + val existing: Option[License] =composed.getOption(value).flatten + existing match { case Some(_) => value case None => composed.replace(newLicense)(value) } From f43ba0f1ab56e4ea45af93e26e6e5fb4e9c7b733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 20 Nov 2024 18:24:44 +0100 Subject: [PATCH 06/31] add test --- .../it/v2/CopyrightAndLicensesSpec.scala | 51 +++++++--- .../resourcemessages/ResourceMessagesV2.scala | 93 ++++++++++++++++++- .../responders/v2/ResourcesResponderV2.scala | 66 +------------ .../v2/ReadResourceV2LensLearningSpec.scala | 57 +++++++----- 4 files changed, 168 insertions(+), 99 deletions(-) diff --git a/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala b/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala index 74d5797c82..c0e9ee3170 100644 --- a/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala @@ -23,8 +23,12 @@ import org.knora.webapi.messages.OntologyConstants.KnoraApiV2Complex.HasLicense import org.knora.webapi.messages.OntologyConstants.KnoraApiV2Complex.StillImageFileValue import org.knora.webapi.models.filemodels.FileType import org.knora.webapi.models.filemodels.UploadFileRequest +import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequestsAndResponses.ProjectUpdateRequest 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.KnoraProject.Shortcode +import org.knora.webapi.slice.admin.domain.service.KnoraProjectService import org.knora.webapi.slice.common.KnoraIris.ValueIri import org.knora.webapi.slice.common.jena.JenaConversions.given import org.knora.webapi.slice.common.jena.ModelOps @@ -43,7 +47,7 @@ object CopyrightAndLicensesSpec extends E2EZSpec { "the creation response should contain the license and copyright attribution", ) { for { - createResourceResponseModel <- createImageWithCopyrightAndLicense + createResourceResponseModel <- createStillImageResource(Some(copyrightAttribution), Some(license)) actualCreatedCopyright <- copyrightValue(createResourceResponseModel) actualCreatedLicense <- licenseValue(createResourceResponseModel) } yield assertTrue( @@ -56,7 +60,7 @@ object CopyrightAndLicensesSpec extends E2EZSpec { "the response when getting the created resource should contain the license and copyright attribution", ) { for { - createResourceResponseModel <- createImageWithCopyrightAndLicense + createResourceResponseModel <- createStillImageResource(Some(copyrightAttribution), Some(license)) resourceId <- resourceId(createResourceResponseModel) getResponseModel <- getResourceFromApi(resourceId) actualCopyright <- copyrightValue(getResponseModel) @@ -71,10 +75,30 @@ object CopyrightAndLicensesSpec extends E2EZSpec { "the response when getting the created value should contain the license and copyright attribution", ) { for { - createResourceResponseModel <- createImageWithCopyrightAndLicense - resourceId <- resourceId(createResourceResponseModel) - valueId <- valueId(createResourceResponseModel) - valueResponseModel <- getValueFromApi(valueId, resourceId) + createResourceResponseModel <- createStillImageResource(Some(copyrightAttribution), Some(license)) + valueResponseModel <- getValueFromApi(createResourceResponseModel) + actualCopyright <- copyrightValue(valueResponseModel) + actualLicense <- licenseValue(valueResponseModel) + } yield assertTrue( + actualCopyright == copyrightAttribution.value, + actualLicense == license.value, + ) + }, + test( + "when creating a resource without copyright attribution and without license " + + "given the project has a default license and default copyright attribution " + + "then the response when getting the created value should contain the default license and default copyright attribution", + ) { + for { + projectService <- ZIO.service[KnoraProjectService] + prj <- + projectService.findByShortcode(Shortcode.unsafeFrom("0001")).someOrFail(new Exception("Project not found")) + _ <- projectService.updateProject( + prj, + ProjectUpdateRequest(copyrightAttribution = Some(copyrightAttribution), license = Some(license)), + ) + createResourceResponseModel <- createStillImageResource() + valueResponseModel <- getValueFromApi(createResourceResponseModel) actualCopyright <- copyrightValue(valueResponseModel) actualLicense <- licenseValue(valueResponseModel) } yield assertTrue( @@ -84,13 +108,16 @@ object CopyrightAndLicensesSpec extends E2EZSpec { }, ) - private def createImageWithCopyrightAndLicense: ZIO[env, Throwable, Model] = { + private def createStillImageResource( + copyrightAttribution: Option[CopyrightAttribution] = None, + license: Option[License] = None, + ): ZIO[env, Throwable, Model] = { val jsonLd = UploadFileRequest .make( FileType.StillImageFile(), "internalFilename.jpg", - copyrightAttribution = Some(copyrightAttribution), - license = Some(license), + copyrightAttribution = copyrightAttribution, + license = license, ) .toJsonLd( className = Some("ThingPicture"), @@ -113,8 +140,10 @@ object CopyrightAndLicensesSpec extends E2EZSpec { model <- ModelOps.fromJsonLd(responseBody).mapError(Exception(_)) } yield model - private def getValueFromApi(valueIri: ValueIri, resourceIri: String) = for { - responseBody <- sendGetRequest(s"/v2/values/${URLEncoder.encode(resourceIri, "UTF-8")}/${valueIri.valueId}") + private def getValueFromApi(createResourceResponse: Model) = for { + valueId <- valueId(createResourceResponse) + resourceId <- resourceId(createResourceResponse) + responseBody <- sendGetRequest(s"/v2/values/${URLEncoder.encode(resourceId, "UTF-8")}/${valueId.valueId}") .filterOrFail(_.status.isSuccess)(s"Failed to get resource $valueId") .flatMap(_.body.asString) model <- ModelOps.fromJsonLd(responseBody).mapError(Exception(_)) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index 8f44105d97..47d01fa1f5 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -5,6 +5,12 @@ package org.knora.webapi.messages.v2.responder.resourcemessages +import monocle.Lens +import monocle.Optional +import monocle.Prism +import monocle.macros.GenLens +import monocle.macros.GenPrism + import java.time.Instant import java.util.UUID @@ -27,6 +33,8 @@ import org.knora.webapi.messages.v2.responder.* import org.knora.webapi.messages.v2.responder.standoffmessages.MappingXMLtoStandoff import org.knora.webapi.messages.v2.responder.valuemessages.* 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 @@ -568,6 +576,79 @@ case class ReadResourceV2( } +object ReadResourceV2 { + + private val fileValueContentLens: Lens[ReadValueV2, ValueContentV2] = + Lens[ReadValueV2, ValueContentV2](_.valueContent)(fc => { + case rv: ReadLinkValueV2 => + fc match { + case lv: LinkValueContentV2 => rv.copy(valueContent = lv) + case _ => rv + } + case rv: ReadTextValueV2 => + fc match { + case tv: TextValueContentV2 => rv.copy(valueContent = tv) + case _ => rv + } + case ov: ReadOtherValueV2 => ov.copy(valueContent = fc) + }) + + private val fileValueContentPrism: Prism[ValueContentV2, FileValueContentV2] = + GenPrism[ValueContentV2, FileValueContentV2] + + private val fileValueLens: Lens[FileValueContentV2, FileValueV2] = + Lens[FileValueContentV2, FileValueV2](_.fileValue)(fv => { + case vc: MovingImageFileValueContentV2 => vc.copy(fileValue = fv) + case vc: StillImageFileValueContentV2 => vc.copy(fileValue = fv) + case vc: AudioFileValueContentV2 => vc.copy(fileValue = fv) + case vc: DocumentFileValueContentV2 => vc.copy(fileValue = fv) + case vc: StillImageExternalFileValueContentV2 => vc.copy(fileValue = fv) + case vc: ArchiveFileValueContentV2 => vc.copy(fileValue = fv) + case vc: TextFileValueContentV2 => vc.copy(fileValue = fv) + }) + + private val copyrightAttributionLens: Lens[FileValueV2, Option[CopyrightAttribution]] = + GenLens[FileValueV2](_.copyrightAttribution) + + private val licenseLens: Lens[FileValueV2, Option[License]] = + GenLens[FileValueV2](_.license) + + private val commonComposed: Optional[ReadValueV2, FileValueV2] = + fileValueContentLens.andThen(fileValueContentPrism).andThen(fileValueLens) + + private def setIfMissing[T]( + optional: Optional[ReadValueV2, Option[T]], + ): Option[T] => ReadResourceV2 => ReadResourceV2 = + newValue => + readResource => { + val newValues: Map[SmartIri, Seq[ReadValueV2]] = + readResource.values.map((iri: SmartIri, seq: Seq[ReadValueV2]) => + ( + iri, + seq.map { readValue => + optional.getOption(readValue).flatten match { + case Some(_) => readValue + case None => optional.replace(newValue)(readValue) + } + }, + ), + ) + readResource.copy(values = newValues) + } + + private val setCopyrightAttributionIfMissing: Option[CopyrightAttribution] => ReadResourceV2 => ReadResourceV2 = + setIfMissing(commonComposed.andThen(copyrightAttributionLens)) + + private val setLicenseIfMissing: Option[License] => ReadResourceV2 => ReadResourceV2 = + setIfMissing(commonComposed.andThen(licenseLens)) + + def setCopyrightAndLicenceIfMissing( + copyright: Option[CopyrightAttribution], + license: Option[License], + ): ReadResourceV2 => ReadResourceV2 = + setCopyrightAttributionIfMissing(copyright).andThen(setLicenseIfMissing(license)) +} + /** * The value of a Knora property sent to Knora to be created in a new resource. * @@ -778,7 +859,17 @@ case class ReadResourcesSequenceV2( mayHaveMoreResults: Boolean = false, ) extends KnoraJsonLDResponseV2 with KnoraReadV2[ReadResourcesSequenceV2] - with UpdateResultInProject { + with UpdateResultInProject { self => + + 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( diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index 831c0ed2fc..22433e823b 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -654,73 +654,9 @@ final case class ResourcesResponderV2( } } responseWithDeletedResourcesReplaced = apiResponse.copy(resources = deletedResourcesReplaced) - response <- addCopyrightAttributionAndLicense(responseWithDeletedResourcesReplaced) - } yield response + } yield responseWithDeletedResourcesReplaced.updateCopyRightAndLicenseDeep() } - private def addCopyrightAttributionAndLicense(seq: ReadResourcesSequenceV2): Task[ReadResourcesSequenceV2] = - ZIO - .foreach(seq.resources)(addCopyrightAttributionAndLicense(seq.projectADM.projectIri)) - .map(newResources => seq.copy(resources = newResources)) - - private def addCopyrightAttributionAndLicense( - projectIri: ProjectIri, - )(resource: ReadResourceV2): Task[ReadResourceV2] = - for { - project <- - knoraProjectService.findById(projectIri).someOrFail(NotFoundException(s"Project $projectIri not found")) - newValues <- addCopyrightAttributionAndLicense(resource.values, project) - } yield resource.copy(values = newValues) - - private def addCopyrightAttributionAndLicense( - valuesMap: Map[SmartIri, Seq[ReadValueV2]], - project: KnoraProject, - ): UIO[Map[SmartIri, Seq[ReadValueV2]]] = - ZIO.foreach(valuesMap)((smartIri, values) => - ZIO - .foreach(values)(value => addCopyrightAttributionAndLicense(project, value)) - .map(newValue => (smartIri, newValue)), - ) - - private def addCopyrightAttributionAndLicense(project: KnoraProject, value: ReadValueV2): UIO[ReadValueV2] = - value match - case rov: ReadOtherValueV2 => - ZIO.succeed(rov.copy(valueContent = addCopyrightAttributionAndLicense(project, rov.valueContent))) - case lv: ReadLinkValueV2 => - val oldNested = lv.valueContent.nestedResource - oldNested match - case Some(nested) => - addCopyrightAttributionAndLicense(nested.values, project).map(newNestedValues => - lv.copy(valueContent = - lv.valueContent.copy(nestedResource = oldNested.map(_.copy(values = newNestedValues))), - ), - ) - case None => ZIO.succeed(lv) - case other => ZIO.succeed(other) - - private def addCopyrightAttributionAndLicense(project: KnoraProject, content: ValueContentV2): ValueContentV2 = - content match - case tfvc: FileValueContentV2 => - val newFv = addCopyrightAttributionAndLicenseFileValue(project, tfvc.fileValue) - tfvc match - case fvc: MovingImageFileValueContentV2 => fvc.copy(fileValue = newFv) - case fvc: StillImageFileValueContentV2 => fvc.copy(fileValue = newFv) - case fvc: AudioFileValueContentV2 => fvc.copy(fileValue = newFv) - case fvc: DocumentFileValueContentV2 => fvc.copy(fileValue = newFv) - case fvc: StillImageExternalFileValueContentV2 => fvc.copy(fileValue = newFv) - case fvc: ArchiveFileValueContentV2 => fvc.copy(fileValue = newFv) - case fvc: TextFileValueContentV2 => fvc.copy(fileValue = newFv) - case other => other - - private def addCopyrightAttributionAndLicenseFileValue(project: KnoraProject, fileValue: FileValueV2): FileValueV2 = - fileValue match - case fv if fv.copyrightAttribution.isEmpty || fv.license.isEmpty => - fv.copy( - copyrightAttribution = fv.copyrightAttribution.orElse(project.copyrightAttribution), - license = fv.license.orElse(project.license), - ) - case other => other - /** * Get the preview of a resource. * diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/ReadResourceV2LensLearningSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/ReadResourceV2LensLearningSpec.scala index 1e4631665a..ca70a2aa18 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/ReadResourceV2LensLearningSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/ReadResourceV2LensLearningSpec.scala @@ -4,29 +4,21 @@ */ package org.knora.webapi.responders.v2 -import org.knora.webapi.messages.SmartIri -import monocle.macros.* import monocle.* import monocle.Lens import monocle.Optional -import org.knora.webapi.ApiV2Complex +import monocle.macros.* +import zio.test.* + +import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.v2.responder.resourcemessages.ReadResourceV2 import org.knora.webapi.messages.v2.responder.valuemessages.* 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.Permission -import zio.test.* - -import java.time.Instant -import java.util.UUID object ReadResourceV2LensLearningSpec extends ZIOSpecDefault { - type ReadValues = Map[SmartIri, Seq[ReadValueV2]] - val valuesLens: Lens[ReadResourceV2, ReadValues] = - GenLens[ReadResourceV2](_.values) - val fileValueContentLens: Lens[ReadValueV2, ValueContentV2] = Lens[ReadValueV2, ValueContentV2](_.valueContent)(fc => { case rv: ReadLinkValueV2 => @@ -60,6 +52,37 @@ object ReadResourceV2LensLearningSpec extends ZIOSpecDefault { GenLens[FileValueV2](_.copyrightAttribution) val licenseLens: Lens[FileValueV2, Option[License]] = GenLens[FileValueV2](_.license) + type ReadResourceValues = Map[SmartIri, Seq[ReadValueV2]] + + private val commonComposed: Optional[ReadValueV2, FileValueV2] = + fileValueContentLens.andThen(fileValueContentPrism).andThen(fileValueLens) + private val composedLicense = commonComposed.andThen(licenseLens) + private val composedCopyright = commonComposed.andThen(copyrightAttributionLens) + + private def setIfMissing[T]( + optional: Optional[ReadValueV2, Option[T]], + ): Option[T] => ReadResourceV2 => ReadResourceV2 = + newValue => + readResource => { + val newValues: ReadResourceValues = readResource.values.map((iri: SmartIri, seq: Seq[ReadValueV2]) => + ( + iri, + seq.map { readValue => + optional.getOption(readValue).flatten match { + case Some(_) => readValue + case None => optional.replace(newValue)(readValue) + } + }, + ), + ) + readResource.copy(values = newValues) + } + + val setCopyrightAttributionIfMissing: Option[CopyrightAttribution] => ReadResourceV2 => ReadResourceV2 = + setIfMissing(composedCopyright) + + val setLicenseIfMissing: Option[License] => ReadResourceV2 => ReadResourceV2 = setIfMissing(composedLicense) + val sf = StringFormatter.getInitializedTestInstance val aLicense = License.unsafeFrom("CC-BY-4.0") @@ -75,16 +98,6 @@ object ReadResourceV2LensLearningSpec extends ZIOSpecDefault { val fileValueWithACopyrightAttribution = FileValueV2("internalFilename", "internalMimeType", None, None, Some(aCopyrightAttribution), None) - val setLicenseIfMissing: Option[License] => ReadValueV2 => ReadValueV2 = newLicense => - value => { - val composed = fileValueContentLens.andThen(fileValueContentPrism).andThen(fileValueLens).andThen(licenseLens) - val existing: Option[License] =composed.getOption(value).flatten - existing match { - case Some(_) => value - case None => composed.replace(newLicense)(value) - } - } - private val fileValueLensesSuite = suite("FileValueV2 lenses")( test("licenseLens should replace an empty license with a new one") { val actual = licenseLens.replace(Some(aLicense))(fileValueWithoutLicenseOrCopyright) From 76363a76847a0c72cd4ec018acea0f0eb616f2cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 20 Nov 2024 18:43:23 +0100 Subject: [PATCH 07/31] rm learning test --- .../v2/ReadResourceV2LensLearningSpec.scala | 125 ------------------ 1 file changed, 125 deletions(-) delete mode 100644 webapi/src/test/scala/org/knora/webapi/responders/v2/ReadResourceV2LensLearningSpec.scala diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/ReadResourceV2LensLearningSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/ReadResourceV2LensLearningSpec.scala deleted file mode 100644 index ca70a2aa18..0000000000 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/ReadResourceV2LensLearningSpec.scala +++ /dev/null @@ -1,125 +0,0 @@ -/* - * 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.responders.v2 -import monocle.* -import monocle.Lens -import monocle.Optional -import monocle.macros.* -import zio.test.* - -import org.knora.webapi.messages.SmartIri -import org.knora.webapi.messages.StringFormatter -import org.knora.webapi.messages.v2.responder.resourcemessages.ReadResourceV2 -import org.knora.webapi.messages.v2.responder.valuemessages.* -import org.knora.webapi.slice.admin.domain.model.KnoraProject.CopyrightAttribution -import org.knora.webapi.slice.admin.domain.model.KnoraProject.License - -object ReadResourceV2LensLearningSpec extends ZIOSpecDefault { - - val fileValueContentLens: Lens[ReadValueV2, ValueContentV2] = - Lens[ReadValueV2, ValueContentV2](_.valueContent)(fc => { - case rv: ReadLinkValueV2 => - fc match { - case lv: LinkValueContentV2 => rv.copy(valueContent = lv) - case _ => rv - } - case rv: ReadTextValueV2 => - fc match { - case tv: TextValueContentV2 => rv.copy(valueContent = tv) - case _ => rv - } - case ov: ReadOtherValueV2 => ov.copy(valueContent = fc) - }) - - val fileValueContentPrism: Prism[ValueContentV2, FileValueContentV2] = - GenPrism[ValueContentV2, FileValueContentV2] - - val fileValueLens: Lens[FileValueContentV2, FileValueV2] = - Lens[FileValueContentV2, FileValueV2](_.fileValue)(fv => { - case vc: MovingImageFileValueContentV2 => vc.copy(fileValue = fv) - case vc: StillImageFileValueContentV2 => vc.copy(fileValue = fv) - case vc: AudioFileValueContentV2 => vc.copy(fileValue = fv) - case vc: DocumentFileValueContentV2 => vc.copy(fileValue = fv) - case vc: StillImageExternalFileValueContentV2 => vc.copy(fileValue = fv) - case vc: ArchiveFileValueContentV2 => vc.copy(fileValue = fv) - case vc: TextFileValueContentV2 => vc.copy(fileValue = fv) - }) - - val copyrightAttributionLens: Lens[FileValueV2, Option[CopyrightAttribution]] = - GenLens[FileValueV2](_.copyrightAttribution) - val licenseLens: Lens[FileValueV2, Option[License]] = GenLens[FileValueV2](_.license) - - type ReadResourceValues = Map[SmartIri, Seq[ReadValueV2]] - - private val commonComposed: Optional[ReadValueV2, FileValueV2] = - fileValueContentLens.andThen(fileValueContentPrism).andThen(fileValueLens) - private val composedLicense = commonComposed.andThen(licenseLens) - private val composedCopyright = commonComposed.andThen(copyrightAttributionLens) - - private def setIfMissing[T]( - optional: Optional[ReadValueV2, Option[T]], - ): Option[T] => ReadResourceV2 => ReadResourceV2 = - newValue => - readResource => { - val newValues: ReadResourceValues = readResource.values.map((iri: SmartIri, seq: Seq[ReadValueV2]) => - ( - iri, - seq.map { readValue => - optional.getOption(readValue).flatten match { - case Some(_) => readValue - case None => optional.replace(newValue)(readValue) - } - }, - ), - ) - readResource.copy(values = newValues) - } - - val setCopyrightAttributionIfMissing: Option[CopyrightAttribution] => ReadResourceV2 => ReadResourceV2 = - setIfMissing(composedCopyright) - - val setLicenseIfMissing: Option[License] => ReadResourceV2 => ReadResourceV2 = setIfMissing(composedLicense) - - val sf = StringFormatter.getInitializedTestInstance - - val aLicense = License.unsafeFrom("CC-BY-4.0") - val anotherLicense = License.unsafeFrom("Apache-2.0") - - val aCopyrightAttribution = CopyrightAttribution.unsafeFrom("2020, John Doe") - val anotherCopyrightAttribution = CopyrightAttribution.unsafeFrom("2024, Jane Doe") - - val fileValueWithoutLicenseOrCopyright = - FileValueV2("internalFilename", "internalMimeType", None, None, None, None) - val fileValueWithALicense = - FileValueV2("internalFilename", "internalMimeType", None, None, None, Some(aLicense)) - val fileValueWithACopyrightAttribution = - FileValueV2("internalFilename", "internalMimeType", None, None, Some(aCopyrightAttribution), None) - - private val fileValueLensesSuite = suite("FileValueV2 lenses")( - test("licenseLens should replace an empty license with a new one") { - val actual = licenseLens.replace(Some(aLicense))(fileValueWithoutLicenseOrCopyright) - assertTrue(actual.license == Some(aLicense)) - }, - test("licenseLens should replace an existing license with a new one") { - val actual = licenseLens.replace(Some(anotherLicense))(fileValueWithALicense) - assertTrue(actual.license == Some(anotherLicense)) - }, - test("copyrightAttributionLens should replace an empty copyrightAttribution with a new one") { - val aCopyrightAttribution = CopyrightAttribution.unsafeFrom("2020, John Doe") - val actual = copyrightAttributionLens.replace(Some(aCopyrightAttribution))(fileValueWithoutLicenseOrCopyright) - assertTrue(actual.copyrightAttribution == Some(aCopyrightAttribution)) - }, - test("copyrightAttributionLens should replace an existing copyrightAttribution with a new one") { - val actual = - copyrightAttributionLens.replace(Some(anotherCopyrightAttribution))(fileValueWithACopyrightAttribution) - assertTrue(actual.copyrightAttribution == Some(anotherCopyrightAttribution)) - }, - ) - - val spec = suite("ReadResourceV2LensLearning")( - fileValueLensesSuite, - ) -} From 7fe7755b69b9bab927f10ad0794ae8001c3b3fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 21 Nov 2024 13:35:05 +0100 Subject: [PATCH 08/31] add fallback to nested resources of link resources --- .../resourcemessages/ResourceMessagesV2.scala | 53 ++++++++++++++++--- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index 47d01fa1f5..a1dab2880d 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -578,6 +578,39 @@ case class ReadResourceV2( object ReadResourceV2 { + private def setCopyrightAndLicenceIfMissingOnLinkedResources( + copyright: Option[CopyrightAttribution], + license: Option[License], + ): ReadResourceV2 => ReadResourceV2 = + rr => { + val newValues: Map[SmartIri, Seq[ReadValueV2]] = rr.values.map((iri: SmartIri, seq: Seq[ReadValueV2]) => + ( + iri, + seq.map { + case lv: ReadLinkValueV2 => + linkValueFromReadValue + .andThen(nestedResourceFromLinkValueContent) + .modifyOption(setCopyrightAndLicenceIfMissingResourceValues(copyright, license))(lv) + .getOrElse(lv) + case other => other + }, + ), + ) + rr.copy(values = newValues) + } + + private val linkValueFromReadValue = Optional[ReadValueV2, LinkValueContentV2] { + case lv: ReadLinkValueV2 => Some(lv.valueContent) + case _ => None + }(lv => { + case rv: ReadLinkValueV2 => rv.copy(valueContent = lv) + case rv: ReadOtherValueV2 => rv.copy(valueContent = lv) + case rv: ReadTextValueV2 => rv + }) + + private val nestedResourceFromLinkValueContent = + Optional[LinkValueContentV2, ReadResourceV2](_.nestedResource)(rr => lv => lv.copy(nestedResource = Some(rr))) + private val fileValueContentLens: Lens[ReadValueV2, ValueContentV2] = Lens[ReadValueV2, ValueContentV2](_.valueContent)(fc => { case rv: ReadLinkValueV2 => @@ -613,7 +646,7 @@ object ReadResourceV2 { private val licenseLens: Lens[FileValueV2, Option[License]] = GenLens[FileValueV2](_.license) - private val commonComposed: Optional[ReadValueV2, FileValueV2] = + private val fileValueFromReadValue: Optional[ReadValueV2, FileValueV2] = fileValueContentLens.andThen(fileValueContentPrism).andThen(fileValueLens) private def setIfMissing[T]( @@ -636,17 +669,23 @@ object ReadResourceV2 { readResource.copy(values = newValues) } - private val setCopyrightAttributionIfMissing: Option[CopyrightAttribution] => ReadResourceV2 => ReadResourceV2 = - setIfMissing(commonComposed.andThen(copyrightAttributionLens)) - - private val setLicenseIfMissing: Option[License] => ReadResourceV2 => ReadResourceV2 = - setIfMissing(commonComposed.andThen(licenseLens)) + private def setCopyrightAndLicenceIfMissingResourceValues( + copyright: Option[CopyrightAttribution], + license: Option[License], + ): ReadResourceV2 => ReadResourceV2 = { + val licenseOptional: Optional[ReadValueV2, Option[License]] = + fileValueFromReadValue.andThen(licenseLens) + val copyrightAttributionOptional: Optional[ReadValueV2, Option[CopyrightAttribution]] = + fileValueFromReadValue.andThen(copyrightAttributionLens) + setIfMissing(licenseOptional)(license).andThen(setIfMissing(copyrightAttributionOptional)(copyright)) + } def setCopyrightAndLicenceIfMissing( copyright: Option[CopyrightAttribution], license: Option[License], ): ReadResourceV2 => ReadResourceV2 = - setCopyrightAttributionIfMissing(copyright).andThen(setLicenseIfMissing(license)) + setCopyrightAndLicenceIfMissingResourceValues(copyright, license) + .andThen(setCopyrightAndLicenceIfMissingOnLinkedResources(copyright, license)) } /** From 127f8de3013d84bb4145123297bf8a41b3f15fb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 21 Nov 2024 14:34:37 +0100 Subject: [PATCH 09/31] add more heap to sbt builds --- .sbtopts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.sbtopts b/.sbtopts index 1ca6d0b39d..2b63e3b2d8 100644 --- a/.sbtopts +++ b/.sbtopts @@ -1 +1 @@ --J-Xmx2G +-J-Xmx4G From 7eeacf350fbc8a75345c1911047f47ed523dc47e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 21 Nov 2024 14:43:20 +0100 Subject: [PATCH 10/31] refactor: reorder --- .../resourcemessages/ResourceMessagesV2.scala | 120 ++++++++---------- 1 file changed, 53 insertions(+), 67 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index a1dab2880d..e219357408 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -578,38 +578,35 @@ case class ReadResourceV2( object ReadResourceV2 { - private def setCopyrightAndLicenceIfMissingOnLinkedResources( + def setCopyrightAndLicenceIfMissing( copyright: Option[CopyrightAttribution], license: Option[License], ): ReadResourceV2 => ReadResourceV2 = - rr => { - val newValues: Map[SmartIri, Seq[ReadValueV2]] = rr.values.map((iri: SmartIri, seq: Seq[ReadValueV2]) => - ( - iri, - seq.map { - case lv: ReadLinkValueV2 => - linkValueFromReadValue - .andThen(nestedResourceFromLinkValueContent) - .modifyOption(setCopyrightAndLicenceIfMissingResourceValues(copyright, license))(lv) - .getOrElse(lv) - case other => other - }, - ), - ) - rr.copy(values = newValues) - } + setCopyrightAndLicenceIfMissingResourceValues(copyright, license) + .andThen(setCopyrightAndLicenceIfMissingOnLinkedResources(copyright, license)) - private val linkValueFromReadValue = Optional[ReadValueV2, LinkValueContentV2] { - case lv: ReadLinkValueV2 => Some(lv.valueContent) - case _ => None - }(lv => { - case rv: ReadLinkValueV2 => rv.copy(valueContent = lv) - case rv: ReadOtherValueV2 => rv.copy(valueContent = lv) - case rv: ReadTextValueV2 => rv - }) + private def setCopyrightAndLicenceIfMissingResourceValues( + copyright: Option[CopyrightAttribution], + license: Option[License], + ): ReadResourceV2 => ReadResourceV2 = + setIfMissing(licenseLens)(license).andThen(setIfMissing(copyrightAttributionLens)(copyright)) + + private val copyrightAttributionLens = GenLens[FileValueV2](_.copyrightAttribution) + private val licenseLens = GenLens[FileValueV2](_.license) + + private def ifMissingMapper[T](optional: Optional[ReadValueV2, Option[T]], newValue: Option[T]) = + (smartIri: SmartIri, seq: Seq[ReadValueV2]) => + ( + smartIri, + seq.map { readValue => + optional.getOption(readValue).flatten match + case Some(_) => readValue + case None => optional.replace(newValue)(readValue) + }, + ) - private val nestedResourceFromLinkValueContent = - Optional[LinkValueContentV2, ReadResourceV2](_.nestedResource)(rr => lv => lv.copy(nestedResource = Some(rr))) + private def setIfMissing[T](opt: Optional[FileValueV2, Option[T]]): Option[T] => ReadResourceV2 => ReadResourceV2 = + value => rr => rr.copy(values = rr.values.map(ifMissingMapper(fileValueFromReadValue.andThen(opt), value)(_, _))) private val fileValueContentLens: Lens[ReadValueV2, ValueContentV2] = Lens[ReadValueV2, ValueContentV2](_.valueContent)(fc => { @@ -640,52 +637,41 @@ object ReadResourceV2 { case vc: TextFileValueContentV2 => vc.copy(fileValue = fv) }) - private val copyrightAttributionLens: Lens[FileValueV2, Option[CopyrightAttribution]] = - GenLens[FileValueV2](_.copyrightAttribution) - - private val licenseLens: Lens[FileValueV2, Option[License]] = - GenLens[FileValueV2](_.license) - private val fileValueFromReadValue: Optional[ReadValueV2, FileValueV2] = fileValueContentLens.andThen(fileValueContentPrism).andThen(fileValueLens) - private def setIfMissing[T]( - optional: Optional[ReadValueV2, Option[T]], - ): Option[T] => ReadResourceV2 => ReadResourceV2 = - newValue => - readResource => { - val newValues: Map[SmartIri, Seq[ReadValueV2]] = - readResource.values.map((iri: SmartIri, seq: Seq[ReadValueV2]) => - ( - iri, - seq.map { readValue => - optional.getOption(readValue).flatten match { - case Some(_) => readValue - case None => optional.replace(newValue)(readValue) - } - }, - ), - ) - readResource.copy(values = newValues) - } - - private def setCopyrightAndLicenceIfMissingResourceValues( - copyright: Option[CopyrightAttribution], - license: Option[License], - ): ReadResourceV2 => ReadResourceV2 = { - val licenseOptional: Optional[ReadValueV2, Option[License]] = - fileValueFromReadValue.andThen(licenseLens) - val copyrightAttributionOptional: Optional[ReadValueV2, Option[CopyrightAttribution]] = - fileValueFromReadValue.andThen(copyrightAttributionLens) - setIfMissing(licenseOptional)(license).andThen(setIfMissing(copyrightAttributionOptional)(copyright)) - } - - def setCopyrightAndLicenceIfMissing( + private def setCopyrightAndLicenceIfMissingOnLinkedResources( copyright: Option[CopyrightAttribution], license: Option[License], ): ReadResourceV2 => ReadResourceV2 = - setCopyrightAndLicenceIfMissingResourceValues(copyright, license) - .andThen(setCopyrightAndLicenceIfMissingOnLinkedResources(copyright, license)) + 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 => + linkValueFromReadValue + .andThen(nestedResourceFromLinkValueContent) + .modifyOption(setCopyrightAndLicenceIfMissingResourceValues(copyright, license))(lv) + .getOrElse(lv) + case other => other + }, + ) + + private val linkValueFromReadValue = Optional[ReadValueV2, LinkValueContentV2] { + case lv: ReadLinkValueV2 => Some(lv.valueContent) + case _ => None + }(lv => { + case rv: ReadLinkValueV2 => rv.copy(valueContent = lv) + case rv: ReadOtherValueV2 => rv.copy(valueContent = lv) + case rv: ReadTextValueV2 => rv + }) + + private val nestedResourceFromLinkValueContent = + Optional[LinkValueContentV2, ReadResourceV2](_.nestedResource)(rr => lv => lv.copy(nestedResource = Some(rr))) + } /** From 308e460c374955b7de4c4b13bf9040c874a6cb9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 21 Nov 2024 16:01:04 +0100 Subject: [PATCH 11/31] update dsp-app in docker-compose.yml --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 59720ccb35..266fa77fc5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: app: - image: daschswiss/dsp-app:v11.21.0 + image: daschswiss/dsp-app:v11.22.1 ports: - "4200:4200" networks: From 84f96ec39ffe0b64c1786333bb827523348cc5a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 21 Nov 2024 18:04:15 +0100 Subject: [PATCH 12/31] add more tests --- .../it/v2/CopyrightAndLicensesSpec.scala | 95 ++++++++++++++----- .../webapi/slice/common/jena/ModelOps.scala | 12 ++- 2 files changed, 79 insertions(+), 28 deletions(-) diff --git a/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala b/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala index c0e9ee3170..9d1715aea2 100644 --- a/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala @@ -38,21 +38,38 @@ import org.knora.webapi.slice.resourceinfo.domain.IriConverter object CopyrightAndLicensesSpec extends E2EZSpec { - private val copyrightAttribution = CopyrightAttribution.unsafeFrom("2020, Example") - private val license = License.unsafeFrom("CC BY-SA 4.0") + private val aCopyrightAttribution = CopyrightAttribution.unsafeFrom("2020, On FileValue") + private val aLicense = License.unsafeFrom("CC BY-SA 4.0") + + private val projectCopyrightAttribution = CopyrightAttribution.unsafeFrom("2024, On Project") + private val projectLicense = License.unsafeFrom("Apache-2.0") val e2eSpec: Spec[Scope & env, Any] = suite("Copyright Attribution and Licenses")( + test( + "given the project does neither have a default license nor a default copyright attribution " + + "when creating a resource without copyright attribution and license " + + "the creation response should not contain the license and copyright attribution", + ) { + for { + createResourceResponseModel <- createStillImageResource() + actualCreatedCopyright <- copyrightValueOption(createResourceResponseModel) + actualCreatedLicense <- licenseValueOption(createResourceResponseModel) + } yield assertTrue( + actualCreatedCopyright.isEmpty, + actualCreatedLicense.isEmpty, + ) + }, test( "when creating a resource with copyright attribution and license " + "the creation response should contain the license and copyright attribution", ) { for { - createResourceResponseModel <- createStillImageResource(Some(copyrightAttribution), Some(license)) + createResourceResponseModel <- createStillImageResource(Some(aCopyrightAttribution), Some(aLicense)) actualCreatedCopyright <- copyrightValue(createResourceResponseModel) actualCreatedLicense <- licenseValue(createResourceResponseModel) } yield assertTrue( - actualCreatedCopyright == copyrightAttribution.value, - actualCreatedLicense == license.value, + actualCreatedCopyright == aCopyrightAttribution.value, + actualCreatedLicense == aLicense.value, ) }, test( @@ -60,14 +77,14 @@ object CopyrightAndLicensesSpec extends E2EZSpec { "the response when getting the created resource should contain the license and copyright attribution", ) { for { - createResourceResponseModel <- createStillImageResource(Some(copyrightAttribution), Some(license)) + createResourceResponseModel <- createStillImageResource(Some(aCopyrightAttribution), Some(aLicense)) resourceId <- resourceId(createResourceResponseModel) getResponseModel <- getResourceFromApi(resourceId) actualCopyright <- copyrightValue(getResponseModel) actualLicense <- licenseValue(getResponseModel) } yield assertTrue( - actualCopyright == copyrightAttribution.value, - actualLicense == license.value, + actualCopyright == aCopyrightAttribution.value, + actualLicense == aLicense.value, ) }, test( @@ -75,13 +92,13 @@ object CopyrightAndLicensesSpec extends E2EZSpec { "the response when getting the created value should contain the license and copyright attribution", ) { for { - createResourceResponseModel <- createStillImageResource(Some(copyrightAttribution), Some(license)) + createResourceResponseModel <- createStillImageResource(Some(aCopyrightAttribution), Some(aLicense)) valueResponseModel <- getValueFromApi(createResourceResponseModel) actualCopyright <- copyrightValue(valueResponseModel) actualLicense <- licenseValue(valueResponseModel) } yield assertTrue( - actualCopyright == copyrightAttribution.value, - actualLicense == license.value, + actualCopyright == aCopyrightAttribution.value, + actualLicense == aLicense.value, ) }, test( @@ -90,24 +107,42 @@ object CopyrightAndLicensesSpec extends E2EZSpec { "then the response when getting the created value should contain the default license and default copyright attribution", ) { for { - projectService <- ZIO.service[KnoraProjectService] - prj <- - projectService.findByShortcode(Shortcode.unsafeFrom("0001")).someOrFail(new Exception("Project not found")) - _ <- projectService.updateProject( - prj, - ProjectUpdateRequest(copyrightAttribution = Some(copyrightAttribution), license = Some(license)), - ) + _ <- addCopyrightAttributionAndLicenseToProject(projectCopyrightAttribution, projectLicense) createResourceResponseModel <- createStillImageResource() valueResponseModel <- getValueFromApi(createResourceResponseModel) actualCopyright <- copyrightValue(valueResponseModel) actualLicense <- licenseValue(valueResponseModel) } yield assertTrue( - actualCopyright == copyrightAttribution.value, - actualLicense == license.value, + actualCopyright == projectCopyrightAttribution.value, + actualLicense == projectLicense.value, + ) + }, + test( + "when creating a resource with copyright attribution and without license " + + "given the project has a default license and default copyright attribution " + + "then the response when getting the created value should contain the license and copyright attribution from resource", + ) { + for { + _ <- addCopyrightAttributionAndLicenseToProject(projectCopyrightAttribution, projectLicense) + createResourceResponseModel <- createStillImageResource(Some(aCopyrightAttribution), Some(aLicense)) + valueResponseModel <- getValueFromApi(createResourceResponseModel) + actualCopyright <- copyrightValue(valueResponseModel) + actualLicense <- licenseValue(valueResponseModel) + } yield assertTrue( + actualCopyright == aCopyrightAttribution.value, + actualLicense == aLicense.value, ) }, ) + private def addCopyrightAttributionAndLicenseToProject(copyrightAttribution: CopyrightAttribution, license: License) = + for { + projectService <- ZIO.service[KnoraProjectService] + prj <- projectService.findByShortcode(Shortcode.unsafeFrom("0001")).someOrFail(new Exception("Project not found")) + change = ProjectUpdateRequest(copyrightAttribution = Some(copyrightAttribution), license = Some(license)) + updated <- projectService.updateProject(prj, change) + } yield updated + private def createStillImageResource( copyrightAttribution: Option[CopyrightAttribution] = None, license: Option[License] = None, @@ -176,8 +211,20 @@ object CopyrightAndLicensesSpec extends E2EZSpec { case _ => ZIO.fail(Exception("Multiple values found")) } - private def copyrightValue(model: Model) = singleStringValue(model, HasCopyrightAttribution) - private def licenseValue(model: Model) = singleStringValue(model, HasLicense) - private def singleStringValue(model: Model, property: Property) = - ZIO.fromEither(model.singleSubjectWithProperty(property).flatMap(_.objectString(property))).mapError(Exception(_)) + private def copyrightValue(model: Model) = + singleStringValueOption(model, HasCopyrightAttribution).someOrFail(new Exception("No copyright found")) + private def copyrightValueOption(model: Model) = + singleStringValueOption(model, HasCopyrightAttribution) + private def licenseValue(model: Model) = + singleStringValueOption(model, HasLicense).someOrFail(new Exception("No license found")) + private def licenseValueOption(model: Model) = + singleStringValueOption(model, HasLicense) + private def singleStringValueOption(model: Model, property: Property): Task[Option[String]] = + ZIO + .fromEither( + model + .singleSubjectWithPropertyOption(property) + .flatMap(_.map(_.objectStringOption(property)).fold(Right(None))(identity)), + ) + .mapError(Exception(_)) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/jena/ModelOps.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/jena/ModelOps.scala index dded0c525e..8c5a32ee9a 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/jena/ModelOps.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/jena/ModelOps.scala @@ -48,14 +48,18 @@ object ModelOps { self => case iris => Left(s"Multiple root resources found in model: ${iris.mkString(", ")}") } - def singleSubjectWithProperty(property: Property): Either[String, Resource] = + def singleSubjectWithPropertyOption(property: Property): Either[String, Option[Resource]] = val subjects = model.listSubjectsWithProperty(property).asScala.toList subjects match { - case s :: Nil => Right(s) - case Nil => Left(s"No resource found with property ${property.getURI}") - case _ => Left(s"Multiple resources found with property ${property.getURI}") + case s :: Nil => Right(Some(s)) + case Nil => Right(None) + case _ => Left(s"Multiple subjects found with property ${property.getURI}") } + def singleSubjectWithProperty(property: Property): Either[String, Resource] = + singleSubjectWithPropertyOption(property).flatMap( + _.toRight(s"No resource found with property ${property.getURI}"), + ) } def fromJsonLd(str: String): ZIO[Scope, String, Model] = from(str, Lang.JSONLD) From 8ebf7921855231c958dc079515c1257216638786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 21 Nov 2024 18:18:38 +0100 Subject: [PATCH 13/31] fmt --- .../org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala b/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala index 9d1715aea2..aa0e890881 100644 --- a/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala @@ -26,7 +26,6 @@ import org.knora.webapi.models.filemodels.UploadFileRequest import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequestsAndResponses.ProjectUpdateRequest 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.KnoraProject.Shortcode import org.knora.webapi.slice.admin.domain.service.KnoraProjectService import org.knora.webapi.slice.common.KnoraIris.ValueIri @@ -154,11 +153,7 @@ object CopyrightAndLicensesSpec extends E2EZSpec { copyrightAttribution = copyrightAttribution, license = license, ) - .toJsonLd( - className = Some("ThingPicture"), - ontologyName = "anything", - ) - + .toJsonLd(className = Some("ThingPicture"), ontologyName = "anything") for { responseBody <- sendPostRequestAsRoot("/v2/resources", jsonLd) .filterOrFail(_.status.isSuccess)(s"Failed to create resource") From 49a948fc13a4c1c662504e44c5c620027844c2ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 21 Nov 2024 18:38:16 +0100 Subject: [PATCH 14/31] log api response when failing --- .../knora/webapi/it/v2/CopyrightAndLicensesSpec.scala | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala b/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala index aa0e890881..e71f37767a 100644 --- a/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala @@ -15,7 +15,6 @@ import zio.test.* import java.net.URLEncoder import scala.jdk.CollectionConverters.IteratorHasAsScala import scala.language.implicitConversions - import org.knora.webapi.E2EZSpec import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.OntologyConstants.KnoraApiV2Complex.HasCopyrightAttribution @@ -34,6 +33,7 @@ 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.resourceinfo.domain.IriConverter +import zio.http.Response object CopyrightAndLicensesSpec extends E2EZSpec { @@ -142,6 +142,9 @@ object CopyrightAndLicensesSpec extends E2EZSpec { updated <- projectService.updateProject(prj, change) } yield updated + private def failResponse(msg: String)(response: Response) = + response.body.asString.flatMap(bodyStr => ZIO.fail(Exception(s"$msg\nstatus: ${response.status}\nbody: $bodyStr"))) + private def createStillImageResource( copyrightAttribution: Option[CopyrightAttribution] = None, license: Option[License] = None, @@ -156,8 +159,8 @@ object CopyrightAndLicensesSpec extends E2EZSpec { .toJsonLd(className = Some("ThingPicture"), ontologyName = "anything") for { responseBody <- sendPostRequestAsRoot("/v2/resources", jsonLd) - .filterOrFail(_.status.isSuccess)(s"Failed to create resource") .mapError(Exception(_)) + .filterOrElseWith(_.status.isSuccess)(failResponse(s"Failed to create resource")) .flatMap(_.body.asString) createResourceResponseModel <- ModelOps.fromJsonLd(responseBody).mapError(Exception(_)) } yield createResourceResponseModel @@ -165,7 +168,7 @@ object CopyrightAndLicensesSpec extends E2EZSpec { private def getResourceFromApi(resourceId: String) = for { responseBody <- sendGetRequest(s"/v2/resources/${URLEncoder.encode(resourceId, "UTF-8")}") - .filterOrFail(_.status.isSuccess)(s"Failed to get resource $resourceId") + .filterOrElseWith(_.status.isSuccess)(failResponse(s"Failed to get resource $resourceId.")) .flatMap(_.body.asString) model <- ModelOps.fromJsonLd(responseBody).mapError(Exception(_)) } yield model @@ -174,7 +177,7 @@ object CopyrightAndLicensesSpec extends E2EZSpec { valueId <- valueId(createResourceResponse) resourceId <- resourceId(createResourceResponse) responseBody <- sendGetRequest(s"/v2/values/${URLEncoder.encode(resourceId, "UTF-8")}/${valueId.valueId}") - .filterOrFail(_.status.isSuccess)(s"Failed to get resource $valueId") + .filterOrElseWith(_.status.isSuccess)(failResponse(s"Failed to get value $resourceId.")) .flatMap(_.body.asString) model <- ModelOps.fromJsonLd(responseBody).mapError(Exception(_)) } yield model From 3fb0a7b92c2df4990bcfd94a6d8943f2275ca623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Fri, 22 Nov 2024 09:23:54 +0100 Subject: [PATCH 15/31] extract optics --- .../it/v2/CopyrightAndLicensesSpec.scala | 3 +- .../resourcemessages/ResourceMessagesV2.scala | 49 +++---------- .../valuemessages/ValueMessagesV2Optics.scala | 70 +++++++++++++++++++ 3 files changed, 80 insertions(+), 42 deletions(-) create mode 100644 webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala diff --git a/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala b/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala index e71f37767a..5a31a0c130 100644 --- a/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala @@ -10,11 +10,13 @@ import org.apache.jena.rdf.model.Property import org.apache.jena.rdf.model.Resource import org.apache.jena.vocabulary.RDF import zio.* +import zio.http.Response import zio.test.* import java.net.URLEncoder import scala.jdk.CollectionConverters.IteratorHasAsScala import scala.language.implicitConversions + import org.knora.webapi.E2EZSpec import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.OntologyConstants.KnoraApiV2Complex.HasCopyrightAttribution @@ -33,7 +35,6 @@ 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.resourceinfo.domain.IriConverter -import zio.http.Response object CopyrightAndLicensesSpec extends E2EZSpec { diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index e219357408..99b999dc1c 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -5,11 +5,7 @@ package org.knora.webapi.messages.v2.responder.resourcemessages -import monocle.Lens import monocle.Optional -import monocle.Prism -import monocle.macros.GenLens -import monocle.macros.GenPrism import java.time.Instant import java.util.UUID @@ -32,6 +28,7 @@ import org.knora.webapi.messages.util.standoff.XMLUtil import org.knora.webapi.messages.v2.responder.* 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.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 @@ -589,10 +586,8 @@ object ReadResourceV2 { copyright: Option[CopyrightAttribution], license: Option[License], ): ReadResourceV2 => ReadResourceV2 = - setIfMissing(licenseLens)(license).andThen(setIfMissing(copyrightAttributionLens)(copyright)) - - private val copyrightAttributionLens = GenLens[FileValueV2](_.copyrightAttribution) - private val licenseLens = GenLens[FileValueV2](_.license) + setIfMissing(FileValueV2Optics.licenseLens)(license) + .andThen(setIfMissing(FileValueV2Optics.copyrightAttributionLens)(copyright)) private def ifMissingMapper[T](optional: Optional[ReadValueV2, Option[T]], newValue: Option[T]) = (smartIri: SmartIri, seq: Seq[ReadValueV2]) => @@ -606,39 +601,11 @@ object ReadResourceV2 { ) private def setIfMissing[T](opt: Optional[FileValueV2, Option[T]]): Option[T] => ReadResourceV2 => ReadResourceV2 = - value => rr => rr.copy(values = rr.values.map(ifMissingMapper(fileValueFromReadValue.andThen(opt), value)(_, _))) - - private val fileValueContentLens: Lens[ReadValueV2, ValueContentV2] = - Lens[ReadValueV2, ValueContentV2](_.valueContent)(fc => { - case rv: ReadLinkValueV2 => - fc match { - case lv: LinkValueContentV2 => rv.copy(valueContent = lv) - case _ => rv - } - case rv: ReadTextValueV2 => - fc match { - case tv: TextValueContentV2 => rv.copy(valueContent = tv) - case _ => rv - } - case ov: ReadOtherValueV2 => ov.copy(valueContent = fc) - }) - - private val fileValueContentPrism: Prism[ValueContentV2, FileValueContentV2] = - GenPrism[ValueContentV2, FileValueContentV2] - - private val fileValueLens: Lens[FileValueContentV2, FileValueV2] = - Lens[FileValueContentV2, FileValueV2](_.fileValue)(fv => { - case vc: MovingImageFileValueContentV2 => vc.copy(fileValue = fv) - case vc: StillImageFileValueContentV2 => vc.copy(fileValue = fv) - case vc: AudioFileValueContentV2 => vc.copy(fileValue = fv) - case vc: DocumentFileValueContentV2 => vc.copy(fileValue = fv) - case vc: StillImageExternalFileValueContentV2 => vc.copy(fileValue = fv) - case vc: ArchiveFileValueContentV2 => vc.copy(fileValue = fv) - case vc: TextFileValueContentV2 => vc.copy(fileValue = fv) - }) - - private val fileValueFromReadValue: Optional[ReadValueV2, FileValueV2] = - fileValueContentLens.andThen(fileValueContentPrism).andThen(fileValueLens) + value => + rr => + rr.copy(values = + rr.values.map(ifMissingMapper(ReadValueV2Optics.fileValueFromReadValue.andThen(opt), value)(_, _)), + ) private def setCopyrightAndLicenceIfMissingOnLinkedResources( copyright: Option[CopyrightAttribution], diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala new file mode 100644 index 0000000000..ce0d48cb53 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala @@ -0,0 +1,70 @@ +/* + * 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.messages.v2.responder.valuemessages + +import monocle.* +import monocle.macros.* + +import org.knora.webapi.slice.admin.domain.model.KnoraProject.CopyrightAttribution +import org.knora.webapi.slice.admin.domain.model.KnoraProject.License + +object ValueMessagesV2Optics { + + object FileValueV2Optics { + + val copyrightAttributionLens: Lens[FileValueV2, Option[CopyrightAttribution]] = + GenLens[FileValueV2](_.copyrightAttribution) + + val licenseLens: Lens[FileValueV2, Option[License]] = + GenLens[FileValueV2](_.license) + + } + + object FileValueContentV2Optics { + + val fileValueLens: Lens[FileValueContentV2, FileValueV2] = + Lens[FileValueContentV2, FileValueV2](_.fileValue)(fv => { + case vc: MovingImageFileValueContentV2 => vc.copy(fileValue = fv) + case vc: StillImageFileValueContentV2 => vc.copy(fileValue = fv) + case vc: AudioFileValueContentV2 => vc.copy(fileValue = fv) + case vc: DocumentFileValueContentV2 => vc.copy(fileValue = fv) + case vc: StillImageExternalFileValueContentV2 => vc.copy(fileValue = fv) + case vc: ArchiveFileValueContentV2 => vc.copy(fileValue = fv) + case vc: TextFileValueContentV2 => vc.copy(fileValue = fv) + }) + + } + + object ReadValueV2Optics { + + val fileValueContentLens: Lens[ReadValueV2, ValueContentV2] = + Lens[ReadValueV2, ValueContentV2](_.valueContent)(fc => { + case rv: ReadLinkValueV2 => + fc match { + case lv: LinkValueContentV2 => rv.copy(valueContent = lv) + case _ => rv + } + case rv: ReadTextValueV2 => + fc match { + case tv: TextValueContentV2 => rv.copy(valueContent = tv) + case _ => rv + } + case ov: ReadOtherValueV2 => ov.copy(valueContent = fc) + }) + + val fileValueFromReadValue: Optional[ReadValueV2, FileValueV2] = + ReadValueV2Optics.fileValueContentLens + .andThen(ValueContentV2Optics.fileValueContentPrism) + .andThen(FileValueContentV2Optics.fileValueLens) + + } + + object ValueContentV2Optics { + + val fileValueContentPrism: Prism[ValueContentV2, FileValueContentV2] = GenPrism[ValueContentV2, FileValueContentV2] + + } +} From 7a1051416dded79e20db55491635abb466d79a60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Fri, 22 Nov 2024 10:06:51 +0100 Subject: [PATCH 16/31] simplify optic --- .../valuemessages/ValueMessagesV2Optics.scala | 27 +++++-------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala index ce0d48cb53..4d283f5868 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala @@ -6,8 +6,8 @@ package org.knora.webapi.messages.v2.responder.valuemessages import monocle.* +import monocle.Optional import monocle.macros.* - import org.knora.webapi.slice.admin.domain.model.KnoraProject.CopyrightAttribution import org.knora.webapi.slice.admin.domain.model.KnoraProject.License @@ -40,31 +40,16 @@ object ValueMessagesV2Optics { object ReadValueV2Optics { - val fileValueContentLens: Lens[ReadValueV2, ValueContentV2] = - Lens[ReadValueV2, ValueContentV2](_.valueContent)(fc => { - case rv: ReadLinkValueV2 => - fc match { - case lv: LinkValueContentV2 => rv.copy(valueContent = lv) - case _ => rv - } - case rv: ReadTextValueV2 => - fc match { - case tv: TextValueContentV2 => rv.copy(valueContent = tv) - case _ => rv - } + val fileValueContentOptional: Optional[ReadValueV2, FileValueContentV2] = + Optional[ReadValueV2, FileValueContentV2](_.valueContent.asOpt[FileValueContentV2])(fc => { + case rv: ReadLinkValueV2 => rv + case rv: ReadTextValueV2 => rv case ov: ReadOtherValueV2 => ov.copy(valueContent = fc) }) val fileValueFromReadValue: Optional[ReadValueV2, FileValueV2] = - ReadValueV2Optics.fileValueContentLens - .andThen(ValueContentV2Optics.fileValueContentPrism) + ReadValueV2Optics.fileValueContentOptional .andThen(FileValueContentV2Optics.fileValueLens) } - - object ValueContentV2Optics { - - val fileValueContentPrism: Prism[ValueContentV2, FileValueContentV2] = GenPrism[ValueContentV2, FileValueContentV2] - - } } From a8d9917ea111892dce1ad1d2a1959f3d4a6ea3ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Fri, 22 Nov 2024 10:09:47 +0100 Subject: [PATCH 17/31] align naming --- .../resourcemessages/ResourceMessagesV2.scala | 9 +++------ .../valuemessages/ValueMessagesV2Optics.scala | 14 +++++++------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index 99b999dc1c..3742265b3c 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -586,8 +586,8 @@ object ReadResourceV2 { copyright: Option[CopyrightAttribution], license: Option[License], ): ReadResourceV2 => ReadResourceV2 = - setIfMissing(FileValueV2Optics.licenseLens)(license) - .andThen(setIfMissing(FileValueV2Optics.copyrightAttributionLens)(copyright)) + setIfMissing(FileValueV2Optics.licenseOption)(license) + .andThen(setIfMissing(FileValueV2Optics.copyrightAttributionOption)(copyright)) private def ifMissingMapper[T](optional: Optional[ReadValueV2, Option[T]], newValue: Option[T]) = (smartIri: SmartIri, seq: Seq[ReadValueV2]) => @@ -602,10 +602,7 @@ object ReadResourceV2 { private def setIfMissing[T](opt: Optional[FileValueV2, Option[T]]): Option[T] => ReadResourceV2 => ReadResourceV2 = value => - rr => - rr.copy(values = - rr.values.map(ifMissingMapper(ReadValueV2Optics.fileValueFromReadValue.andThen(opt), value)(_, _)), - ) + rr => rr.copy(values = rr.values.map(ifMissingMapper(ReadValueV2Optics.fileValueV2.andThen(opt), value)(_, _))) private def setCopyrightAndLicenceIfMissingOnLinkedResources( copyright: Option[CopyrightAttribution], diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala index 4d283f5868..d24f490173 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala @@ -8,6 +8,7 @@ package org.knora.webapi.messages.v2.responder.valuemessages import monocle.* import monocle.Optional import monocle.macros.* + import org.knora.webapi.slice.admin.domain.model.KnoraProject.CopyrightAttribution import org.knora.webapi.slice.admin.domain.model.KnoraProject.License @@ -15,17 +16,17 @@ object ValueMessagesV2Optics { object FileValueV2Optics { - val copyrightAttributionLens: Lens[FileValueV2, Option[CopyrightAttribution]] = + val copyrightAttributionOption: Lens[FileValueV2, Option[CopyrightAttribution]] = GenLens[FileValueV2](_.copyrightAttribution) - val licenseLens: Lens[FileValueV2, Option[License]] = + val licenseOption: Lens[FileValueV2, Option[License]] = GenLens[FileValueV2](_.license) } object FileValueContentV2Optics { - val fileValueLens: Lens[FileValueContentV2, FileValueV2] = + val fileValueV2: Lens[FileValueContentV2, FileValueV2] = Lens[FileValueContentV2, FileValueV2](_.fileValue)(fv => { case vc: MovingImageFileValueContentV2 => vc.copy(fileValue = fv) case vc: StillImageFileValueContentV2 => vc.copy(fileValue = fv) @@ -40,16 +41,15 @@ object ValueMessagesV2Optics { object ReadValueV2Optics { - val fileValueContentOptional: Optional[ReadValueV2, FileValueContentV2] = + val fileValueContentV2: Optional[ReadValueV2, FileValueContentV2] = Optional[ReadValueV2, FileValueContentV2](_.valueContent.asOpt[FileValueContentV2])(fc => { case rv: ReadLinkValueV2 => rv case rv: ReadTextValueV2 => rv case ov: ReadOtherValueV2 => ov.copy(valueContent = fc) }) - val fileValueFromReadValue: Optional[ReadValueV2, FileValueV2] = - ReadValueV2Optics.fileValueContentOptional - .andThen(FileValueContentV2Optics.fileValueLens) + val fileValueV2: Optional[ReadValueV2, FileValueV2] = + ReadValueV2Optics.fileValueContentV2.andThen(FileValueContentV2Optics.fileValueV2) } } From ec050c683098e1446d03359d9e8dbfa86db85f35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Fri, 22 Nov 2024 10:32:30 +0100 Subject: [PATCH 18/31] align naming --- .../resourcemessages/ResourceMessagesV2.scala | 16 +--------------- .../valuemessages/ValueMessagesV2Optics.scala | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index 3742265b3c..3cc1f0012b 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -616,26 +616,12 @@ object ReadResourceV2 { iri, seq.map { case lv: ReadLinkValueV2 => - linkValueFromReadValue - .andThen(nestedResourceFromLinkValueContent) + ReadValueV2Optics.nestedResourceOfLinkValueContent .modifyOption(setCopyrightAndLicenceIfMissingResourceValues(copyright, license))(lv) .getOrElse(lv) case other => other }, ) - - private val linkValueFromReadValue = Optional[ReadValueV2, LinkValueContentV2] { - case lv: ReadLinkValueV2 => Some(lv.valueContent) - case _ => None - }(lv => { - case rv: ReadLinkValueV2 => rv.copy(valueContent = lv) - case rv: ReadOtherValueV2 => rv.copy(valueContent = lv) - case rv: ReadTextValueV2 => rv - }) - - private val nestedResourceFromLinkValueContent = - Optional[LinkValueContentV2, ReadResourceV2](_.nestedResource)(rr => lv => lv.copy(nestedResource = Some(rr))) - } /** diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala index d24f490173..981c95eaf5 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala @@ -9,6 +9,7 @@ import monocle.* import monocle.Optional import monocle.macros.* +import org.knora.webapi.messages.v2.responder.resourcemessages.ReadResourceV2 import org.knora.webapi.slice.admin.domain.model.KnoraProject.CopyrightAttribution import org.knora.webapi.slice.admin.domain.model.KnoraProject.License @@ -39,6 +40,13 @@ object ValueMessagesV2Optics { } + object LinkValueContentV2Optics { + + val nestedResource: Optional[LinkValueContentV2, ReadResourceV2] = + Optional[LinkValueContentV2, ReadResourceV2](_.nestedResource)(rr => lv => lv.copy(nestedResource = Some(rr))) + + } + object ReadValueV2Optics { val fileValueContentV2: Optional[ReadValueV2, FileValueContentV2] = @@ -51,5 +59,15 @@ object ValueMessagesV2Optics { val fileValueV2: Optional[ReadValueV2, FileValueV2] = ReadValueV2Optics.fileValueContentV2.andThen(FileValueContentV2Optics.fileValueV2) + val linkValueContentV2: Optional[ReadValueV2, LinkValueContentV2] = + Optional[ReadValueV2, LinkValueContentV2](_.valueContent.asOpt[LinkValueContentV2])(lv => { + case rv: ReadLinkValueV2 => rv.copy(valueContent = lv) + case rv: ReadOtherValueV2 => rv.copy(valueContent = lv) + case rv: ReadTextValueV2 => rv + }) + + val nestedResourceOfLinkValueContent: Optional[ReadValueV2, ReadResourceV2] = + ReadValueV2Optics.linkValueContentV2.andThen(LinkValueContentV2Optics.nestedResource) + } } From f6f4fcd3ce9b098c616acd3ae5c457377a465c35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Fri, 22 Nov 2024 12:11:21 +0100 Subject: [PATCH 19/31] more optics --- .../resourcemessages/ResourceMessagesV2.scala | 41 +++++++++++-------- .../ResourceMessagesV2Optics.scala | 34 +++++++++++++++ .../valuemessages/ValueMessagesV2Optics.scala | 8 ++++ 3 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2Optics.scala diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index 3cc1f0012b..a20b195c0c 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -26,6 +26,7 @@ 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.* @@ -585,24 +586,32 @@ object ReadResourceV2 { private def setCopyrightAndLicenceIfMissingResourceValues( copyright: Option[CopyrightAttribution], license: Option[License], - ): ReadResourceV2 => ReadResourceV2 = - setIfMissing(FileValueV2Optics.licenseOption)(license) - .andThen(setIfMissing(FileValueV2Optics.copyrightAttributionOption)(copyright)) + ): ReadResourceV2 => ReadResourceV2 = { rr => - private def ifMissingMapper[T](optional: Optional[ReadValueV2, Option[T]], newValue: Option[T]) = - (smartIri: SmartIri, seq: Seq[ReadValueV2]) => - ( - smartIri, - seq.map { readValue => - optional.getOption(readValue).flatten match - case Some(_) => readValue - case None => optional.replace(newValue)(readValue) - }, - ) + 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) - private def setIfMissing[T](opt: Optional[FileValueV2, Option[T]]): Option[T] => ReadResourceV2 => ReadResourceV2 = - value => - rr => rr.copy(values = rr.values.map(ifMissingMapper(ReadValueV2Optics.fileValueV2.andThen(opt), value)(_, _))) + 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), + )(rr) + } private def setCopyrightAndLicenceIfMissingOnLinkedResources( copyright: Option[CopyrightAttribution], diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2Optics.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2Optics.scala new file mode 100644 index 0000000000..575454b2ac --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2Optics.scala @@ -0,0 +1,34 @@ +/* + * 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.messages.v2.responder.resourcemessages + +import monocle.* +import monocle.macros.* + +import org.knora.webapi.messages.SmartIri +import org.knora.webapi.messages.v2.responder.valuemessages.ReadValueV2 + +object ResourceMessagesV2Optics { + + type ReadResourceV2Values = Map[SmartIri, Seq[ReadValueV2]] + + object ReadResourceV2Optics { + + val values: Lens[ReadResourceV2, ReadResourceV2Values] = GenLens[ReadResourceV2](_.values) + + private def inValues(predicate: Seq[ReadValueV2] => Boolean): Optional[ReadResourceV2Values, Seq[ReadValueV2]] = + Optional[ReadResourceV2Values, Seq[ReadValueV2]](_.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]] = + values.andThen(inValues(predicate)) + } +} diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala index 981c95eaf5..0f9ccf7a6f 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala @@ -69,5 +69,13 @@ object ValueMessagesV2Optics { val nestedResourceOfLinkValueContent: Optional[ReadValueV2, ReadResourceV2] = ReadValueV2Optics.linkValueContentV2.andThen(LinkValueContentV2Optics.nestedResource) + def elements(predicate: ReadValueV2 => Boolean): Optional[Seq[ReadValueV2], ReadValueV2] = + Optional[Seq[ReadValueV2], ReadValueV2](_.find(predicate))(newValue => + values => + values.map { + case v if predicate(v) => newValue + case other => other + }, + ) } } From 18501a7df80e4b4fc5d1daee2fea9f4c35228384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Fri, 22 Nov 2024 12:35:33 +0100 Subject: [PATCH 20/31] simplify --- .../v2/responder/resourcemessages/ResourceMessagesV2.scala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index a20b195c0c..f6c60806f8 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -586,8 +586,7 @@ object ReadResourceV2 { private def setCopyrightAndLicenceIfMissingResourceValues( copyright: Option[CopyrightAttribution], license: Option[License], - ): ReadResourceV2 => ReadResourceV2 = { rr => - + ): ReadResourceV2 => ReadResourceV2 = { def readValueWith(predicate: FileValueV2 => Boolean): ReadValueV2 => Boolean = rv => ReadValueV2Optics.fileValueV2.getOption(rv).exists(predicate) @@ -610,7 +609,7 @@ object ReadResourceV2 { setIfMissing(FileValueV2Optics.licenseOption, license).andThen( setIfMissing(FileValueV2Optics.copyrightAttributionOption, copyright), - )(rr) + ) } private def setCopyrightAndLicenceIfMissingOnLinkedResources( From b5ea046efb84b5e01946fe2074b0ddfe737f08db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Fri, 22 Nov 2024 13:31:55 +0100 Subject: [PATCH 21/31] add more tests and fix --- .../it/v2/CopyrightAndLicensesSpec.scala | 63 +++++++++++++++---- .../responders/v2/ResourcesResponderV2.scala | 4 +- .../domain/service/KnoraProjectService.scala | 2 + 3 files changed, 56 insertions(+), 13 deletions(-) diff --git a/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala b/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala index 5a31a0c130..0685beaeb9 100644 --- a/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala @@ -12,19 +12,20 @@ import org.apache.jena.vocabulary.RDF import zio.* import zio.http.Response import zio.test.* +import zio.test.TestAspect import java.net.URLEncoder 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 import org.knora.webapi.messages.OntologyConstants.KnoraApiV2Complex.StillImageFileValue import org.knora.webapi.models.filemodels.FileType import org.knora.webapi.models.filemodels.UploadFileRequest -import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequestsAndResponses.ProjectUpdateRequest 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.Shortcode @@ -44,10 +45,11 @@ object CopyrightAndLicensesSpec extends E2EZSpec { private val projectCopyrightAttribution = CopyrightAttribution.unsafeFrom("2024, On Project") private val projectLicense = License.unsafeFrom("Apache-2.0") - val e2eSpec: Spec[Scope & env, Any] = suite("Copyright Attribution and Licenses")( + private val givenProjectHasNoCopyrightAttributionAndLicenseSuite = suite( + "given the project does not have a license and does not have a copyright attribution ", + )( test( - "given the project does neither have a default license nor a default copyright attribution " + - "when creating a resource without copyright attribution and license " + + "when creating a resource without copyright attribution and license" + "the creation response should not contain the license and copyright attribution", ) { for { @@ -101,13 +103,16 @@ object CopyrightAndLicensesSpec extends E2EZSpec { actualLicense == aLicense.value, ) }, + ) @@ TestAspect.before(removeCopyrightAttributionAndLicenseFromProject()) + + private val givenProjectHasCopyrightAttributionAndLicenseSuite = suite( + "given the project has a license and has a copyright attribution", + )( test( "when creating a resource without copyright attribution and without license " + - "given the project has a default license and default copyright attribution " + "then the response when getting the created value should contain the default license and default copyright attribution", ) { for { - _ <- addCopyrightAttributionAndLicenseToProject(projectCopyrightAttribution, projectLicense) createResourceResponseModel <- createStillImageResource() valueResponseModel <- getValueFromApi(createResourceResponseModel) actualCopyright <- copyrightValue(valueResponseModel) @@ -117,13 +122,37 @@ object CopyrightAndLicensesSpec extends E2EZSpec { actualLicense == projectLicense.value, ) }, + test( + "when creating a resource without copyright attribution and without license " + + "then the create response contain the license and copyright attribution from resource", + ) { + for { + createResourceResponseModel <- createStillImageResource() + actualCopyright <- copyrightValue(createResourceResponseModel) + actualLicense <- licenseValue(createResourceResponseModel) + } yield assertTrue( + actualCopyright == projectCopyrightAttribution.value, + actualLicense == projectLicense.value, + ) + }, + test( + "when creating a resource with copyright attribution and license " + + "then the create response contain the license and copyright attribution from resource", + ) { + for { + createResourceResponseModel <- createStillImageResource(Some(aCopyrightAttribution), Some(aLicense)) + actualCopyright <- copyrightValue(createResourceResponseModel) + actualLicense <- licenseValue(createResourceResponseModel) + } yield assertTrue( + actualCopyright == aCopyrightAttribution.value, + actualLicense == aLicense.value, + ) + }, test( "when creating a resource with copyright attribution and without license " + - "given the project has a default license and default copyright attribution " + "then the response when getting the created value should contain the license and copyright attribution from resource", ) { for { - _ <- addCopyrightAttributionAndLicenseToProject(projectCopyrightAttribution, projectLicense) createResourceResponseModel <- createStillImageResource(Some(aCopyrightAttribution), Some(aLicense)) valueResponseModel <- getValueFromApi(createResourceResponseModel) actualCopyright <- copyrightValue(valueResponseModel) @@ -133,14 +162,26 @@ object CopyrightAndLicensesSpec extends E2EZSpec { actualLicense == aLicense.value, ) }, + ) @@ TestAspect.before(addCopyrightAttributionAndLicenseToProject()) + + val e2eSpec: Spec[Scope & env, Any] = suite("Copyright Attribution and Licenses")( + givenProjectHasNoCopyrightAttributionAndLicenseSuite, + givenProjectHasCopyrightAttributionAndLicenseSuite, ) - private def addCopyrightAttributionAndLicenseToProject(copyrightAttribution: CopyrightAttribution, license: License) = + private def removeCopyrightAttributionAndLicenseFromProject() = + setCopyrightAttributionAndLicenseToProject(None, None) + private def addCopyrightAttributionAndLicenseToProject() = + setCopyrightAttributionAndLicenseToProject(Some(projectCopyrightAttribution), Some(projectLicense)) + private def setCopyrightAttributionAndLicenseToProject( + copyrightAttribution: Option[CopyrightAttribution], + license: Option[License], + ) = for { projectService <- ZIO.service[KnoraProjectService] prj <- projectService.findByShortcode(Shortcode.unsafeFrom("0001")).someOrFail(new Exception("Project not found")) - change = ProjectUpdateRequest(copyrightAttribution = Some(copyrightAttribution), license = Some(license)) - updated <- projectService.updateProject(prj, change) + change = prj.copy(copyrightAttribution = copyrightAttribution, license = license) + updated <- projectService.save(change) } yield updated private def failResponse(msg: String)(response: Response) = diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index 22433e823b..20447d75d3 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -192,7 +192,7 @@ final case class ResourcesResponderV2( } def createResource(createResource: CreateResourceRequestV2): Task[ReadResourcesSequenceV2] = - createHandler(createResource) + createHandler(createResource).map(_.updateCopyRightAndLicenseDeep()) /** * If resource has already been modified, make sure that its lastModificationDate is given in the request body. @@ -722,7 +722,7 @@ final case class ResourcesResponderV2( } } - } yield responseWithDeletedResourcesReplaced + } yield responseWithDeletedResourcesReplaced.updateCopyRightAndLicenseDeep() } /** diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectService.scala index 3af6f33f00..b7b9859a53 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectService.scala @@ -105,6 +105,8 @@ final case class KnoraProjectService(knoraProjectRepo: KnoraProjectRepo, ontolog ) } yield updated + def save(project: KnoraProject): Task[KnoraProject] = knoraProjectRepo.save(project) + def getNamedGraphsForProject(project: KnoraProject): Task[List[InternalIri]] = { val projectGraph = ProjectService.projectDataNamedGraphV2(project) ontologyRepo From 255d12547353023313ccdd613cd44e6c2a32a43c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Fri, 22 Nov 2024 13:49:27 +0100 Subject: [PATCH 22/31] update when rendering the jsonld --- .../resourcemessages/ResourceMessagesV2.scala | 14 ++++++++------ .../responders/v2/ResourcesResponderV2.scala | 6 +++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index f6c60806f8..42cc8d4140 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -844,7 +844,7 @@ case class ReadResourcesSequenceV2( with KnoraReadV2[ReadResourcesSequenceV2] with UpdateResultInProject { self => - def updateCopyRightAndLicenseDeep(): ReadResourcesSequenceV2 = { + private def updateCopyRightAndLicenseDeep(): ReadResourcesSequenceV2 = { val newResources = self.resources.map { resource => ReadResourceV2.setCopyrightAndLicenceIfMissing( self.projectADM.copyrightAttribution, @@ -935,11 +935,13 @@ case class ReadResourcesSequenceV2( appConfig: AppConfig, schemaOptions: Set[Rendering] = Set.empty, ): JsonLDDocument = - toOntologySchema(targetSchema).generateJsonLD( - targetSchema = targetSchema, - appConfig = appConfig, - schemaOptions = schemaOptions, - ) + updateCopyRightAndLicenseDeep() + .toOntologySchema(targetSchema) + .generateJsonLD( + targetSchema = targetSchema, + appConfig = appConfig, + schemaOptions = schemaOptions, + ) /** * Checks that a [[ReadResourcesSequenceV2]] contains exactly one resource, and returns that resource. diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index 20447d75d3..80013676e2 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -192,7 +192,7 @@ final case class ResourcesResponderV2( } def createResource(createResource: CreateResourceRequestV2): Task[ReadResourcesSequenceV2] = - createHandler(createResource).map(_.updateCopyRightAndLicenseDeep()) + createHandler(createResource) /** * If resource has already been modified, make sure that its lastModificationDate is given in the request body. @@ -654,7 +654,7 @@ final case class ResourcesResponderV2( } } responseWithDeletedResourcesReplaced = apiResponse.copy(resources = deletedResourcesReplaced) - } yield responseWithDeletedResourcesReplaced.updateCopyRightAndLicenseDeep() + } yield responseWithDeletedResourcesReplaced } /** @@ -722,7 +722,7 @@ final case class ResourcesResponderV2( } } - } yield responseWithDeletedResourcesReplaced.updateCopyRightAndLicenseDeep() + } yield responseWithDeletedResourcesReplaced } /** From fc2e00f403c1353c434e0099d1921576b8708a9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Fri, 22 Nov 2024 19:09:37 +0100 Subject: [PATCH 23/31] copy values from project when creating values or resources --- .../it/v2/CopyrightAndLicensesSpec.scala | 1 - .../resourcemessages/ResourceMessagesV2.scala | 77 +------------------ .../ResourceMessagesV2Optics.scala | 45 +++++++++-- .../valuemessages/ValueMessagesV2Optics.scala | 5 +- .../responders/v2/ValuesResponderV2.scala | 47 +++++++++-- .../resources/CreateResourceV2Handler.scala | 33 +++++++- 6 files changed, 116 insertions(+), 92 deletions(-) diff --git a/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala b/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala index 0685beaeb9..d7e5cc9ed3 100644 --- a/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala @@ -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 diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index 42cc8d4140..e226264b3b 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -5,8 +5,6 @@ package org.knora.webapi.messages.v2.responder.resourcemessages -import monocle.Optional - import java.time.Instant import java.util.UUID @@ -26,13 +24,10 @@ 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.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 @@ -571,65 +566,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 - }, - ) } /** @@ -844,16 +780,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)), @@ -935,8 +861,7 @@ case class ReadResourcesSequenceV2( appConfig: AppConfig, schemaOptions: Set[Rendering] = Set.empty, ): JsonLDDocument = - updateCopyRightAndLicenseDeep() - .toOntologySchema(targetSchema) + toOntologySchema(targetSchema) .generateJsonLD( targetSchema = targetSchema, appConfig = appConfig, diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2Optics.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2Optics.scala index 575454b2ac..3ad3fb5a75 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2Optics.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2Optics.scala @@ -9,18 +9,20 @@ 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 CreateResourceV2Optics { + type CreateResourceV2Values = Map[SmartIri, Seq[CreateValueInNewResourceV2]] - object ReadResourceV2Optics { + val values: Lens[CreateResourceV2, CreateResourceV2Values] = GenLens[CreateResourceV2](_.values) - val values: Lens[ReadResourceV2, ReadResourceV2Values] = GenLens[ReadResourceV2](_.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) @@ -28,7 +30,34 @@ object ResourceMessagesV2Optics { }, ) - 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 + }, + ) + } } diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala index 0f9ccf7a6f..22237b7446 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala @@ -37,7 +37,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 { diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala index 06b66d73a0..6f04c7dad0 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala @@ -5,6 +5,7 @@ package org.knora.webapi.responders.v2 +import monocle.PLens import zio.* import java.time.Instant @@ -32,17 +33,25 @@ 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.KnoraProject.CopyrightAttribution +import org.knora.webapi.slice.admin.domain.model.KnoraProject.License 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 @@ -50,6 +59,8 @@ 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, @@ -58,6 +69,19 @@ final case class ValuesResponderV2( permissionsResponder: PermissionsResponder, )(implicit val stringFormatter: StringFormatter) { + private def setCopyrightAndLicenceIfMissing( + license: Option[License], + copyrightAttribution: Option[CopyrightAttribution], + ): FileValueContentV2 => FileValueContentV2 = + FileValueContentV2Optics.licenseOption + .filter(_.isEmpty) + .replace(license) + .andThen( + FileValueContentV2Optics.copyRightAttributionOption + .filter(_.isEmpty) + .replace(copyrightAttribution), + ) + /** * Creates a new value in an existing resource. * @@ -73,13 +97,23 @@ 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.license, project.copyrightAttribution)(fileValueContent) + case other => other // Get ontology information about the submitted property. propertyInfoRequestForSubmittedProperty = @@ -836,10 +870,13 @@ final case class ValuesResponderV2( ) // Convert the submitted value content to the internal schema. + project = resourceInfo.projectADM submittedInternalValueContent: ValueContentV2 = - updateValueContentV2.valueContent.toOntologySchema( - InternalSchema, - ) + (updateValueContentV2.valueContent match + case fv: FileValueContentV2 => + setCopyrightAndLicenceIfMissing(project.license, project.copyrightAttribution)(fv) + case other => other + ).toOntologySchema(InternalSchema) // Check that the object of the adjusted property (the value to be created, or the target of the link to be created) will have // the correct type for the adjusted property's knora-base:objectClassConstraint. diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/resources/CreateResourceV2Handler.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/resources/CreateResourceV2Handler.scala index 0a42465cab..ad1931c97e 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/resources/CreateResourceV2Handler.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/resources/CreateResourceV2Handler.scala @@ -6,9 +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 @@ -28,8 +30,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 @@ -85,6 +90,27 @@ final case class CreateResourceV2Handler( def apply(createResourceRequestV2: CreateResourceRequestV2): Task[ReadResourcesSequenceV2] = triplestoreUpdate(createResourceRequestV2) + private def replaceCopyrightAttributionAndLicenseIfMissing(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] = @@ -166,9 +192,14 @@ 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( + replaceCopyrightAttributionAndLicenseIfMissing(project)(createResourceRequestV2.createResource) + .toOntologySchema(InternalSchema), + ) // Check link targets and list nodes that should exist. _ <- checkStandoffLinkTargets( From b8c31640e60c3446f98781ac5e576b3c30bcdb36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Mon, 25 Nov 2024 11:33:21 +0100 Subject: [PATCH 24/31] simplify parameter list --- .../webapi/messages/StringFormatter.scala | 3 +- .../valuemessages/ValueMessagesV2.scala | 6 ++- .../responders/v2/ValuesResponderV2.scala | 39 ++++++------------- 3 files changed, 19 insertions(+), 29 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala b/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala index 45a29961ad..51713235f2 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala @@ -476,7 +476,8 @@ sealed trait SmartIri extends Ordered[SmartIri] with KnoraContentV2[SmartIri] { /** * Converts this IRI to ApiV2Complex schema. */ - def toComplexSchema: SmartIri = toOntologySchema(ApiV2Complex) + def toComplexSchema: SmartIri = toOntologySchema(ApiV2Complex) + def toInternalSchema: SmartIri = toOntologySchema(InternalSchema) /** * Constructs a short prefix label for the ontology that the IRI belongs to. diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala index 88f0574fc7..497472de6e 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala @@ -525,6 +525,8 @@ sealed trait UpdateValueV2 { * A custom value creation date. */ val valueCreationDate: Option[Instant] + + def valueType: SmartIri } /** @@ -551,7 +553,9 @@ case class UpdateValueContentV2( permissions: Option[String] = None, valueCreationDate: Option[Instant] = None, newValueVersionIri: Option[SmartIri] = None, -) extends UpdateValueV2 +) extends UpdateValueV2 { + override def valueType: SmartIri = valueContent.valueType +} /** * New permissions for a value. diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala index 6f04c7dad0..c5e0535563 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala @@ -613,20 +613,18 @@ final case class ValuesResponderV2( * Gets information about a resource, a submitted property, and a value of the property, and does * some checks to see if the submitted information is correct. * - * @param resourceIri the IRI of the resource. - * @param submittedExternalResourceClassIri the submitted external IRI of the resource class. - * @param submittedExternalPropertyIri the submitted external IRI of the property. - * @param valueIri the IRI of the value. - * @param submittedExternalValueType the submitted external IRI of the value type. + * @param updateValue the submitted value update to check * @return a [[ResourcePropertyValue]]. */ def getResourcePropertyValue( - resourceIri: IRI, - submittedExternalResourceClassIri: SmartIri, - submittedExternalPropertyIri: SmartIri, - valueIri: IRI, - submittedExternalValueType: SmartIri, - ): Task[ResourcePropertyValue] = + updateValue: UpdateValueV2, + ): Task[ResourcePropertyValue] = { + + val resourceIri = updateValue.resourceIri + val submittedExternalResourceClassIri = updateValue.resourceClassIri + val submittedExternalPropertyIri = updateValue.propertyIri + val valueIri = updateValue.valueIri + val submittedExternalValueType = updateValue.valueType for { submittedInternalPropertyIri <- ZIO.attempt(submittedExternalPropertyIri.toOntologySchema(InternalSchema)) submittedInternalValueType <- ZIO.attempt(submittedExternalValueType.toOntologySchema(InternalSchema)) @@ -732,6 +730,7 @@ final case class ValuesResponderV2( adjustedInternalPropertyInfo, currentValue, ) + } /** * Updates the permissions attached to a value. @@ -744,14 +743,7 @@ final case class ValuesResponderV2( ): Task[UpdateValueResponseV2] = for { // Do the initial checks, and get information about the resource, the property, and the value. - resourcePropertyValue <- - getResourcePropertyValue( - resourceIri = updateValuePermissionsV2.resourceIri, - submittedExternalResourceClassIri = updateValuePermissionsV2.resourceClassIri, - submittedExternalPropertyIri = updateValuePermissionsV2.propertyIri, - valueIri = updateValuePermissionsV2.valueIri, - submittedExternalValueType = updateValuePermissionsV2.valueType, - ) + resourcePropertyValue <- getResourcePropertyValue(updateValuePermissionsV2) resourceInfo: ReadResourceV2 = resourcePropertyValue.resource submittedInternalPropertyIri: SmartIri = resourcePropertyValue.submittedInternalPropertyIri @@ -821,14 +813,7 @@ final case class ValuesResponderV2( ): Task[UpdateValueResponseV2] = { for { // Do the initial checks, and get information about the resource, the property, and the value. - resourcePropertyValue <- - getResourcePropertyValue( - resourceIri = updateValueContentV2.resourceIri, - submittedExternalResourceClassIri = updateValueContentV2.resourceClassIri, - submittedExternalPropertyIri = updateValueContentV2.propertyIri, - valueIri = updateValueContentV2.valueIri, - submittedExternalValueType = updateValueContentV2.valueContent.valueType, - ) + resourcePropertyValue <- getResourcePropertyValue(updateValueContentV2) resourceInfo: ReadResourceV2 = resourcePropertyValue.resource submittedInternalPropertyIri: SmartIri = resourcePropertyValue.submittedInternalPropertyIri From 0edfc58fec9c10262024d842ea7412144d786db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Mon, 25 Nov 2024 14:19:17 +0100 Subject: [PATCH 25/31] refactor: move inner methods out --- .../responders/v2/ValuesResponderV2.scala | 676 ++++++++---------- 1 file changed, 318 insertions(+), 358 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala index c5e0535563..538f50c407 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala @@ -341,7 +341,6 @@ final case class ValuesResponderV2( projectADM = resourceInfo.projectADM, ) } - for { // Don't allow anonymous users to create values. _ <- ZIO.when(requestingUser.isAnonymousUser)( @@ -589,396 +588,357 @@ final case class ValuesResponderV2( updateValue: UpdateValueV2, requestingUser: User, apiRequestId: UUID, - ): Task[UpdateValueResponseV2] = { - - /** - * Information about a resource, a submitted property, and a value of the property. - * - * @param resource the contents of the resource. - * @param submittedInternalPropertyIri the internal IRI of the submitted property. - * @param adjustedInternalPropertyInfo the internal definition of the submitted property, adjusted - * as follows: an adjusted version of the submitted property: - * if it's a link value property, substitute the - * corresponding link property. - * @param value the requested value. - */ - case class ResourcePropertyValue( - resource: ReadResourceV2, - submittedInternalPropertyIri: SmartIri, - adjustedInternalPropertyInfo: ReadPropertyInfoV2, - value: ReadValueV2, - ) - - /** - * Gets information about a resource, a submitted property, and a value of the property, and does - * some checks to see if the submitted information is correct. - * - * @param updateValue the submitted value update to check - * @return a [[ResourcePropertyValue]]. - */ - def getResourcePropertyValue( - updateValue: UpdateValueV2, - ): Task[ResourcePropertyValue] = { - - val resourceIri = updateValue.resourceIri - val submittedExternalResourceClassIri = updateValue.resourceClassIri - val submittedExternalPropertyIri = updateValue.propertyIri - val valueIri = updateValue.valueIri - val submittedExternalValueType = updateValue.valueType - for { - submittedInternalPropertyIri <- ZIO.attempt(submittedExternalPropertyIri.toOntologySchema(InternalSchema)) - submittedInternalValueType <- ZIO.attempt(submittedExternalValueType.toOntologySchema(InternalSchema)) - - // Get ontology information about the submitted property. - propertyInfoRequestForSubmittedProperty = - PropertiesGetRequestV2( - propertyIris = Set(submittedInternalPropertyIri), - allLanguages = false, - requestingUser = requestingUser, - ) - - propertyInfoResponseForSubmittedProperty <- - messageRelay.ask[ReadOntologyV2](propertyInfoRequestForSubmittedProperty) - - propertyInfoForSubmittedProperty: ReadPropertyInfoV2 = - propertyInfoResponseForSubmittedProperty.properties( - submittedInternalPropertyIri, - ) - - // Don't accept link properties. - _ <- - ZIO.when(propertyInfoForSubmittedProperty.isLinkProp)( - ZIO.fail( - BadRequestException( - s"Invalid property <${propertyInfoForSubmittedProperty.entityInfoContent.propertyIri.toOntologySchema(ApiV2Complex)}>. Use a link value property to submit a link.", - ), - ), - ) - - // Don't accept knora-api:hasStandoffLinkToValue. - _ <- ZIO.when( - submittedExternalPropertyIri.toString == OntologyConstants.KnoraApiV2Complex.HasStandoffLinkToValue, - )(ZIO.fail(BadRequestException(s"Values of <$submittedExternalPropertyIri> cannot be updated directly"))) - - // Make an adjusted version of the submitted property: if it's a link value property, substitute the - // corresponding link property, whose objects we will need to query. Get ontology information about the - // adjusted property. - adjustedInternalPropertyInfo <- - getAdjustedInternalPropertyInfo( - submittedPropertyIri = submittedExternalPropertyIri, - maybeSubmittedValueType = Some(submittedExternalValueType), - propertyInfoForSubmittedProperty = propertyInfoForSubmittedProperty, - requestingUser = requestingUser, - ) + ): Task[UpdateValueResponseV2] = + ZIO + .fail(ForbiddenException("Anonymous users aren't allowed to update values")) + .when(requestingUser.isAnonymousUser) *> + IriLocker.runWithIriLock( + apiRequestId, + updateValue.resourceIri, + updateValue match { + case updateContent: UpdateValueContentV2 => updateValueContent(updateContent, requestingUser) + case updatePermissions: UpdateValuePermissionsV2 => updateValuePermissions(updatePermissions, requestingUser) + }, + ) - // Get the resource's metadata and relevant property objects, using the adjusted property. Do this as the system user, - // so we can see objects that the user doesn't have permission to see. - resourceInfo <- - getResourceWithPropertyValues( - resourceIri = resourceIri, - propertyInfo = adjustedInternalPropertyInfo, - requestingUser = KnoraSystemInstances.Users.SystemUser, - ) + /** + * Updates the permissions attached to a value. + * + * @param updateValue the update request. + * @return an [[UpdateValueResponseV2]]. + */ + private def updateValuePermissions(updateValue: UpdateValuePermissionsV2, requestingUser: User) = + for { + // Do the initial checks, and get information about the resource, the property, and the value. + resourcePropertyValue <- checkValueAndRetrieveResourceProperties(updateValue, requestingUser) + + resourceInfo: ReadResourceV2 = resourcePropertyValue.resource + currentValue: ReadValueV2 = resourcePropertyValue.value + + // Validate and reformat the submitted permissions. + newValuePermissionLiteral <- permissionUtilADM.validatePermissions(updateValue.permissions) + + // Check that the user has Permission.ObjectAccess.ChangeRights on the value, and that the new permissions are + // different from the current ones. + currentPermissionsParsed <- ZIO.attempt(PermissionUtilADM.parsePermissions(currentValue.permissions)) + newPermissionsParsed <- + ZIO.attempt( + PermissionUtilADM.parsePermissions( + updateValue.permissions, + (permissionLiteral: String) => throw AssertionException(s"Invalid permission literal: $permissionLiteral"), + ), + ) - _ <- - ZIO.when(resourceInfo.resourceClassIri != submittedExternalResourceClassIri.toOntologySchema(InternalSchema))( - ZIO.fail( - BadRequestException( - s"The rdf:type of resource <$resourceIri> is not <$submittedExternalResourceClassIri>", - ), - ), - ) + _ <- ZIO.when(newPermissionsParsed == currentPermissionsParsed)( + ZIO.fail(BadRequestException(s"The submitted permissions are the same as the current ones")), + ) - // Check that the resource has the value that the user wants to update, as an object of the submitted property. - currentValue <- - ZIO - .fromOption(for { - values <- resourceInfo.values.get(submittedInternalPropertyIri) - curVal <- values.find(_.valueIri == valueIri) - } yield curVal) - .orElseFail( - NotFoundException( - s"Resource <$resourceIri> does not have value <$valueIri> as an object of property <$submittedExternalPropertyIri>", - ), - ) - isSameType = currentValue.valueContent.valueType == submittedInternalValueType - isStillImageTypes = - Set( - submittedInternalValueType.toInternalIri.value, - currentValue.valueContent.valueType.toInternalIri.value, - ).subsetOf(Set(StillImageExternalFileValue, StillImageFileValue)) - _ <- - ZIO.unless(isSameType || isStillImageTypes)( - ZIO.fail( - BadRequestException( - s"Value <$valueIri> has type <${currentValue.valueContent.valueType.toOntologySchema(ApiV2Complex)}>, but the submitted type was <$submittedExternalValueType>", - ), - ), - ) + _ <- resourceUtilV2.checkValuePermission( + resourceInfo = resourceInfo, + valueInfo = currentValue, + permissionNeeded = Permission.ObjectAccess.ChangeRights, + requestingUser = requestingUser, + ) - // If a custom value creation date was submitted, make sure it's later than the date of the current version. - _ <- ZIO.when(updateValue.valueCreationDate.exists(!_.isAfter(currentValue.valueCreationDate)))( - ZIO.fail( - BadRequestException( - "A custom value creation date must be later than the date of the current version", - ), - ), - ) - } yield ResourcePropertyValue( - resourceInfo, - submittedInternalPropertyIri, - adjustedInternalPropertyInfo, - currentValue, - ) - } + // Do the update. + dataNamedGraph: IRI = ProjectService.projectDataNamedGraphV2(resourceInfo.projectADM).value + newValueIri <- + iriService.checkOrCreateEntityIri( + updateValue.newValueVersionIri, + stringFormatter.makeRandomValueIri(resourceInfo.resourceIri), + ) - /** - * Updates the permissions attached to a value. - * - * @param updateValuePermissionsV2 the update request. - * @return an [[UpdateValueResponseV2]]. - */ - def makeTaskFutureToUpdateValuePermissions( - updateValuePermissionsV2: UpdateValuePermissionsV2, - ): Task[UpdateValueResponseV2] = - for { - // Do the initial checks, and get information about the resource, the property, and the value. - resourcePropertyValue <- getResourcePropertyValue(updateValuePermissionsV2) - - resourceInfo: ReadResourceV2 = resourcePropertyValue.resource - submittedInternalPropertyIri: SmartIri = resourcePropertyValue.submittedInternalPropertyIri - currentValue: ReadValueV2 = resourcePropertyValue.value - - // Validate and reformat the submitted permissions. - newValuePermissionLiteral <- permissionUtilADM.validatePermissions(updateValuePermissionsV2.permissions) - - // Check that the user has Permission.ObjectAccess.ChangeRights on the value, and that the new permissions are - // different from the current ones. - currentPermissionsParsed <- ZIO.attempt(PermissionUtilADM.parsePermissions(currentValue.permissions)) - newPermissionsParsed <- - ZIO.attempt( - PermissionUtilADM.parsePermissions( - updateValuePermissionsV2.permissions, - (permissionLiteral: String) => throw AssertionException(s"Invalid permission literal: $permissionLiteral"), - ), - ) + currentTime = updateValue.valueCreationDate.getOrElse(Instant.now) - _ <- ZIO.when(newPermissionsParsed == currentPermissionsParsed)( - ZIO.fail(BadRequestException(s"The submitted permissions are the same as the current ones")), - ) + sparqlUpdate = sparql.v2.txt.changeValuePermissions( + dataNamedGraph = dataNamedGraph, + resourceIri = resourceInfo.resourceIri, + propertyIri = updateValue.propertyIri.toInternalSchema, + currentValueIri = currentValue.valueIri, + valueTypeIri = currentValue.valueContent.valueType, + newValueIri = newValueIri, + newPermissions = newValuePermissionLiteral, + currentTime = currentTime, + ) + _ <- triplestoreService.query(Update(sparqlUpdate)) + } yield UpdateValueResponseV2( + newValueIri, + currentValue.valueContent.valueType, + currentValue.valueHasUUID, + resourceInfo.projectADM, + ) - _ <- resourceUtilV2.checkValuePermission( - resourceInfo = resourceInfo, - valueInfo = currentValue, - permissionNeeded = Permission.ObjectAccess.ChangeRights, - requestingUser = requestingUser, - ) + /** + * Updates the contents of a value. + * + * @param updateValue the update request. + * @return an [[UpdateValueResponseV2]]. + */ + private def updateValueContent( + updateValue: UpdateValueContentV2, + requestingUser: User, + ): Task[UpdateValueResponseV2] = { + for { + resourcePropertyValue <- checkValueAndRetrieveResourceProperties(updateValue, requestingUser) - // Do the update. - dataNamedGraph: IRI = ProjectService.projectDataNamedGraphV2(resourceInfo.projectADM).value - newValueIri <- - iriService.checkOrCreateEntityIri( - updateValuePermissionsV2.newValueVersionIri, - stringFormatter.makeRandomValueIri(resourceInfo.resourceIri), - ) + resourceInfo: ReadResourceV2 = resourcePropertyValue.resource + adjustedInternalPropertyInfo: ReadPropertyInfoV2 = resourcePropertyValue.adjustedInternalPropertyInfo + currentValue: ReadValueV2 = resourcePropertyValue.value - currentTime = updateValuePermissionsV2.valueCreationDate.getOrElse(Instant.now) + // Did the user submit permissions for the new value? + newValueVersionPermissionLiteral <- + updateValue.permissions match { + case Some(permissions) => + // Yes. Validate them. + permissionUtilADM.validatePermissions(permissions) - sparqlUpdate = sparql.v2.txt.changeValuePermissions( - dataNamedGraph = dataNamedGraph, - resourceIri = resourceInfo.resourceIri, - propertyIri = submittedInternalPropertyIri, - currentValueIri = currentValue.valueIri, - valueTypeIri = currentValue.valueContent.valueType, - newValueIri = newValueIri, - newPermissions = newValuePermissionLiteral, - currentTime = currentTime, - ) - _ <- triplestoreService.query(Update(sparqlUpdate)) - } yield UpdateValueResponseV2( - newValueIri, - currentValue.valueContent.valueType, - currentValue.valueHasUUID, - resourceInfo.projectADM, - ) + case None => + // No. Use the permissions on the current version of the value. + ZIO.succeed(currentValue.permissions) + } - /** - * Updates the contents of a value. - * - * @param updateValueContentV2 the update request. - * @return an [[UpdateValueResponseV2]]. - */ - def makeTaskFutureToUpdateValueContent( - updateValueContentV2: UpdateValueContentV2, - ): Task[UpdateValueResponseV2] = { - for { - // Do the initial checks, and get information about the resource, the property, and the value. - resourcePropertyValue <- getResourcePropertyValue(updateValueContentV2) + // Check that the user has permission to do the update. If they want to change the permissions + // on the value, they need Permission.ObjectAccess.ChangeRights, otherwise they need Permission.ObjectAccess.Modify. + currentPermissionsParsed <- ZIO.attempt(PermissionUtilADM.parsePermissions(currentValue.permissions)) + newPermissionsParsed <- + ZIO.attempt( + PermissionUtilADM.parsePermissions( + newValueVersionPermissionLiteral, + (permissionLiteral: String) => throw AssertionException(s"Invalid permission literal: $permissionLiteral"), + ), + ) - resourceInfo: ReadResourceV2 = resourcePropertyValue.resource - submittedInternalPropertyIri: SmartIri = resourcePropertyValue.submittedInternalPropertyIri - adjustedInternalPropertyInfo: ReadPropertyInfoV2 = resourcePropertyValue.adjustedInternalPropertyInfo - currentValue: ReadValueV2 = resourcePropertyValue.value + permissionNeeded = + if (newPermissionsParsed != currentPermissionsParsed) { Permission.ObjectAccess.ChangeRights } + else { Permission.ObjectAccess.Modify } - // Did the user submit permissions for the new value? - newValueVersionPermissionLiteral <- - updateValueContentV2.permissions match { - case Some(permissions) => - // Yes. Validate them. - permissionUtilADM.validatePermissions(permissions) + _ <- resourceUtilV2.checkValuePermission( + resourceInfo = resourceInfo, + valueInfo = currentValue, + permissionNeeded = permissionNeeded, + requestingUser = requestingUser, + ) - case None => - // No. Use the permissions on the current version of the value. - ZIO.succeed(currentValue.permissions) - } + // Convert the submitted value content to the internal schema. + project = resourceInfo.projectADM + submittedInternalValueContent: ValueContentV2 = + (updateValue.valueContent match + case fv: FileValueContentV2 => + setCopyrightAndLicenceIfMissing(project.license, project.copyrightAttribution)(fv) + case other => other + ).toOntologySchema(InternalSchema) + + // Check that the object of the adjusted property (the value to be created, or the target of the link to be created) will have + // the correct type for the adjusted property's knora-base:objectClassConstraint. + _ <- checkPropertyObjectClassConstraint( + propertyInfo = adjustedInternalPropertyInfo, + valueContent = submittedInternalValueContent, + requestingUser = requestingUser, + ) - // Check that the user has permission to do the update. If they want to change the permissions - // on the value, they need Permission.ObjectAccess.ChangeRights, otherwise they need Permission.ObjectAccess.Modify. - currentPermissionsParsed <- ZIO.attempt(PermissionUtilADM.parsePermissions(currentValue.permissions)) - newPermissionsParsed <- - ZIO.attempt( - PermissionUtilADM.parsePermissions( - newValueVersionPermissionLiteral, - (permissionLiteral: String) => throw AssertionException(s"Invalid permission literal: $permissionLiteral"), - ), - ) + _ <- ifIsListValueThenCheckItPointsToListNodeWhichIsNotARootNode(submittedInternalValueContent) - permissionNeeded = - if (newPermissionsParsed != currentPermissionsParsed) { Permission.ObjectAccess.ChangeRights } - else { Permission.ObjectAccess.Modify } + // Check that the updated value would not duplicate the current value version. + unescapedSubmittedInternalValueContent = submittedInternalValueContent.unescape - _ <- resourceUtilV2.checkValuePermission( - resourceInfo = resourceInfo, - valueInfo = currentValue, - permissionNeeded = permissionNeeded, - requestingUser = requestingUser, - ) + _ <- ZIO.when(unescapedSubmittedInternalValueContent.wouldDuplicateCurrentVersion(currentValue.valueContent))( + ZIO.fail(DuplicateValueException("The submitted value is the same as the current version")), + ) - // Convert the submitted value content to the internal schema. - project = resourceInfo.projectADM - submittedInternalValueContent: ValueContentV2 = - (updateValueContentV2.valueContent match - case fv: FileValueContentV2 => - setCopyrightAndLicenceIfMissing(project.license, project.copyrightAttribution)(fv) - case other => other - ).toOntologySchema(InternalSchema) + // Check that the updated value would not duplicate another existing value of the resource. + currentValuesForProp: Seq[ReadValueV2] = + resourceInfo.values + .getOrElse(updateValue.propertyIri.toInternalSchema, Seq.empty[ReadValueV2]) + .filter(_.valueIri != updateValue.valueIri) + + _ <- ZIO.when( + currentValuesForProp.exists(currentVal => + unescapedSubmittedInternalValueContent.wouldDuplicateOtherValue(currentVal.valueContent), + ), + )(ZIO.fail(DuplicateValueException())) + + _ <- submittedInternalValueContent match { + case textValueContent: TextValueContentV2 => + // This is a text value. Check that the resources pointed to by any standoff link tags exist + // and that the user has permission to see them. + checkResourceIris( + textValueContent.standoffLinkTagTargetResourceIris, + requestingUser, + ) - // Check that the object of the adjusted property (the value to be created, or the target of the link to be created) will have - // the correct type for the adjusted property's knora-base:objectClassConstraint. - _ <- checkPropertyObjectClassConstraint( - propertyInfo = adjustedInternalPropertyInfo, - valueContent = submittedInternalValueContent, - requestingUser = requestingUser, - ) + case _: LinkValueContentV2 => + // We're updating a link. This means deleting an existing link and creating a new one, so + // check that the user has permission to modify the resource. + resourceUtilV2.checkResourcePermission( + resourceInfo = resourceInfo, + permissionNeeded = Permission.ObjectAccess.Modify, + requestingUser = requestingUser, + ) - _ <- ifIsListValueThenCheckItPointsToListNodeWhichIsNotARootNode(submittedInternalValueContent) + case _ => ZIO.unit + } - // Check that the updated value would not duplicate the current value version. - unescapedSubmittedInternalValueContent = submittedInternalValueContent.unescape + dataNamedGraph: IRI = ProjectService.projectDataNamedGraphV2(resourceInfo.projectADM).value - _ <- ZIO.when(unescapedSubmittedInternalValueContent.wouldDuplicateCurrentVersion(currentValue.valueContent))( - ZIO.fail(DuplicateValueException("The submitted value is the same as the current version")), - ) + // Create the new value version. + newValueVersion <- + (currentValue, submittedInternalValueContent) match { + case ( + currentLinkValue: ReadLinkValueV2, + newLinkValue: LinkValueContentV2, + ) => + updateLinkValueV2AfterChecks( + dataNamedGraph = dataNamedGraph, + resourceInfo = resourceInfo, + linkPropertyIri = adjustedInternalPropertyInfo.entityInfoContent.propertyIri, + currentLinkValue = currentLinkValue, + newLinkValue = newLinkValue, + valueCreator = requestingUser.id, + valuePermissions = newValueVersionPermissionLiteral, + valueCreationDate = updateValue.valueCreationDate, + newValueVersionIri = updateValue.newValueVersionIri, + requestingUser = requestingUser, + ) - // Check that the updated value would not duplicate another existing value of the resource. - currentValuesForProp: Seq[ReadValueV2] = - resourceInfo.values - .getOrElse(submittedInternalPropertyIri, Seq.empty[ReadValueV2]) - .filter(_.valueIri != updateValueContentV2.valueIri) + case _ => + updateOrdinaryValueV2AfterChecks( + dataNamedGraph = dataNamedGraph, + resourceInfo = resourceInfo, + propertyIri = adjustedInternalPropertyInfo.entityInfoContent.propertyIri, + currentValue = currentValue, + newValueVersion = submittedInternalValueContent, + valueCreator = requestingUser.id, + valuePermissions = newValueVersionPermissionLiteral, + valueCreationDate = updateValue.valueCreationDate, + newValueVersionIri = updateValue.newValueVersionIri, + requestingUser = requestingUser, + ) + } + } yield UpdateValueResponseV2( + valueIri = newValueVersion.newValueIri, + valueType = newValueVersion.valueContent.valueType, + valueUUID = newValueVersion.newValueUUID, + projectADM = resourceInfo.projectADM, + ) + } - _ <- ZIO.when( - currentValuesForProp.exists(currentVal => - unescapedSubmittedInternalValueContent.wouldDuplicateOtherValue(currentVal.valueContent), - ), - )(ZIO.fail(DuplicateValueException())) + /** + * Information about a resource, a submitted property, and a value of the property. + * + * @param resource the contents of the resource. + * @param adjustedInternalPropertyInfo the internal definition of the submitted property, adjusted + * as follows: an adjusted version of the submitted property: + * if it's a link value property, substitute the + * corresponding link property. + * @param value the requested value. + */ + private case class ResourcePropertyValue( + resource: ReadResourceV2, + adjustedInternalPropertyInfo: ReadPropertyInfoV2, + value: ReadValueV2, + ) - _ <- submittedInternalValueContent match { - case textValueContent: TextValueContentV2 => - // This is a text value. Check that the resources pointed to by any standoff link tags exist - // and that the user has permission to see them. - checkResourceIris( - textValueContent.standoffLinkTagTargetResourceIris, - requestingUser, - ) + /** + * Gets information about a resource, a submitted property, and a value of the property, and does + * some checks to see if the submitted information is correct. + * + * @param updateValue the submitted value update to check + * @return a [[ResourcePropertyValue]]. + */ + private def checkValueAndRetrieveResourceProperties(updateValue: UpdateValueV2, requestingUser: User) = + for { + submittedInternalPropertyIri <- ZIO.attempt(updateValue.propertyIri.toInternalSchema) + submittedInternalValueType <- ZIO.attempt(updateValue.valueType.toInternalSchema) + + // Get ontology information about the submitted property. + propertyInfoRequestForSubmittedProperty = + PropertiesGetRequestV2( + propertyIris = Set(submittedInternalPropertyIri), + allLanguages = false, + requestingUser = requestingUser, + ) - case _: LinkValueContentV2 => - // We're updating a link. This means deleting an existing link and creating a new one, so - // check that the user has permission to modify the resource. - resourceUtilV2.checkResourcePermission( - resourceInfo = resourceInfo, - permissionNeeded = Permission.ObjectAccess.Modify, - requestingUser = requestingUser, - ) + propertyInfoResponseForSubmittedProperty <- + messageRelay.ask[ReadOntologyV2](propertyInfoRequestForSubmittedProperty) - case _ => ZIO.unit - } + propertyInfoForSubmittedProperty: ReadPropertyInfoV2 = + propertyInfoResponseForSubmittedProperty.properties(submittedInternalPropertyIri) - dataNamedGraph: IRI = ProjectService.projectDataNamedGraphV2(resourceInfo.projectADM).value + _ <- { + val msg = + s"Invalid property <${propertyInfoForSubmittedProperty.entityInfoContent.propertyIri.toComplexSchema}>." + + s" Use a link value property to submit a link." + ZIO.fail(BadRequestException(msg)).when(propertyInfoForSubmittedProperty.isLinkProp) + } - // Create the new value version. - newValueVersion <- - (currentValue, submittedInternalValueContent) match { - case ( - currentLinkValue: ReadLinkValueV2, - newLinkValue: LinkValueContentV2, - ) => - updateLinkValueV2AfterChecks( - dataNamedGraph = dataNamedGraph, - resourceInfo = resourceInfo, - linkPropertyIri = adjustedInternalPropertyInfo.entityInfoContent.propertyIri, - currentLinkValue = currentLinkValue, - newLinkValue = newLinkValue, - valueCreator = requestingUser.id, - valuePermissions = newValueVersionPermissionLiteral, - valueCreationDate = updateValueContentV2.valueCreationDate, - newValueVersionIri = updateValueContentV2.newValueVersionIri, - requestingUser = requestingUser, - ) + // Don't accept knora-api:hasStandoffLinkToValue. + _ <- ZIO.when( + updateValue.propertyIri.toString == OntologyConstants.KnoraApiV2Complex.HasStandoffLinkToValue, + )(ZIO.fail(BadRequestException(s"Values of <${updateValue.propertyIri}> cannot be updated directly"))) + + // Make an adjusted version of the submitted property: if it's a link value property, substitute the + // corresponding link property, whose objects we will need to query. Get ontology information about the + // adjusted property. + adjustedInternalPropertyInfo <- getAdjustedInternalPropertyInfo( + updateValue.propertyIri, + Some(updateValue.valueType), + propertyInfoForSubmittedProperty, + requestingUser, + ) + + // Get the resource's metadata and relevant property objects, using the adjusted property. Do this as the system user, + // so we can see objects that the user doesn't have permission to see. + resourceInfo <- + getResourceWithPropertyValues( + resourceIri = updateValue.resourceIri, + propertyInfo = adjustedInternalPropertyInfo, + requestingUser = KnoraSystemInstances.Users.SystemUser, + ) - case _ => - updateOrdinaryValueV2AfterChecks( - dataNamedGraph = dataNamedGraph, - resourceInfo = resourceInfo, - propertyIri = adjustedInternalPropertyInfo.entityInfoContent.propertyIri, - currentValue = currentValue, - newValueVersion = submittedInternalValueContent, - valueCreator = requestingUser.id, - valuePermissions = newValueVersionPermissionLiteral, - valueCreationDate = updateValueContentV2.valueCreationDate, - newValueVersionIri = updateValueContentV2.newValueVersionIri, - requestingUser = requestingUser, - ) - } - } yield UpdateValueResponseV2( - valueIri = newValueVersion.newValueIri, - valueType = newValueVersion.valueContent.valueType, - valueUUID = newValueVersion.newValueUUID, - projectADM = resourceInfo.projectADM, - ) - } + _ <- { + val msg = s"The rdf:type of resource <${updateValue.resourceIri}> is not <${updateValue.resourceClassIri}>" + ZIO + .fail(BadRequestException(msg)) + .when(resourceInfo.resourceClassIri != updateValue.resourceClassIri.toInternalSchema) + } - if (requestingUser.isAnonymousUser) { - ZIO.fail(ForbiddenException("Anonymous users aren't allowed to update values")) - } else { - updateValue match { - case updateValueContentV2: UpdateValueContentV2 => - // This is a request to update the content of a value. - IriLocker.runWithIriLock( - apiRequestId, - updateValueContentV2.resourceIri, - makeTaskFutureToUpdateValueContent(updateValueContentV2), + // Check that the resource has the value that the user wants to update, as an object of the submitted property. + currentValue <- + ZIO + .fromOption(for { + values <- resourceInfo.values.get(submittedInternalPropertyIri) + curVal <- values.find(_.valueIri == updateValue.valueIri) + } yield curVal) + .orElseFail( + NotFoundException( + s"Resource <${updateValue.resourceIri}> does not have value <${updateValue.valueIri}> as an object of property <${updateValue.propertyIri}>", + ), ) + isSameType = currentValue.valueContent.valueType == submittedInternalValueType + isStillImageTypes = + Set( + submittedInternalValueType.toInternalIri.value, + currentValue.valueContent.valueType.toInternalIri.value, + ).subsetOf(Set(StillImageExternalFileValue, StillImageFileValue)) - case updateValuePermissionsV2: UpdateValuePermissionsV2 => - // This is a request to update the permissions attached to a value. - IriLocker.runWithIriLock( - apiRequestId, - updateValuePermissionsV2.resourceIri, - makeTaskFutureToUpdateValuePermissions(updateValuePermissionsV2), - ) - } - } - } + _ <- + ZIO.unless(isSameType || isStillImageTypes)( + ZIO.fail( + BadRequestException( + s"Value <${updateValue.valueIri}> has type <${currentValue.valueContent.valueType.toOntologySchema(ApiV2Complex)}>, but the submitted type was <${updateValue.valueType}>", + ), + ), + ) + + // If a custom value creation date was submitted, make sure it's later than the date of the current version. + _ <- ZIO.when(updateValue.valueCreationDate.exists(!_.isAfter(currentValue.valueCreationDate)))( + ZIO.fail( + BadRequestException( + "A custom value creation date must be later than the date of the current version", + ), + ), + ) + } yield ResourcePropertyValue(resourceInfo, adjustedInternalPropertyInfo, currentValue) /** * Changes an ordinary value (i.e. not a link), assuming that pre-update checks have already been done. From 5dfff511d8b9cbe39631f55ad2884bbbaff0c0b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Mon, 25 Nov 2024 13:37:44 +0100 Subject: [PATCH 26/31] rm unused code --- .../valuemessages/ValueMessagesV2Optics.scala | 39 ------------------- 1 file changed, 39 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala index 22237b7446..30d95edc97 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala @@ -42,43 +42,4 @@ object ValueMessagesV2Optics { val licenseOption: Lens[FileValueContentV2, Option[License]] = fileValueV2.andThen(FileValueV2Optics.licenseOption) } - - object LinkValueContentV2Optics { - - val nestedResource: Optional[LinkValueContentV2, ReadResourceV2] = - Optional[LinkValueContentV2, ReadResourceV2](_.nestedResource)(rr => lv => lv.copy(nestedResource = Some(rr))) - - } - - object ReadValueV2Optics { - - val fileValueContentV2: Optional[ReadValueV2, FileValueContentV2] = - Optional[ReadValueV2, FileValueContentV2](_.valueContent.asOpt[FileValueContentV2])(fc => { - case rv: ReadLinkValueV2 => rv - case rv: ReadTextValueV2 => rv - case ov: ReadOtherValueV2 => ov.copy(valueContent = fc) - }) - - val fileValueV2: Optional[ReadValueV2, FileValueV2] = - ReadValueV2Optics.fileValueContentV2.andThen(FileValueContentV2Optics.fileValueV2) - - val linkValueContentV2: Optional[ReadValueV2, LinkValueContentV2] = - Optional[ReadValueV2, LinkValueContentV2](_.valueContent.asOpt[LinkValueContentV2])(lv => { - case rv: ReadLinkValueV2 => rv.copy(valueContent = lv) - case rv: ReadOtherValueV2 => rv.copy(valueContent = lv) - case rv: ReadTextValueV2 => rv - }) - - val nestedResourceOfLinkValueContent: Optional[ReadValueV2, ReadResourceV2] = - ReadValueV2Optics.linkValueContentV2.andThen(LinkValueContentV2Optics.nestedResource) - - def elements(predicate: ReadValueV2 => Boolean): Optional[Seq[ReadValueV2], ReadValueV2] = - Optional[Seq[ReadValueV2], ReadValueV2](_.find(predicate))(newValue => - values => - values.map { - case v if predicate(v) => newValue - case other => other - }, - ) - } } From e1bb20e85f648257269405cf6d4e0ce432db6fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Mon, 25 Nov 2024 13:58:07 +0100 Subject: [PATCH 27/31] replace copyright attribution and license when updating a value, when not given --- .../valuemessages/ValueMessagesV2.scala | 14 ++++- .../valuemessages/ValueMessagesV2Optics.scala | 2 - .../responders/v2/ValuesResponderV2.scala | 51 +++++++------------ 3 files changed, 32 insertions(+), 35 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala index 497472de6e..a6e5b9823f 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala @@ -14,7 +14,6 @@ import java.time.Instant import java.util.UUID import scala.language.implicitConversions import scala.util.Try - import dsp.errors.AssertionException import dsp.errors.BadRequestException import dsp.errors.NotFoundException @@ -39,6 +38,7 @@ import org.knora.webapi.messages.v2.responder.* import org.knora.webapi.messages.v2.responder.resourcemessages.ReadResourceV2 import org.knora.webapi.messages.v2.responder.standoffmessages.* import org.knora.webapi.messages.v2.responder.valuemessages.ValueContentV2.FileInfo +import org.knora.webapi.messages.v2.responder.valuemessages.ValueMessagesV2Optics.FileValueContentV2Optics import org.knora.webapi.routing.RouteUtilZ import org.knora.webapi.slice.admin.api.model.MaintenanceRequests.AssetId import org.knora.webapi.slice.admin.api.model.Project @@ -673,6 +673,18 @@ sealed trait ValueContentV2 extends KnoraContentV2[ValueContentV2] with WithAsIs * Generates instances of value content classes (subclasses of [[ValueContentV2]]) from JSON-LD input. */ object ValueContentV2 { + def replaceCopyrightAndLicenceIfMissing( + license: Option[License], + copyrightAttribution: Option[CopyrightAttribution], + vc: ValueContentV2, + ): ValueContentV2 = vc match { + case fvc: FileValueContentV2 => + FileValueContentV2Optics.licenseOption + .filter(_.isEmpty) + .replace(license) + .andThen(FileValueContentV2Optics.copyRightAttributionOption.filter(_.isEmpty).replace(copyrightAttribution))(fvc) + case other => other + } final case class FileInfo(filename: IRI, metadata: FileMetadataSipiResponse) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala index 30d95edc97..0919d18093 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2Optics.scala @@ -6,10 +6,8 @@ package org.knora.webapi.messages.v2.responder.valuemessages import monocle.* -import monocle.Optional import monocle.macros.* -import org.knora.webapi.messages.v2.responder.resourcemessages.ReadResourceV2 import org.knora.webapi.slice.admin.domain.model.KnoraProject.CopyrightAttribution import org.knora.webapi.slice.admin.domain.model.KnoraProject.License diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala index 538f50c407..3820c234c8 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala @@ -5,12 +5,10 @@ package org.knora.webapi.responders.v2 -import monocle.PLens import zio.* import java.time.Instant import java.util.UUID - import dsp.errors.* import dsp.valueobjects.UuidUtil import org.knora.webapi.* @@ -34,13 +32,10 @@ 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.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.KnoraProject.CopyrightAttribution -import org.knora.webapi.slice.admin.domain.model.KnoraProject.License 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 @@ -69,19 +64,6 @@ final case class ValuesResponderV2( permissionsResponder: PermissionsResponder, )(implicit val stringFormatter: StringFormatter) { - private def setCopyrightAndLicenceIfMissing( - license: Option[License], - copyrightAttribution: Option[CopyrightAttribution], - ): FileValueContentV2 => FileValueContentV2 = - FileValueContentV2Optics.licenseOption - .filter(_.isEmpty) - .replace(license) - .andThen( - FileValueContentV2Optics.copyRightAttributionOption - .filter(_.isEmpty) - .replace(copyrightAttribution), - ) - /** * Creates a new value in an existing resource. * @@ -109,11 +91,13 @@ final case class ValuesResponderV2( submittedInternalPropertyIri <- ZIO.attempt(valueToCreate.propertyIri.toOntologySchema(InternalSchema)) - submittedInternalValueContent: ValueContentV2 = - valueToCreate.valueContent.toOntologySchema(InternalSchema) match - case fileValueContent: FileValueContentV2 => - setCopyrightAndLicenceIfMissing(project.license, project.copyrightAttribution)(fileValueContent) - case other => other + submittedInternalValueContent = ValueContentV2 + .replaceCopyrightAndLicenceIfMissing( + project.license, + project.copyrightAttribution, + valueToCreate.valueContent, + ) + .toOntologySchema(InternalSchema) // Get ontology information about the submitted property. propertyInfoRequestForSubmittedProperty = @@ -721,12 +705,10 @@ final case class ValuesResponderV2( // Convert the submitted value content to the internal schema. project = resourceInfo.projectADM - submittedInternalValueContent: ValueContentV2 = - (updateValue.valueContent match - case fv: FileValueContentV2 => - setCopyrightAndLicenceIfMissing(project.license, project.copyrightAttribution)(fv) - case other => other - ).toOntologySchema(InternalSchema) + submittedInternalValueContent = + ValueContentV2 + .replaceCopyrightAndLicenceIfMissing(project.license, project.copyrightAttribution, updateValue.valueContent) + .toOntologySchema(InternalSchema) // Check that the object of the adjusted property (the value to be created, or the target of the link to be created) will have // the correct type for the adjusted property's knora-base:objectClassConstraint. @@ -1036,6 +1018,11 @@ final case class ValuesResponderV2( currentTime: Instant = valueCreationDate.getOrElse(Instant.now) // Generate a SPARQL update. + newValue: ValueContentV2 = ValueContentV2.replaceCopyrightAndLicenceIfMissing( + resourceInfo.projectADM.license, + resourceInfo.projectADM.copyrightAttribution, + newValueVersion, + ) sparqlUpdate = sparql.v2.txt.addValueVersion( dataNamedGraph = dataNamedGraph, resourceIri = resourceInfo.resourceIri, @@ -1043,10 +1030,10 @@ final case class ValuesResponderV2( currentValueIri = currentValue.valueIri, newValueIri = newValueIri, valueTypeIri = currentValue.valueContent.valueType, - value = newValueVersion, + value = newValue, valueCreator = valueCreator, valuePermissions = valuePermissions, - maybeComment = newValueVersion.comment, + maybeComment = newValue.comment, linkUpdates = standoffLinkUpdates, currentTime = currentTime, requestingUser = requestingUser.id, From b514744867ce90143250e75d1e0c82034cf136c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Mon, 25 Nov 2024 14:03:22 +0100 Subject: [PATCH 28/31] fmt --- .../v2/responder/valuemessages/ValueMessagesV2.scala | 5 ++++- .../org/knora/webapi/responders/v2/ValuesResponderV2.scala | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala index a6e5b9823f..4e0f3b35d3 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala @@ -14,6 +14,7 @@ import java.time.Instant import java.util.UUID import scala.language.implicitConversions import scala.util.Try + import dsp.errors.AssertionException import dsp.errors.BadRequestException import dsp.errors.NotFoundException @@ -682,7 +683,9 @@ object ValueContentV2 { FileValueContentV2Optics.licenseOption .filter(_.isEmpty) .replace(license) - .andThen(FileValueContentV2Optics.copyRightAttributionOption.filter(_.isEmpty).replace(copyrightAttribution))(fvc) + .andThen(FileValueContentV2Optics.copyRightAttributionOption.filter(_.isEmpty).replace(copyrightAttribution))( + fvc, + ) case other => other } diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala index 3820c234c8..3f23039c3f 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala @@ -9,6 +9,7 @@ import zio.* import java.time.Instant import java.util.UUID + import dsp.errors.* import dsp.valueobjects.UuidUtil import org.knora.webapi.* From 9b138f9a6735873eec011e8dd628b7647352d5b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Mon, 25 Nov 2024 14:22:07 +0100 Subject: [PATCH 29/31] fmt --- .../scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala index 3f23039c3f..1c58c74b42 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala @@ -32,7 +32,6 @@ 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 From e5caf4fd04bdc577bd7ad38b9b83c3cb6a9c9747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 26 Nov 2024 12:45:29 +0100 Subject: [PATCH 30/31] add test --- .../scala/org/knora/webapi/E2EZSpec.scala | 13 +++++ .../it/v2/CopyrightAndLicensesSpec.scala | 49 +++++++++++++++++++ .../responders/v2/ValuesResponderV2.scala | 2 +- 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/integration/src/test/scala/org/knora/webapi/E2EZSpec.scala b/integration/src/test/scala/org/knora/webapi/E2EZSpec.scala index dd28dcc529..90a56acbcf 100644 --- a/integration/src/test/scala/org/knora/webapi/E2EZSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/E2EZSpec.scala @@ -56,6 +56,19 @@ abstract class E2EZSpec extends ZIOSpecDefault with TestStartupUtils { response <- client.url(urlFull).addHeaders(Headers(bearer)).get("").orDie } yield response + def sendPutRequestAsRoot(url: String, body: Body): URIO[env, Response] = + for { + token <- getRootToken.mapError(Exception(_)).orDie + response <- sendPutRequest(url, body, Some(token)) + } yield response + + def sendPutRequest(url: String, body: Body, token: Option[String] = None): URIO[env, Response] = + for { + client <- ZIO.service[Client] + bearer = token.map(Header.Authorization.Bearer(_)).toList + response <- client.url(url"http://localhost:3333").addHeaders(Headers(bearer)).put(url)(body).orDie + } yield response + def sendGetRequestStringOrFail(url: String, token: Option[String] = None): ZIO[env, String, String] = for { response <- sendGetRequest(url, token) diff --git a/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala b/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala index d7e5cc9ed3..328d2f8c37 100644 --- a/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/it/v2/CopyrightAndLicensesSpec.scala @@ -10,6 +10,7 @@ import org.apache.jena.rdf.model.Property import org.apache.jena.rdf.model.Resource import org.apache.jena.vocabulary.RDF import zio.* +import zio.http.Body import zio.http.Response import zio.test.* import zio.test.TestAspect @@ -102,6 +103,26 @@ object CopyrightAndLicensesSpec extends E2EZSpec { actualLicense == aLicense.value, ) }, + test( + "when creating a resource without copyright attribution and license " + + "and when providing the project with copyright attribution and license " + + "and then updating the value" + + "the response when getting the updated value should contain the license and copyright attribution of the project", + ) { + for { + createResourceResponseModel <- createStillImageResource(None, None) + _ <- addCopyrightAttributionAndLicenseToProject() + resourceId <- resourceId(createResourceResponseModel) + valueId <- valueId(createResourceResponseModel) + _ <- updateValue(resourceId, valueId) + valueGetResponse <- getValueFromApi(createResourceResponseModel) + actualCopyright <- copyrightValue(valueGetResponse) + actualLicense <- licenseValue(valueGetResponse) + } yield assertTrue( + actualCopyright == projectCopyrightAttribution.value, + actualLicense == projectLicense.value, + ) + }, ) @@ TestAspect.before(removeCopyrightAttributionAndLicenseFromProject()) private val givenProjectHasCopyrightAttributionAndLicenseSuite = suite( @@ -207,6 +228,34 @@ object CopyrightAndLicensesSpec extends E2EZSpec { } yield createResourceResponseModel } + private def updateValue(resourceIri: String, valueId: ValueIri) = { + val jsonLd = + s""" + |{ + | "@id": "${resourceIri}", + | "@type": "anything:ThingPicture", + | "knora-api:hasStillImageFileValue": { + | "@id" : "${valueId.smartIri.toComplexSchema.toIri}", + | "@type": "knora-api:StillImageFileValue", + | "knora-api:fileValueHasFilename": "test.jpg" + | }, + | "@context": { + | "knora-api": "http://api.knora.org/ontology/knora-api/v2#", + | "anything": "http://0.0.0.0:3333/ontology/0001/anything/v2#" + | } + |} + |""".stripMargin + for { + _ <- Console.printLine(jsonLd) + _ <- ModelOps.fromJsonLd(jsonLd).mapError(Exception(_)) + responseBody <- + sendPutRequestAsRoot("/v2/values", Body.fromString(jsonLd)) + .filterOrElseWith(_.status.isSuccess)(failResponse(s"Value update failed $valueId resource $resourceIri.")) + .flatMap(_.body.asString) + model <- ModelOps.fromJsonLd(responseBody).mapError(Exception(_)) + } yield model + } + private def getResourceFromApi(resourceId: String) = for { responseBody <- sendGetRequest(s"/v2/resources/${URLEncoder.encode(resourceId, "UTF-8")}") .filterOrElseWith(_.status.isSuccess)(failResponse(s"Failed to get resource $resourceId.")) diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala index 1c58c74b42..1deecb50cf 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala @@ -1045,7 +1045,7 @@ final case class ValuesResponderV2( } yield UnverifiedValueV2( newValueIri = newValueIri, newValueUUID = currentValue.valueHasUUID, - valueContent = newValueVersion.unescape, + valueContent = newValue.unescape, permissions = valuePermissions, creationDate = currentTime, ) From 29d56973db0bd4431299f10b07678eb24c442b1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 26 Nov 2024 22:46:23 +0100 Subject: [PATCH 31/31] rm unused imports --- .../org/knora/webapi/responders/v2/ResourcesResponderV2.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index 80013676e2..4462869caa 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -42,8 +42,6 @@ import org.knora.webapi.responders.IriService import org.knora.webapi.responders.Responder import org.knora.webapi.responders.admin.PermissionsResponder import org.knora.webapi.responders.v2.resources.CreateResourceV2Handler -import org.knora.webapi.slice.admin.api.model.Project -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.Permission import org.knora.webapi.slice.admin.domain.model.User