Skip to content

Commit

Permalink
feat: Add copyright attribution and license to FileValues and expose …
Browse files Browse the repository at this point in the history
…on v2 api (DEV-4351) (#3431)
  • Loading branch information
seakayone authored Nov 20, 2024
1 parent 6ecc9ee commit f9dcd98
Show file tree
Hide file tree
Showing 18 changed files with 454 additions and 105 deletions.
3 changes: 3 additions & 0 deletions integration/src/test/scala/org/knora/webapi/E2EZSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ abstract class E2EZSpec extends ZIOSpecDefault with TestStartupUtils {
result <- ZIO.fromEither(response.fromJson[B])
} yield result

def sendPostRequestAsRoot(url: String, data: String): ZIO[env, String, Response] =
getRootToken.flatMap(token => sendPostRequest(url, data, Some(token)))

def sendPostRequest(url: String, data: String, token: Option[String] = None): ZIO[env, String, Response] =
for {
client <- ZIO.service[Client]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,7 @@ class ValuesRouteV2E2ESpec extends E2ESpec {
private val customValueIri: IRI = s"http://rdfh.ch/0001/a-thing/values/$customValueUUID"

"The values v2 endpoint" should {

"get the latest versions of values, given their UUIDs" in {
// The UUIDs of values in TestDing.
val testDingValues: Map[String, String] = Map(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
* 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.it.v2

import org.apache.jena.rdf.model.Model
import org.apache.jena.rdf.model.Property
import org.apache.jena.rdf.model.Resource
import org.apache.jena.vocabulary.RDF
import zio.*
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
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.domain.model.KnoraProject.CopyrightAttribution
import org.knora.webapi.slice.admin.domain.model.KnoraProject.License
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
import org.knora.webapi.slice.common.jena.ModelOps.*
import org.knora.webapi.slice.common.jena.ResourceOps.*
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")

val e2eSpec: Spec[Scope & env, Any] = suite("Copyright Attribution and Licenses")(
test(
"when creating a resource with copyright attribution and license " +
"the creation response should contain the license and copyright attribution",
) {
for {
createResourceResponseModel <- createImageWithCopyrightAndLicense
actualCreatedCopyright <- copyrightValue(createResourceResponseModel)
actualCreatedLicense <- licenseValue(createResourceResponseModel)
} yield assertTrue(
actualCreatedCopyright == copyrightAttribution.value,
actualCreatedLicense == license.value,
)
},
test(
"when creating a resource with copyright attribution and license " +
"the response when getting the created resource should contain the license and copyright attribution",
) {
for {
createResourceResponseModel <- createImageWithCopyrightAndLicense
resourceId <- resourceId(createResourceResponseModel)
getResponseModel <- getResourceFromApi(resourceId)
actualCopyright <- copyrightValue(getResponseModel)
actualLicense <- licenseValue(getResponseModel)
} yield assertTrue(
actualCopyright == copyrightAttribution.value,
actualLicense == license.value,
)
},
test(
"when creating a resource with copyright attribution and license " +
"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)
actualCopyright <- copyrightValue(valueResponseModel)
actualLicense <- licenseValue(valueResponseModel)
} yield assertTrue(
actualCopyright == copyrightAttribution.value,
actualLicense == license.value,
)
},
)

private def createImageWithCopyrightAndLicense: ZIO[env, Throwable, Model] = {
val jsonLd = UploadFileRequest
.make(
FileType.StillImageFile(),
"internalFilename",
copyrightAttribution = Some(copyrightAttribution),
license = Some(license),
)
.toJsonLd(
className = Some("ThingPicture"),
ontologyName = "anything",
)

for {
responseBody <- sendPostRequestAsRoot("/v2/resources", jsonLd)
.filterOrFail(_.status.isSuccess)(s"Failed to create resource")
.mapError(Exception(_))
.flatMap(_.body.asString)
createResourceResponseModel <- ModelOps.fromJsonLd(responseBody).mapError(Exception(_))
} yield createResourceResponseModel
}

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")
.flatMap(_.body.asString)
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}")
.filterOrFail(_.status.isSuccess)(s"Failed to get resource $valueId")
.flatMap(_.body.asString)
model <- ModelOps.fromJsonLd(responseBody).mapError(Exception(_))
} yield model

private def resourceId(model: Model): Task[String] =
ZIO
.fromEither(
for {
root <- model.singleRootResource
id <- root.uri.toRight("No URI found for root resource")
} yield id,
)
.mapError(Exception(_))

private def valueId(model: Model): ZIO[IriConverter, Throwable, ValueIri] = {
val subs = model
.listSubjectsWithProperty(RDF.`type`)
.asScala
.filter(_.getProperty(RDF.`type`).getObject.asResource().hasURI(StillImageFileValue))
.toList
subs match
case s :: Nil =>
ZIO
.fromEither(s.uri.toRight("No URI found for value"))
.mapError(Exception(_))
.flatMap(str => ZIO.serviceWithZIO[IriConverter](_.asSmartIri(str)))
.flatMap(iri => ZIO.fromEither(ValueIri.from(iri)).mapError(Exception(_)))
case Nil => ZIO.fail(Exception("No value found"))
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(_))
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import org.knora.webapi.messages.OntologyConstants
import org.knora.webapi.messages.SmartIri
import org.knora.webapi.messages.StringFormatter
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.resources.IiifImageRequestUrl

object FileModelUtil {
Expand Down Expand Up @@ -84,6 +86,8 @@ object FileModelUtil {
originalFilename: Option[String],
originalMimeType: Option[String],
comment: Option[String],
copyrightAttribution: Option[CopyrightAttribution],
license: Option[License],
): FileValueContentV2 =
fileType match {
case FileType.DocumentFile(pageCount, dimX, dimY) =>
Expand All @@ -94,6 +98,8 @@ object FileModelUtil {
internalMimeType = internalMimeType.getOrElse("application/pdf"),
originalFilename = originalFilename,
originalMimeType = Some(originalMimeType.getOrElse("application/pdf")),
copyrightAttribution,
license,
),
pageCount = pageCount,
dimX = dimX,
Expand All @@ -108,6 +114,8 @@ object FileModelUtil {
internalMimeType = internalMimeType.getOrElse("image/jp2"),
originalFilename = originalFilename,
originalMimeType = originalMimeType,
copyrightAttribution,
license,
),
dimX = dimX,
dimY = dimY,
Expand All @@ -121,6 +129,8 @@ object FileModelUtil {
internalMimeType = internalMimeType.getOrElse("image/jp2"),
originalFilename = originalFilename,
originalMimeType = originalMimeType,
copyrightAttribution,
license,
),
externalUrl = externalUrl,
comment = comment,
Expand All @@ -133,6 +143,8 @@ object FileModelUtil {
internalMimeType = internalMimeType.get,
originalFilename = originalFilename,
originalMimeType = internalMimeType,
copyrightAttribution,
license,
),
)
case FileType.TextFile =>
Expand All @@ -143,6 +155,8 @@ object FileModelUtil {
internalMimeType = internalMimeType.get,
originalFilename = originalFilename,
originalMimeType = internalMimeType,
copyrightAttribution,
license,
),
)
case FileType.AudioFile =>
Expand All @@ -153,6 +167,8 @@ object FileModelUtil {
internalMimeType = internalMimeType.get,
originalFilename = originalFilename,
originalMimeType = internalMimeType,
copyrightAttribution,
license,
),
)
case FileType.ArchiveFile =>
Expand All @@ -163,6 +179,8 @@ object FileModelUtil {
internalMimeType = internalMimeType.getOrElse("application/zip"),
originalFilename = originalFilename,
originalMimeType = internalMimeType,
copyrightAttribution,
license,
),
comment = comment,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,24 @@ import org.knora.webapi.messages.v2.responder.resourcemessages.CreateResourceV2
import org.knora.webapi.messages.v2.responder.resourcemessages.CreateValueInNewResourceV2
import org.knora.webapi.sharedtestdata.SharedTestDataADM
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

sealed abstract case class UploadFileRequest private (
fileType: FileType,
internalFilename: String,
label: String,
resourceIRI: Option[String] = None,
copyrightAttribution: Option[CopyrightAttribution] = None,
license: Option[License] = None,
) {

/**
* Create a JSON-LD serialization of the request. This can be used for e2e and integration tests.
*
* @param className the class name of the resource. Optional.
* @param shortcode the project's shortcode. Optional.
* @param ontologyName the name of the ontology to be prefixed to the class name. Defaults to `"knora-api"`
* @param uuid the uuid of the project to which the resource should be added. Defaults to `"0001"`
* @param className the class name of the resource. Optional.
* @param ontologyIRI IRI of the ontology, to which the prefix should resolve. Optional.
* @return JSON-LD serialization of the request.
*/
Expand All @@ -55,6 +59,8 @@ sealed abstract case class UploadFileRequest private (
| "$fileValuePropertyName" : {
| "@type" : "$fileValueType",
| "knora-api:fileValueHasFilename" : "$internalFilename"
| ${copyrightAttribution.map(ca => s""","knora-api:hasCopyrightAttribution" : "${ca.value}"""").getOrElse("")}
| ${license.map(l => s""","knora-api:hasLicense" : "${l.value}"""").getOrElse("")}
| },
| "knora-api:attachedToProject" : {
| "@id" : "http://rdfh.ch/projects/$shortcode"
Expand Down Expand Up @@ -104,6 +110,8 @@ sealed abstract case class UploadFileRequest private (
resourceClassIRI: Option[SmartIri] = None,
valuePropertyIRI: Option[SmartIri] = None,
project: Option[Project] = None,
copyrightAttribution: Option[CopyrightAttribution] = None,
license: Option[License] = None,
): CreateResourceV2 = {
implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance

Expand All @@ -122,6 +130,8 @@ sealed abstract case class UploadFileRequest private (
originalFilename = originalFilename,
originalMimeType = originalMimeType,
comment = comment,
copyrightAttribution,
license,
)

val values = List(
Expand Down Expand Up @@ -172,13 +182,10 @@ object UploadFileRequest {
internalFilename: String,
label: String = "test label",
resourceIRI: Option[String] = None,
copyrightAttribution: Option[CopyrightAttribution] = None,
license: Option[License] = None,
): UploadFileRequest =
new UploadFileRequest(
fileType = fileType,
internalFilename = internalFilename,
label = label,
resourceIRI = resourceIRI,
) {}
new UploadFileRequest(fileType, internalFilename, label, resourceIRI, copyrightAttribution, license) {}
}

sealed abstract case class ChangeFileRequest private (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,8 @@ class FileModelsSpec extends CoreSpec {
internalMimeType = "application/pdf",
originalFilename = None,
originalMimeType = Some("application/pdf"),
None,
None,
),
pageCount = Some(1),
dimX = Some(100),
Expand Down Expand Up @@ -388,6 +390,8 @@ class FileModelsSpec extends CoreSpec {
internalMimeType = internalMimetype.get,
originalFilename = originalFilename,
originalMimeType = originalMimeType,
None,
None,
),
pageCount = pageCount,
dimX = dimX,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ class ValuesResponderV2Spec extends CoreSpec with ImplicitSender {
}

"The values responder" should {

"create an integer value" in {
// Add the value.

Expand Down Expand Up @@ -4320,6 +4321,8 @@ class ValuesResponderV2Spec extends CoreSpec with ImplicitSender {
Some("test.tiff"),
Some(mimeTypeTIFF),
None,
None,
None,
),
),
anythingUser1,
Expand Down Expand Up @@ -4369,6 +4372,8 @@ class ValuesResponderV2Spec extends CoreSpec with ImplicitSender {
originalFilename,
originalMimeType,
None,
None,
None,
),
),
anythingUser1,
Expand Down Expand Up @@ -4418,6 +4423,8 @@ class ValuesResponderV2Spec extends CoreSpec with ImplicitSender {
internalMimeType = mimeTypeJP2,
originalFilename = Some("test.tiff"),
originalMimeType = Some(mimeTypeTIFF),
None,
None,
),
dimX = 512,
dimY = 256,
Expand Down Expand Up @@ -4453,6 +4460,8 @@ class ValuesResponderV2Spec extends CoreSpec with ImplicitSender {
internalMimeType = mimeTypeJP2,
originalFilename = Some("test.tiff"),
originalMimeType = Some(mimeTypeTIFF),
None,
None,
),
dimX = 512,
dimY = 256,
Expand Down
Loading

0 comments on commit f9dcd98

Please sign in to comment.