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 license and copyright attribution fallback (DEV-4352) #3433

Merged
merged 31 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
1bfa708
add copyright fallback to resources
seakayone Nov 20, 2024
7068231
fmt
seakayone Nov 20, 2024
0ac89b9
add monocle
seakayone Nov 20, 2024
5e82b63
wip
seakayone Nov 20, 2024
498de35
wip
seakayone Nov 20, 2024
f43ba0f
add test
seakayone Nov 20, 2024
76363a7
rm learning test
seakayone Nov 20, 2024
7fe7755
add fallback to nested resources of link resources
seakayone Nov 21, 2024
127f8de
add more heap to sbt builds
seakayone Nov 21, 2024
7eeacf3
refactor: reorder
seakayone Nov 21, 2024
308e460
update dsp-app in docker-compose.yml
seakayone Nov 21, 2024
84f96ec
add more tests
seakayone Nov 21, 2024
8ebf792
fmt
seakayone Nov 21, 2024
49a948f
log api response when failing
seakayone Nov 21, 2024
3fb0a7b
extract optics
seakayone Nov 22, 2024
7a10514
simplify optic
seakayone Nov 22, 2024
a8d9917
align naming
seakayone Nov 22, 2024
ec050c6
align naming
seakayone Nov 22, 2024
f6f4fcd
more optics
seakayone Nov 22, 2024
18501a7
simplify
seakayone Nov 22, 2024
b5ea046
add more tests and fix
seakayone Nov 22, 2024
255d125
update when rendering the jsonld
seakayone Nov 22, 2024
fc2e00f
copy values from project when creating values or resources
seakayone Nov 22, 2024
b8c3164
simplify parameter list
seakayone Nov 25, 2024
0edfc58
refactor: move inner methods out
seakayone Nov 25, 2024
5dfff51
rm unused code
seakayone Nov 25, 2024
e1bb20e
replace copyright attribution and license when updating a value, when…
seakayone Nov 25, 2024
b514744
fmt
seakayone Nov 25, 2024
9b138f9
fmt
seakayone Nov 25, 2024
e5caf4f
add test
seakayone Nov 26, 2024
29d5697
rm unused imports
seakayone Nov 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading