Skip to content

Commit

Permalink
feat: Add license and copyright attribution fallback (DEV-4352) (#3433)
Browse files Browse the repository at this point in the history
  • Loading branch information
seakayone authored Nov 27, 2024
1 parent b74a33c commit 0a726e9
Show file tree
Hide file tree
Showing 15 changed files with 741 additions and 423 deletions.
2 changes: 1 addition & 1 deletion .sbtopts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
-J-Xmx2G
-J-Xmx4G
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
services:
app:
image: daschswiss/dsp-app:v11.21.0
image: daschswiss/dsp-app:v11.22.1
ports:
- "4200:4200"
networks:
Expand Down
13 changes: 13 additions & 0 deletions integration/src/test/scala/org/knora/webapi/E2EZSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ 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

import java.net.URLEncoder
import scala.jdk.CollectionConverters.IteratorHasAsScala
Expand All @@ -25,6 +28,8 @@ 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.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
Expand All @@ -34,88 +39,235 @@ 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")

val e2eSpec: Spec[Scope & env, Any] = suite("Copyright Attribution and Licenses")(
private val projectCopyrightAttribution = CopyrightAttribution.unsafeFrom("2024, On Project")
private val projectLicense = License.unsafeFrom("Apache-2.0")

private val givenProjectHasNoCopyrightAttributionAndLicenseSuite = suite(
"given the project does not have a license and does not have a copyright attribution ",
)(
test(
"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 <- createImageWithCopyrightAndLicense
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(
"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
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(
"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
createResourceResponseModel <- createStillImageResource(Some(aCopyrightAttribution), Some(aLicense))
valueResponseModel <- getValueFromApi(createResourceResponseModel)
actualCopyright <- copyrightValue(valueResponseModel)
actualLicense <- licenseValue(valueResponseModel)
} yield assertTrue(
actualCopyright == aCopyrightAttribution.value,
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)
valueResponseModel <- getValueFromApi(valueId, resourceId)
_ <- 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(
"given the project has a license and has a copyright attribution",
)(
test(
"when creating a resource without copyright attribution and without license " +
"then the response when getting the created value should contain the default license and default copyright attribution",
) {
for {
createResourceResponseModel <- createStillImageResource()
valueResponseModel <- getValueFromApi(createResourceResponseModel)
actualCopyright <- copyrightValue(valueResponseModel)
actualLicense <- licenseValue(valueResponseModel)
} yield assertTrue(
actualCopyright == projectCopyrightAttribution.value,
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 " +
"then the response when getting the created value should contain the license and copyright attribution from resource",
) {
for {
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,
)
},
) @@ TestAspect.before(addCopyrightAttributionAndLicenseToProject())

val e2eSpec: Spec[Scope & env, Any] = suite("Copyright Attribution and Licenses")(
givenProjectHasNoCopyrightAttributionAndLicenseSuite,
givenProjectHasCopyrightAttributionAndLicenseSuite,
)

private def createImageWithCopyrightAndLicense: ZIO[env, Throwable, Model] = {
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 = prj.copy(copyrightAttribution = copyrightAttribution, license = license)
updated <- projectService.save(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,
): ZIO[env, Throwable, Model] = {
val jsonLd = UploadFileRequest
.make(
FileType.StillImageFile(),
"internalFilename.jpg",
copyrightAttribution = Some(copyrightAttribution),
license = Some(license),
)
.toJsonLd(
className = Some("ThingPicture"),
ontologyName = "anything",
copyrightAttribution = copyrightAttribution,
license = license,
)

.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
}

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")}")
.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

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")
private def getValueFromApi(createResourceResponse: Model) = for {
valueId <- valueId(createResourceResponse)
resourceId <- resourceId(createResourceResponse)
responseBody <- sendGetRequest(s"/v2/values/${URLEncoder.encode(resourceId, "UTF-8")}/${valueId.valueId}")
.filterOrElseWith(_.status.isSuccess)(failResponse(s"Failed to get value $resourceId."))
.flatMap(_.body.asString)
model <- ModelOps.fromJsonLd(responseBody).mapError(Exception(_))
} yield model
Expand Down Expand Up @@ -147,8 +299,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(_))
}
11 changes: 10 additions & 1 deletion project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading

0 comments on commit 0a726e9

Please sign in to comment.