Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add copyright attribution and license to FileValues and expose on v2 api (DEV-4351) #3431

Merged
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
Loading