diff --git a/integration/src/test/scala/org/knora/webapi/ProjectEraseIT.scala b/integration/src/test/scala/org/knora/webapi/ProjectEraseIT.scala index aaee0bc1fd..1ec53592e3 100644 --- a/integration/src/test/scala/org/knora/webapi/ProjectEraseIT.scala +++ b/integration/src/test/scala/org/knora/webapi/ProjectEraseIT.scala @@ -85,6 +85,7 @@ object ProjectEraseIT extends E2EZSpec { KnoraProject.SelfJoin.CanJoin, None, None, + None, ), ), ).orDie 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 328d2f8c37..afe1254416 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 @@ -22,12 +22,14 @@ 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.HasLicenseText +import org.knora.webapi.messages.OntologyConstants.KnoraApiV2Complex.HasLicenseUri 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.admin.domain.model.KnoraProject.LicenseText +import org.knora.webapi.slice.admin.domain.model.KnoraProject.LicenseUri 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 @@ -40,10 +42,12 @@ import org.knora.webapi.slice.resourceinfo.domain.IriConverter object CopyrightAndLicensesSpec extends E2EZSpec { private val aCopyrightAttribution = CopyrightAttribution.unsafeFrom("2020, On FileValue") - private val aLicense = License.unsafeFrom("CC BY-SA 4.0") + private val aLicenseText = LicenseText.unsafeFrom("CC BY-SA 4.0") + private val aLicenseUri = LicenseUri.unsafeFrom("https://creativecommons.org/licenses/by-sa/4.0/") private val projectCopyrightAttribution = CopyrightAttribution.unsafeFrom("2024, On Project") - private val projectLicense = License.unsafeFrom("Apache-2.0") + private val projectLicenseText = LicenseText.unsafeFrom("Apache-2.0") + private val projectLicenseUri = LicenseUri.unsafeFrom("https://www.apache.org/licenses/LICENSE-2.0") private val givenProjectHasNoCopyrightAttributionAndLicenseSuite = suite( "given the project does not have a license and does not have a copyright attribution ", @@ -55,10 +59,12 @@ object CopyrightAndLicensesSpec extends E2EZSpec { for { createResourceResponseModel <- createStillImageResource() actualCreatedCopyright <- copyrightValueOption(createResourceResponseModel) - actualCreatedLicense <- licenseValueOption(createResourceResponseModel) + actualCreatedLicenseText <- licenseTextValueOption(createResourceResponseModel) + actualCreatedLicenseUri <- licenseUriValueOption(createResourceResponseModel) } yield assertTrue( actualCreatedCopyright.isEmpty, - actualCreatedLicense.isEmpty, + actualCreatedLicenseText.isEmpty, + actualCreatedLicenseUri.isEmpty, ) }, test( @@ -66,12 +72,15 @@ object CopyrightAndLicensesSpec extends E2EZSpec { "the creation response should contain the license and copyright attribution", ) { for { - createResourceResponseModel <- createStillImageResource(Some(aCopyrightAttribution), Some(aLicense)) - actualCreatedCopyright <- copyrightValue(createResourceResponseModel) - actualCreatedLicense <- licenseValue(createResourceResponseModel) + createResourceResponseModel <- + createStillImageResource(Some(aCopyrightAttribution), Some(aLicenseText), Some(aLicenseUri)) + actualCreatedCopyright <- copyrightValue(createResourceResponseModel) + actualCreatedLicenseText <- licenseTextValue(createResourceResponseModel) + actualCreatedLicenseUri <- licenseUriValue(createResourceResponseModel) } yield assertTrue( actualCreatedCopyright == aCopyrightAttribution.value, - actualCreatedLicense == aLicense.value, + actualCreatedLicenseText == aLicenseText.value, + actualCreatedLicenseUri == aLicenseUri.value, ) }, test( @@ -79,14 +88,17 @@ object CopyrightAndLicensesSpec extends E2EZSpec { "the response when getting the created resource should contain the license and copyright attribution", ) { for { - createResourceResponseModel <- createStillImageResource(Some(aCopyrightAttribution), Some(aLicense)) - resourceId <- resourceId(createResourceResponseModel) - getResponseModel <- getResourceFromApi(resourceId) - actualCopyright <- copyrightValue(getResponseModel) - actualLicense <- licenseValue(getResponseModel) + createResourceResponseModel <- + createStillImageResource(Some(aCopyrightAttribution), Some(aLicenseText), Some(aLicenseUri)) + resourceId <- resourceId(createResourceResponseModel) + getResponseModel <- getResourceFromApi(resourceId) + actualCopyright <- copyrightValue(getResponseModel) + actualLicenseText <- licenseTextValue(getResponseModel) + actualLicenseUri <- licenseUriValue(getResponseModel) } yield assertTrue( actualCopyright == aCopyrightAttribution.value, - actualLicense == aLicense.value, + actualLicenseText == aLicenseText.value, + actualLicenseUri == aLicenseUri.value, ) }, test( @@ -94,13 +106,16 @@ object CopyrightAndLicensesSpec extends E2EZSpec { "the response when getting the created value should contain the license and copyright attribution", ) { for { - createResourceResponseModel <- createStillImageResource(Some(aCopyrightAttribution), Some(aLicense)) - valueResponseModel <- getValueFromApi(createResourceResponseModel) - actualCopyright <- copyrightValue(valueResponseModel) - actualLicense <- licenseValue(valueResponseModel) + createResourceResponseModel <- + createStillImageResource(Some(aCopyrightAttribution), Some(aLicenseText), Some(aLicenseUri)) + valueResponseModel <- getValueFromApi(createResourceResponseModel) + actualCopyright <- copyrightValue(valueResponseModel) + actualLicenseText <- licenseTextValue(valueResponseModel) + actualLicenseUri <- licenseUriValue(valueResponseModel) } yield assertTrue( actualCopyright == aCopyrightAttribution.value, - actualLicense == aLicense.value, + actualLicenseText == aLicenseText.value, + actualLicenseUri == aLicenseUri.value, ) }, test( @@ -110,17 +125,19 @@ object CopyrightAndLicensesSpec extends E2EZSpec { "the response when getting the updated value should contain the license and copyright attribution of the project", ) { for { - createResourceResponseModel <- createStillImageResource(None, None) + createResourceResponseModel <- createStillImageResource() _ <- addCopyrightAttributionAndLicenseToProject() resourceId <- resourceId(createResourceResponseModel) valueId <- valueId(createResourceResponseModel) _ <- updateValue(resourceId, valueId) valueGetResponse <- getValueFromApi(createResourceResponseModel) actualCopyright <- copyrightValue(valueGetResponse) - actualLicense <- licenseValue(valueGetResponse) + actualLicenseText <- licenseTextValue(valueGetResponse) + actualLicenseUri <- licenseUriValue(valueGetResponse) } yield assertTrue( actualCopyright == projectCopyrightAttribution.value, - actualLicense == projectLicense.value, + actualLicenseText == projectLicenseText.value, + actualLicenseUri == projectLicenseUri.value, ) }, ) @@ TestAspect.before(removeCopyrightAttributionAndLicenseFromProject()) @@ -136,10 +153,12 @@ object CopyrightAndLicensesSpec extends E2EZSpec { createResourceResponseModel <- createStillImageResource() valueResponseModel <- getValueFromApi(createResourceResponseModel) actualCopyright <- copyrightValue(valueResponseModel) - actualLicense <- licenseValue(valueResponseModel) + actualLicenseText <- licenseTextValue(valueResponseModel) + actualLicenseUri <- licenseUriValue(valueResponseModel) } yield assertTrue( actualCopyright == projectCopyrightAttribution.value, - actualLicense == projectLicense.value, + actualLicenseText == projectLicenseText.value, + actualLicenseUri == projectLicenseUri.value, ) }, test( @@ -149,10 +168,12 @@ object CopyrightAndLicensesSpec extends E2EZSpec { for { createResourceResponseModel <- createStillImageResource() actualCopyright <- copyrightValue(createResourceResponseModel) - actualLicense <- licenseValue(createResourceResponseModel) + actualLicenseText <- licenseTextValue(createResourceResponseModel) + actualLicenseUri <- licenseUriValue(createResourceResponseModel) } yield assertTrue( actualCopyright == projectCopyrightAttribution.value, - actualLicense == projectLicense.value, + actualLicenseText == projectLicenseText.value, + actualLicenseUri == projectLicenseUri.value, ) }, test( @@ -160,12 +181,15 @@ object CopyrightAndLicensesSpec extends E2EZSpec { "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) + createResourceResponseModel <- + createStillImageResource(Some(aCopyrightAttribution), Some(aLicenseText), Some(aLicenseUri)) + actualCopyright <- copyrightValue(createResourceResponseModel) + actualLicenseText <- licenseTextValue(createResourceResponseModel) + actualLicenseUri <- licenseUriValue(createResourceResponseModel) } yield assertTrue( actualCopyright == aCopyrightAttribution.value, - actualLicense == aLicense.value, + actualLicenseText == aLicenseText.value, + actualLicenseUri == aLicenseUri.value, ) }, test( @@ -173,13 +197,16 @@ object CopyrightAndLicensesSpec extends E2EZSpec { "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) + createResourceResponseModel <- + createStillImageResource(Some(aCopyrightAttribution), Some(aLicenseText), Some(aLicenseUri)) + valueResponseModel <- getValueFromApi(createResourceResponseModel) + actualCopyright <- copyrightValue(valueResponseModel) + actualLicenseText <- licenseTextValue(valueResponseModel) + actualLicenseUri <- licenseUriValue(valueResponseModel) } yield assertTrue( actualCopyright == aCopyrightAttribution.value, - actualLicense == aLicense.value, + actualLicenseText == aLicenseText.value, + actualLicenseUri == aLicenseUri.value, ) }, ) @@ TestAspect.before(addCopyrightAttributionAndLicenseToProject()) @@ -190,17 +217,22 @@ object CopyrightAndLicensesSpec extends E2EZSpec { ) private def removeCopyrightAttributionAndLicenseFromProject() = - setCopyrightAttributionAndLicenseToProject(None, None) + setCopyrightAttributionAndLicenseToProject(None, None, None) private def addCopyrightAttributionAndLicenseToProject() = - setCopyrightAttributionAndLicenseToProject(Some(projectCopyrightAttribution), Some(projectLicense)) + setCopyrightAttributionAndLicenseToProject( + Some(projectCopyrightAttribution), + Some(projectLicenseText), + Some(projectLicenseUri), + ) private def setCopyrightAttributionAndLicenseToProject( copyrightAttribution: Option[CopyrightAttribution], - license: Option[License], + licenseText: Option[LicenseText], + licenseUri: Option[LicenseUri], ) = for { projectService <- ZIO.service[KnoraProjectService] prj <- projectService.findByShortcode(Shortcode.unsafeFrom("0001")).someOrFail(new Exception("Project not found")) - change = prj.copy(copyrightAttribution = copyrightAttribution, license = license) + change = prj.copy(copyrightAttribution = copyrightAttribution, licenseText = licenseText, licenseUri = licenseUri) updated <- projectService.save(change) } yield updated @@ -209,14 +241,16 @@ object CopyrightAndLicensesSpec extends E2EZSpec { private def createStillImageResource( copyrightAttribution: Option[CopyrightAttribution] = None, - license: Option[License] = None, + licenseText: Option[LicenseText] = None, + licenseUri: Option[LicenseUri] = None, ): ZIO[env, Throwable, Model] = { val jsonLd = UploadFileRequest .make( FileType.StillImageFile(), "internalFilename.jpg", copyrightAttribution = copyrightAttribution, - license = license, + licenseText = licenseText, + licenseUri = licenseUri, ) .toJsonLd(className = Some("ThingPicture"), ontologyName = "anything") for { @@ -303,10 +337,14 @@ object CopyrightAndLicensesSpec extends E2EZSpec { 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 licenseTextValue(model: Model) = + singleStringValueOption(model, HasLicenseText).someOrFail(new Exception("No license text found")) + private def licenseTextValueOption(model: Model) = + singleStringValueOption(model, HasLicenseText) + private def licenseUriValue(model: Model) = + singleStringValueOption(model, HasLicenseUri).someOrFail(new Exception("No license uri found")) + private def licenseUriValueOption(model: Model) = + singleStringValueOption(model, HasLicenseUri) private def singleStringValueOption(model: Model, property: Property): Task[Option[String]] = ZIO .fromEither( diff --git a/integration/src/test/scala/org/knora/webapi/messages/admin/responder/IntegrationTestAdminJsonProtocol.scala b/integration/src/test/scala/org/knora/webapi/messages/admin/responder/IntegrationTestAdminJsonProtocol.scala index d6595ab841..c45046f6be 100644 --- a/integration/src/test/scala/org/knora/webapi/messages/admin/responder/IntegrationTestAdminJsonProtocol.scala +++ b/integration/src/test/scala/org/knora/webapi/messages/admin/responder/IntegrationTestAdminJsonProtocol.scala @@ -46,7 +46,8 @@ import org.knora.webapi.slice.admin.api.model.ProjectMembersGetResponseADM import org.knora.webapi.slice.admin.api.model.ProjectOperationResponseADM import org.knora.webapi.slice.admin.domain.model.Group 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.LicenseText +import org.knora.webapi.slice.admin.domain.model.KnoraProject.LicenseUri import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.common.Value.StringValue @@ -193,7 +194,8 @@ object IntegrationTestAdminJsonProtocol extends TriplestoreJsonProtocol { "status", "selfjoin", "copyrightAttribution", - "license", + "licenseText", + "licenseUri", ), ) @@ -209,8 +211,12 @@ object IntegrationTestAdminJsonProtocol extends TriplestoreJsonProtocol { override val from: String => Either[String, CopyrightAttribution] = CopyrightAttribution.from } - implicit object LicenseFormat extends StringValueFormat[License] { - override val from: String => Either[String, License] = License.from + implicit object LicenseTextFormat extends StringValueFormat[LicenseText] { + override val from: String => Either[String, LicenseText] = LicenseText.from + } + + implicit object LicenseUriFormat extends StringValueFormat[LicenseUri] { + override val from: String => Either[String, LicenseUri] = LicenseUri.from } implicit val groupFormat: JsonFormat[Group] = jsonFormat6(Group.apply) diff --git a/integration/src/test/scala/org/knora/webapi/models/filemodels/FileModelUtil.scala b/integration/src/test/scala/org/knora/webapi/models/filemodels/FileModelUtil.scala index a7bec11c00..ba45634fd8 100644 --- a/integration/src/test/scala/org/knora/webapi/models/filemodels/FileModelUtil.scala +++ b/integration/src/test/scala/org/knora/webapi/models/filemodels/FileModelUtil.scala @@ -12,7 +12,8 @@ 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.admin.domain.model.KnoraProject.LicenseText +import org.knora.webapi.slice.admin.domain.model.KnoraProject.LicenseUri import org.knora.webapi.slice.resources.IiifImageRequestUrl object FileModelUtil { @@ -87,7 +88,8 @@ object FileModelUtil { originalMimeType: Option[String], comment: Option[String], copyrightAttribution: Option[CopyrightAttribution], - license: Option[License], + licenseText: Option[LicenseText], + licenseUri: Option[LicenseUri], ): FileValueContentV2 = fileType match { case FileType.DocumentFile(pageCount, dimX, dimY) => @@ -99,7 +101,8 @@ object FileModelUtil { originalFilename = originalFilename, originalMimeType = Some(originalMimeType.getOrElse("application/pdf")), copyrightAttribution, - license, + licenseText, + licenseUri, ), pageCount = pageCount, dimX = dimX, @@ -115,7 +118,8 @@ object FileModelUtil { originalFilename = originalFilename, originalMimeType = originalMimeType, copyrightAttribution, - license, + licenseText, + licenseUri, ), dimX = dimX, dimY = dimY, @@ -130,7 +134,8 @@ object FileModelUtil { originalFilename = originalFilename, originalMimeType = originalMimeType, copyrightAttribution, - license, + licenseText, + licenseUri, ), externalUrl = externalUrl, comment = comment, @@ -144,7 +149,8 @@ object FileModelUtil { originalFilename = originalFilename, originalMimeType = internalMimeType, copyrightAttribution, - license, + licenseText, + licenseUri, ), ) case FileType.TextFile => @@ -156,7 +162,8 @@ object FileModelUtil { originalFilename = originalFilename, originalMimeType = internalMimeType, copyrightAttribution, - license, + licenseText, + licenseUri, ), ) case FileType.AudioFile => @@ -168,7 +175,8 @@ object FileModelUtil { originalFilename = originalFilename, originalMimeType = internalMimeType, copyrightAttribution, - license, + licenseText, + licenseUri, ), ) case FileType.ArchiveFile => @@ -180,7 +188,8 @@ object FileModelUtil { originalFilename = originalFilename, originalMimeType = internalMimeType, copyrightAttribution, - license, + licenseText, + licenseUri, ), comment = comment, ) diff --git a/integration/src/test/scala/org/knora/webapi/models/filemodels/FileModels.scala b/integration/src/test/scala/org/knora/webapi/models/filemodels/FileModels.scala index 60ac0e42bc..413ede216a 100644 --- a/integration/src/test/scala/org/knora/webapi/models/filemodels/FileModels.scala +++ b/integration/src/test/scala/org/knora/webapi/models/filemodels/FileModels.scala @@ -16,7 +16,8 @@ import org.knora.webapi.messages.v2.responder.resourcemessages.CreateValueInNewR 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 +import org.knora.webapi.slice.admin.domain.model.KnoraProject.LicenseText +import org.knora.webapi.slice.admin.domain.model.KnoraProject.LicenseUri sealed abstract case class UploadFileRequest private ( fileType: FileType, @@ -24,7 +25,8 @@ sealed abstract case class UploadFileRequest private ( label: String, resourceIRI: Option[String] = None, copyrightAttribution: Option[CopyrightAttribution] = None, - license: Option[License] = None, + licenseText: Option[LicenseText] = None, + licenseUri: Option[LicenseUri] = None, ) { /** @@ -60,7 +62,10 @@ sealed abstract case class UploadFileRequest private ( | "@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("")} + | ${licenseText.map(l => s""","knora-api:hasLicenseText" : "${l.value}"""").getOrElse("")} + | ${licenseUri + .map(u => s""", "knora-api:hasLicenseUri" : { "@type" : "xsd:anyURI", "@value":"${u.value}" }""") + .getOrElse("")} | }, | "knora-api:attachedToProject" : { | "@id" : "http://rdfh.ch/projects/$shortcode" @@ -111,7 +116,8 @@ sealed abstract case class UploadFileRequest private ( valuePropertyIRI: Option[SmartIri] = None, project: Option[Project] = None, copyrightAttribution: Option[CopyrightAttribution] = None, - license: Option[License] = None, + licenseText: Option[LicenseText] = None, + licenseUri: Option[LicenseUri] = None, ): CreateResourceV2 = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -131,7 +137,8 @@ sealed abstract case class UploadFileRequest private ( originalMimeType = originalMimeType, comment = comment, copyrightAttribution, - license, + licenseText, + licenseUri, ) val values = List( @@ -183,9 +190,18 @@ object UploadFileRequest { label: String = "test label", resourceIRI: Option[String] = None, copyrightAttribution: Option[CopyrightAttribution] = None, - license: Option[License] = None, + licenseText: Option[LicenseText] = None, + licenseUri: Option[LicenseUri] = None, ): UploadFileRequest = - new UploadFileRequest(fileType, internalFilename, label, resourceIRI, copyrightAttribution, license) {} + new UploadFileRequest( + fileType, + internalFilename, + label, + resourceIRI, + copyrightAttribution, + licenseText, + licenseUri, + ) {} } sealed abstract case class ChangeFileRequest private ( diff --git a/integration/src/test/scala/org/knora/webapi/models/filemodels/FileModelsSpec.scala b/integration/src/test/scala/org/knora/webapi/models/filemodels/FileModelsSpec.scala index d64775f558..828f4b0864 100644 --- a/integration/src/test/scala/org/knora/webapi/models/filemodels/FileModelsSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/models/filemodels/FileModelsSpec.scala @@ -312,6 +312,7 @@ class FileModelsSpec extends CoreSpec { originalMimeType = Some("application/pdf"), None, None, + None, ), pageCount = Some(1), dimX = Some(100), @@ -392,6 +393,7 @@ class FileModelsSpec extends CoreSpec { originalMimeType = originalMimeType, None, None, + None, ), pageCount = pageCount, dimX = dimX, diff --git a/integration/src/test/scala/org/knora/webapi/responders/admin/ProjectRestServiceSpec.scala b/integration/src/test/scala/org/knora/webapi/responders/admin/ProjectRestServiceSpec.scala index 24db171d85..5a33822e5e 100644 --- a/integration/src/test/scala/org/knora/webapi/responders/admin/ProjectRestServiceSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/responders/admin/ProjectRestServiceSpec.scala @@ -163,7 +163,8 @@ class ProjectRestServiceSpec extends CoreSpec with ImplicitSender { status = Status.Active, selfjoin = SelfJoin.CannotJoin, copyrightAttribution = Some(CopyrightAttribution.unsafeFrom("2024, Example Project")), - license = Some(License.unsafeFrom("CC-BY-4.0")), + licenseText = Some(LicenseText.unsafeFrom("CC-BY-4.0")), + licenseUri = Some(LicenseUri.unsafeFrom("https://creativecommons.org/licenses/by/4.0/")), ), SharedTestDataADM.rootUser, ), @@ -177,7 +178,10 @@ class ProjectRestServiceSpec extends CoreSpec with ImplicitSender { Seq(StringLiteralV2.from(value = "project description", language = Some("en"))), ) received.project.copyrightAttribution should be(Some(CopyrightAttribution.unsafeFrom("2024, Example Project"))) - received.project.license should be(Some(License.unsafeFrom("CC-BY-4.0"))) + received.project.licenseText should be(Some(LicenseText.unsafeFrom("CC-BY-4.0"))) + received.project.licenseUri should be( + Some(LicenseUri.unsafeFrom("https://creativecommons.org/licenses/by/4.0/")), + ) newProjectIri.set(received.project.id) @@ -263,6 +267,7 @@ class ProjectRestServiceSpec extends CoreSpec with ImplicitSender { selfjoin = SelfJoin.CannotJoin, None, None, + None, ), SharedTestDataADM.rootUser, ), @@ -276,7 +281,8 @@ class ProjectRestServiceSpec extends CoreSpec with ImplicitSender { Seq(StringLiteralV2.from(value = "project description", language = Some("en"))), ) received.project.copyrightAttribution should be(None) - received.project.license should be(None) + received.project.licenseText should be(None) + received.project.licenseUri should be(None) } "CREATE a project that its info has special characters" in { @@ -301,6 +307,7 @@ class ProjectRestServiceSpec extends CoreSpec with ImplicitSender { selfjoin = SelfJoin.CannotJoin, None, None, + None, ), SharedTestDataADM.rootUser, ), @@ -334,7 +341,8 @@ class ProjectRestServiceSpec extends CoreSpec with ImplicitSender { status = Status.Active, selfjoin = SelfJoin.CannotJoin, copyrightAttribution = None, - license = None, + licenseText = None, + licenseUri = None, ), SharedTestDataADM.rootUser, ), @@ -358,7 +366,8 @@ class ProjectRestServiceSpec extends CoreSpec with ImplicitSender { status = Status.Active, selfjoin = SelfJoin.CannotJoin, copyrightAttribution = None, - license = None, + licenseText = None, + licenseUri = None, ), SharedTestDataADM.rootUser, ), diff --git a/integration/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala b/integration/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala index 76c5cac3bf..8cfa73bc48 100644 --- a/integration/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala +++ b/integration/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala @@ -4321,6 +4321,7 @@ class ValuesResponderV2Spec extends CoreSpec with ImplicitSender { None, None, None, + None, ), ), anythingUser1, @@ -4372,6 +4373,7 @@ class ValuesResponderV2Spec extends CoreSpec with ImplicitSender { None, None, None, + None, ), ), anythingUser1, diff --git a/integration/src/test/scala/org/knora/webapi/sharedtestdata/SharedTestDataADM.scala b/integration/src/test/scala/org/knora/webapi/sharedtestdata/SharedTestDataADM.scala index b4e11edcc4..d55946c45f 100644 --- a/integration/src/test/scala/org/knora/webapi/sharedtestdata/SharedTestDataADM.scala +++ b/integration/src/test/scala/org/knora/webapi/sharedtestdata/SharedTestDataADM.scala @@ -170,7 +170,8 @@ object SharedTestDataADM { status = true, selfjoin = false, copyrightAttribution = None, - license = None, + licenseText = None, + licenseUri = None, ) /* represents the full ProjectADM of the default shared ontologies project */ @@ -186,7 +187,8 @@ object SharedTestDataADM { status = true, selfjoin = false, copyrightAttribution = None, - license = None, + licenseText = None, + licenseUri = None, ) /** @@ -300,7 +302,8 @@ object SharedTestDataADM { status = true, selfjoin = false, copyrightAttribution = None, - license = None, + licenseText = None, + licenseUri = None, ) /* represents the full ProjectADM of the images project in the external format */ @@ -316,7 +319,8 @@ object SharedTestDataADM { status = true, selfjoin = false, copyrightAttribution = None, - license = None, + licenseText = None, + licenseUri = None, ) /* represents the full GroupADM of the images ProjectAdmin group */ @@ -481,7 +485,8 @@ object SharedTestDataADM { status = true, selfjoin = false, copyrightAttribution = None, - license = None, + licenseText = None, + licenseUri = None, ) /* represents the ProjectADM of the incunabula project in the external format*/ @@ -518,7 +523,8 @@ object SharedTestDataADM { status = true, selfjoin = false, copyrightAttribution = None, - license = None, + licenseText = None, + licenseUri = None, ) /** @@ -632,7 +638,8 @@ object SharedTestDataADM { status = true, selfjoin = false, copyrightAttribution = None, - license = None, + licenseText = None, + licenseUri = None, ) def anythingProjectExternal: Project = Project( @@ -650,7 +657,8 @@ object SharedTestDataADM { status = true, selfjoin = false, copyrightAttribution = None, - license = None, + licenseText = None, + licenseUri = None, ) /* represents the full GroupADM of the Thing searcher group */ @@ -689,7 +697,8 @@ object SharedTestDataADM { status = true, selfjoin = false, copyrightAttribution = None, - license = None, + licenseText = None, + licenseUri = None, ) /* represents the user profile of 'superuser' as found in admin-data.ttl */ @@ -740,6 +749,7 @@ object SharedTestDataADM { status = false, selfjoin = false, copyrightAttribution = None, - license = None, + licenseText = None, + licenseUri = None, ) } diff --git a/test_data/generated_test_data/ontologyR2RV2/knoraApiOntologySimple.jsonld b/test_data/generated_test_data/ontologyR2RV2/knoraApiOntologySimple.jsonld index a012f6a442..0cc263a9db 100644 --- a/test_data/generated_test_data/ontologyR2RV2/knoraApiOntologySimple.jsonld +++ b/test_data/generated_test_data/ontologyR2RV2/knoraApiOntologySimple.jsonld @@ -913,7 +913,14 @@ { "@type": "owl:Restriction", "owl:onProperty": { - "@id": "knora-api:hasLicense" + "@id": "knora-api:hasLicenseText" + }, + "owl:maxCardinality": 1 + }, + { + "@type": "owl:Restriction", + "owl:onProperty": { + "@id": "knora-api:hasLicenseUri" }, "owl:maxCardinality": 1 } @@ -1419,13 +1426,21 @@ "@id": "knora-api:hasKeyword" }, { - "@id": "knora-api:hasLicense", + "@id": "knora-api:hasLicenseText", "@type": "owl:DatatypeProperty", "knora-api:objectType": { "@id": "xsd:string" }, "rdfs:comment": "Specifies the terms under which a work can be used. This statement may be a reference to a well-known license, such as Creative Commons (e.g. 'CC BY-SA') or a custom license." }, + { + "@id": "knora-api:hasLicenseUri", + "@type": "owl:DatatypeProperty", + "knora-api:objectType": { + "@id": "xsd:anyUri" + }, + "rdfs:comment": "Canonical link to license." + }, { "rdfs:label": "has Link to", "rdfs:subPropertyOf": { diff --git a/test_data/generated_test_data/ontologyR2RV2/knoraApiOntologyWithValueObjects.jsonld b/test_data/generated_test_data/ontologyR2RV2/knoraApiOntologyWithValueObjects.jsonld index 667d0eb6aa..6e2fe91bcb 100644 --- a/test_data/generated_test_data/ontologyR2RV2/knoraApiOntologyWithValueObjects.jsonld +++ b/test_data/generated_test_data/ontologyR2RV2/knoraApiOntologyWithValueObjects.jsonld @@ -245,7 +245,15 @@ { "@type": "owl:Restriction", "owl:onProperty": { - "@id": "knora-api:hasLicense" + "@id": "knora-api:hasLicenseText" + }, + "owl:maxCardinality": 1, + "knora-api:isInherited": true + }, + { + "@type": "owl:Restriction", + "owl:onProperty": { + "@id": "knora-api:hasLicenseUri" }, "owl:maxCardinality": 1, "knora-api:isInherited": true @@ -544,7 +552,15 @@ { "@type": "owl:Restriction", "owl:onProperty": { - "@id": "knora-api:hasLicense" + "@id": "knora-api:hasLicenseText" + }, + "owl:maxCardinality": 1, + "knora-api:isInherited": true + }, + { + "@type": "owl:Restriction", + "owl:onProperty": { + "@id": "knora-api:hasLicenseUri" }, "owl:maxCardinality": 1, "knora-api:isInherited": true @@ -1358,7 +1374,15 @@ { "@type": "owl:Restriction", "owl:onProperty": { - "@id": "knora-api:hasLicense" + "@id": "knora-api:hasLicenseText" + }, + "owl:maxCardinality": 1, + "knora-api:isInherited": true + }, + { + "@type": "owl:Restriction", + "owl:onProperty": { + "@id": "knora-api:hasLicenseUri" }, "owl:maxCardinality": 1, "knora-api:isInherited": true @@ -2329,7 +2353,15 @@ { "@type": "owl:Restriction", "owl:onProperty": { - "@id": "knora-api:hasLicense" + "@id": "knora-api:hasLicenseText" + }, + "owl:maxCardinality": 1, + "knora-api:isInherited": true + }, + { + "@type": "owl:Restriction", + "owl:onProperty": { + "@id": "knora-api:hasLicenseUri" }, "owl:maxCardinality": 1, "knora-api:isInherited": true @@ -2633,7 +2665,14 @@ { "@type": "owl:Restriction", "owl:onProperty": { - "@id": "knora-api:hasLicense" + "@id": "knora-api:hasLicenseText" + }, + "owl:maxCardinality": 1 + }, + { + "@type": "owl:Restriction", + "owl:onProperty": { + "@id": "knora-api:hasLicenseUri" }, "owl:maxCardinality": 1 }, @@ -3784,7 +3823,15 @@ { "@type": "owl:Restriction", "owl:onProperty": { - "@id": "knora-api:hasLicense" + "@id": "knora-api:hasLicenseText" + }, + "owl:maxCardinality": 1, + "knora-api:isInherited": true + }, + { + "@type": "owl:Restriction", + "owl:onProperty": { + "@id": "knora-api:hasLicenseUri" }, "owl:maxCardinality": 1, "knora-api:isInherited": true @@ -6030,7 +6077,15 @@ { "@type": "owl:Restriction", "owl:onProperty": { - "@id": "knora-api:hasLicense" + "@id": "knora-api:hasLicenseText" + }, + "owl:maxCardinality": 1, + "knora-api:isInherited": true + }, + { + "@type": "owl:Restriction", + "owl:onProperty": { + "@id": "knora-api:hasLicenseUri" }, "owl:maxCardinality": 1, "knora-api:isInherited": true @@ -6184,7 +6239,15 @@ { "@type": "owl:Restriction", "owl:onProperty": { - "@id": "knora-api:hasLicense" + "@id": "knora-api:hasLicenseText" + }, + "owl:maxCardinality": 1, + "knora-api:isInherited": true + }, + { + "@type": "owl:Restriction", + "owl:onProperty": { + "@id": "knora-api:hasLicenseUri" }, "owl:maxCardinality": 1, "knora-api:isInherited": true @@ -6345,7 +6408,15 @@ { "@type": "owl:Restriction", "owl:onProperty": { - "@id": "knora-api:hasLicense" + "@id": "knora-api:hasLicenseText" + }, + "owl:maxCardinality": 1, + "knora-api:isInherited": true + }, + { + "@type": "owl:Restriction", + "owl:onProperty": { + "@id": "knora-api:hasLicenseUri" }, "owl:maxCardinality": 1, "knora-api:isInherited": true @@ -6671,7 +6742,15 @@ { "@type": "owl:Restriction", "owl:onProperty": { - "@id": "knora-api:hasLicense" + "@id": "knora-api:hasLicenseText" + }, + "owl:maxCardinality": 1, + "knora-api:isInherited": true + }, + { + "@type": "owl:Restriction", + "owl:onProperty": { + "@id": "knora-api:hasLicenseUri" }, "owl:maxCardinality": 1, "knora-api:isInherited": true @@ -8455,13 +8534,22 @@ "rdfs:comment": "Indicates a keyword of a resource" }, { - "rdfs:label": "has copyright attribution", + "rdfs:label": "has license text", "rdfs:comment": "Specifies the terms under which a work can be used. This statement may be a reference to a well-known license, such as Creative Commons (e.g. CC BY-SA) or a custom license.", "@type": "owl:DatatypeProperty", "knora-api:objectType": { "@id": "xsd:string" }, - "@id": "knora-api:hasLicense" + "@id": "knora-api:hasLicenseText" + }, + { + "rdfs:label": "has license URI", + "rdfs:comment": "Canonical link to license.", + "@type": "owl:DatatypeProperty", + "knora-api:objectType": { + "@id": "xsd:string" + }, + "@id": "knora-api:hasLicenseUri" }, { "rdfs:label": "has Link to", diff --git a/webapi/src/main/resources/knora-ontologies/knora-admin.ttl b/webapi/src/main/resources/knora-ontologies/knora-admin.ttl index 5368c6e3f7..cd4ab9a89c 100644 --- a/webapi/src/main/resources/knora-ontologies/knora-admin.ttl +++ b/webapi/src/main/resources/knora-ontologies/knora-admin.ttl @@ -387,7 +387,10 @@ owl:onProperty knora-base:hasCopyrightAttribution ; owl:maxCardinality "1"^^xsd:nonNegativeInteger ], [ rdf:type owl:Restriction ; - owl:onProperty knora-base:hasLicense ; + owl:onProperty knora-base:hasLicenseText ; + owl:maxCardinality "1"^^xsd:nonNegativeInteger ], + [ rdf:type owl:Restriction ; + owl:onProperty knora-base:hasLicenseUri ; owl:maxCardinality "1"^^xsd:nonNegativeInteger ]; rdfs:comment "Represents a project that uses Knora."@en . diff --git a/webapi/src/main/resources/knora-ontologies/knora-base.ttl b/webapi/src/main/resources/knora-ontologies/knora-base.ttl index 3e976e6513..ccbf4297ab 100644 --- a/webapi/src/main/resources/knora-ontologies/knora-base.ttl +++ b/webapi/src/main/resources/knora-ontologies/knora-base.ttl @@ -20,7 +20,7 @@ rdf:type owl:Ontology ; rdfs:label "The Knora base ontology"@en ; :attachedToProject knora-admin:SystemProject ; - :ontologyVersion "knora-base v42" . + :ontologyVersion "knora-base v43" . ################################################################# @@ -536,11 +536,17 @@ ### http://www.knora.org/ontology/knora-base#hasLicense -:hasLicense +:hasLicenseText rdf:type owl:DatatypeProperty ; rdfs:comment "Specifies the terms under which a work can be used. This statement may be a reference to a well-known license, such as Creative Commons (e.g. 'CC BY-SA') or a custom license."@en ; :objectDatatypeConstraint xsd:string . + +:hasLicenseUri + rdf:type owl:DatatypeProperty ; + rdfs:comment "Canonical link to license."@en ; + :objectDatatypeConstraint xsd:anyUri . + ### http://www.knora.org/ontology/knora-base#isAnnotationOf :isAnnotationOf @@ -1497,7 +1503,10 @@ owl:onProperty :hasCopyrightAttribution ; owl:maxCardinality "1"^^xsd:nonNegativeInteger ], [ rdf:type owl:Restriction ; - owl:onProperty :hasLicense ; + owl:onProperty :hasLicenseText ; + owl:maxCardinality "1"^^xsd:nonNegativeInteger ], + [ rdf:type owl:Restriction ; + owl:onProperty :hasLicenseUri ; owl:maxCardinality "1"^^xsd:nonNegativeInteger ]. diff --git a/webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala b/webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala index ed6fc1db1e..66b9a80457 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala @@ -264,7 +264,8 @@ object OntologyConstants { val HasDocumentFileValue: IRI = KnoraBasePrefixExpansion + "hasDocumentFileValue" val HasArchiveFileValue: IRI = KnoraBasePrefixExpansion + "hasArchiveFileValue" val HasCopyrightAttribution: IRI = KnoraBasePrefixExpansion + "hasCopyrightAttribution" - val HasLicense: IRI = KnoraBasePrefixExpansion + "hasLicense" + val HasLicenseText: IRI = KnoraBasePrefixExpansion + "hasLicenseText" + val HasLicenseUri: IRI = KnoraBasePrefixExpansion + "hasLicenseUri" val ResourceIcon: IRI = KnoraBasePrefixExpansion + "resourceIcon" @@ -626,7 +627,8 @@ object OntologyConstants { val HasStandoffLinkToValue: IRI = KnoraApiV2PrefixExpansion + "hasStandoffLinkToValue" val HasPermissions: IRI = KnoraApiV2PrefixExpansion + "hasPermissions" val HasCopyrightAttribution: IRI = KnoraApiV2PrefixExpansion + "hasCopyrightAttribution" - val HasLicense: IRI = KnoraApiV2PrefixExpansion + "hasLicense" + val HasLicenseText: IRI = KnoraApiV2PrefixExpansion + "hasLicenseText" + val HasLicenseUri: IRI = KnoraApiV2PrefixExpansion + "hasLicenseUri" val UserHasPermission: String = KnoraApiV2PrefixExpansion + "userHasPermission" val CreationDate: IRI = KnoraApiV2PrefixExpansion + "creationDate" val LastModificationDate: IRI = KnoraApiV2PrefixExpansion + "lastModificationDate" diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/ConstructResponseUtilV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/ConstructResponseUtilV2.scala index 29a7057052..216a8f3bf3 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/ConstructResponseUtilV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/ConstructResponseUtilV2.scala @@ -49,7 +49,8 @@ import org.knora.webapi.messages.v2.responder.standoffmessages.GetXSLTransformat import org.knora.webapi.messages.v2.responder.standoffmessages.MappingXMLtoStandoff 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.KnoraProject.LicenseText +import org.knora.webapi.slice.admin.domain.model.KnoraProject.LicenseUri import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.model.ListProperties.ListIri import org.knora.webapi.slice.admin.domain.model.Permission @@ -1089,7 +1090,11 @@ final case class ConstructResponseUtilV2Live( copyrightAttribution = valueObject .maybeStringObject(OntologyConstants.KnoraBase.HasCopyrightAttribution.toSmartIri) .map(CopyrightAttribution.unsafeFrom), - license = valueObject.maybeStringObject(OntologyConstants.KnoraBase.HasLicense.toSmartIri).map(License.unsafeFrom), + licenseText = valueObject + .maybeStringObject(OntologyConstants.KnoraBase.HasLicenseText.toSmartIri) + .map(LicenseText.unsafeFrom), + licenseUri = + valueObject.maybeIriObject(OntologyConstants.KnoraBase.HasLicenseUri.toSmartIri).map(LicenseUri.unsafeFrom), ) valueType match { diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/KnoraBaseToApiV2ComplexTransformationRules.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/KnoraBaseToApiV2ComplexTransformationRules.scala index c28fa904fa..d9abd335a0 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/KnoraBaseToApiV2ComplexTransformationRules.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/KnoraBaseToApiV2ComplexTransformationRules.scala @@ -156,12 +156,16 @@ object KnoraBaseToApiV2ComplexTransformationRules extends OntologyTransformation .withRdfLabelEn("has copyright attribution") .withRdfCommentEn("The copyright statement that gives credit to the original author.") - private val HasLicense = makeOwlDatatypeProperty(KA.HasLicense, XSD.STRING) - .withRdfLabelEn("has copyright attribution") + private val HasLicenseText = makeOwlDatatypeProperty(KA.HasLicenseText, XSD.STRING) + .withRdfLabelEn("has license text") .withRdfCommentEn( "Specifies the terms under which a work can be used. This statement may be a reference to a well-known license, such as Creative Commons (e.g. CC BY-SA) or a custom license.", ) + private val HasLicenseUri = makeOwlDatatypeProperty(KA.HasLicenseUri, XSD.STRING) + .withRdfLabelEn("has license URI") + .withRdfCommentEn("Canonical link to license.") + private val ValueAsString = makeOwlDatatypeProperty(KA.ValueAsString, XSD.STRING) .withSubjectType(KA.Value) .withRdfCommentEn("A plain string representation of a value") @@ -657,7 +661,8 @@ object KnoraBaseToApiV2ComplexTransformationRules extends OntologyTransformation GeonameValueAsGeonameCode, HasCopyrightAttribution, HasIncomingLinkValue, - HasLicense, + HasLicenseText, + HasLicenseUri, IntValueAsInt, IntervalValueHasEnd, IntervalValueHasStart, 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 e226264b3b..ebb620db04 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,8 @@ package org.knora.webapi.messages.v2.responder.resourcemessages +import monocle.Optional + import java.time.Instant import java.util.UUID @@ -24,10 +26,15 @@ 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.CreateResourceV2Optics +import org.knora.webapi.messages.v2.responder.resourcemessages.ResourceMessagesV2Optics.CreateValueInNewResourceV2Optics 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.LicenseText +import org.knora.webapi.slice.admin.domain.model.KnoraProject.LicenseUri 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 @@ -626,6 +633,35 @@ case class CreateResourceV2( ) } +object CreateResourceV2 { + def replaceCopyrightAndLicenceIfMissing( + licenseText: Option[LicenseText], + licenseUri: Option[LicenseUri], + copyrightAttribution: Option[CopyrightAttribution], + cr: 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(licenseText, FileValueV2Optics.licenseTextOption) + .andThen(replaceIfEmpty(licenseUri, FileValueV2Optics.licenseUriOption)) + .andThen(replaceIfEmpty(copyrightAttribution, FileValueV2Optics.copyrightAttributionOption))(cr) + } +} + /** * Represents a request to create a resource. * 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 4e0f3b35d3..9d74c5e028 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 @@ -6,6 +6,7 @@ package org.knora.webapi.messages.v2.responder.valuemessages import org.apache.jena.rdf.model.Resource +import org.apache.jena.vocabulary.XSD import zio.IO import zio.ZIO @@ -44,7 +45,8 @@ 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 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.LicenseText +import org.knora.webapi.slice.admin.domain.model.KnoraProject.LicenseUri import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode import org.knora.webapi.slice.admin.domain.model.Permission import org.knora.webapi.slice.common.Value.StringValue @@ -58,10 +60,7 @@ import org.knora.webapi.store.iiif.api.SipiService import org.knora.webapi.util.WithAsIs private def objectCommentOption(r: Resource): Either[String, Option[String]] = - r.objectStringOption(ValueHasComment) flatMap { - case Some(str) => Iri.toSparqlEncodedString(str).toRight(s"Invalid comment: $str").map(Some(_)) - case None => Right(None) - } + r.objectStringOption(ValueHasComment, str => Iri.toSparqlEncodedString(str).toRight(s"Invalid comment: $str")) /** * Represents a successful response to a create value Request. @@ -675,17 +674,17 @@ sealed trait ValueContentV2 extends KnoraContentV2[ValueContentV2] with WithAsIs */ object ValueContentV2 { def replaceCopyrightAndLicenceIfMissing( - license: Option[License], + licenseText: Option[LicenseText], + licenseUri: Option[LicenseUri], 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 fvc: FileValueContentV2 => { + val lt = FileValueContentV2Optics.licenseTextOption.filter(_.isEmpty).replace(licenseText) + val lu = FileValueContentV2Optics.licenseUriOption.filter(_.isEmpty).replace(licenseUri) + val cp = FileValueContentV2Optics.copyrightAttributionOption.filter(_.isEmpty).replace(copyrightAttribution) + lt.andThen(lu).andThen(cp)(fvc) + } case other => other } @@ -867,25 +866,19 @@ object DateValueContentV2 { ) } - def from(r: Resource): Either[String, DateValueContentV2] = { - def objectEraOption(resource: Resource, property: String) = for { - eraStr <- resource.objectStringOption(property) - era <- eraStr match - case Some(e) => DateEraV2.fromString(e).map(Some(_)) - case None => Right(None) - } yield era + def from(r: Resource): Either[String, DateValueContentV2] = for { startYear <- r.objectInt(DateValueHasStartYear) startMonth <- r.objectIntOption(DateValueHasStartMonth) startDay <- r.objectIntOption(DateValueHasStartDay) - startEra <- objectEraOption(r, DateValueHasStartEra) + startEra <- r.objectStringOption(DateValueHasStartEra, DateEraV2.fromString) endYear <- r.objectInt(DateValueHasEndYear) endMonth <- r.objectIntOption(DateValueHasEndMonth) endDay <- r.objectIntOption(DateValueHasEndDay) - endEra <- objectEraOption(r, DateValueHasEndEra) + endEra <- r.objectStringOption(DateValueHasEndEra, DateEraV2.fromString) - calendarName <- r.objectString(DateValueHasCalendar).flatMap(CalendarNameV2.fromString) + calendarName <- r.objectString(DateValueHasCalendar, CalendarNameV2.fromString) // validate the combination of start/end dates and calendarName _ <- if (startMonth.isEmpty && startDay.isDefined) Left(s"Start day defined, missing start month") else Right(()) @@ -910,7 +903,6 @@ object DateValueContentV2 { calendarName, comment, ) - } } /** @@ -2022,7 +2014,8 @@ case class FileValueV2( originalFilename: Option[String], originalMimeType: Option[String], copyrightAttribution: Option[CopyrightAttribution], - license: Option[License], + licenseText: Option[LicenseText], + licenseUri: Option[LicenseUri], ) /** @@ -2046,6 +2039,13 @@ sealed trait FileValueContentV2 extends ValueContentV2 { def toJsonLDObjectMapInComplexSchema(fileUrl: String): Map[IRI, JsonLDValue] = { def mkJsonLdString: StringValue => JsonLDString = sv => JsonLDString(sv.value) + def mkJsonLdUri: StringValue => JsonLDObject = sv => + JsonLDObject( + Map( + "@type" -> JsonLDString("http://www.w3.org/2001/XMLSchema#anyURI"), + "@value" -> JsonLDString(sv.value), + ), + ) val knownValues: Map[IRI, JsonLDValue] = Map( FileValueHasFilename -> JsonLDString(fileValue.internalFilename), FileValueAsUrl -> JsonLDUtil.datatypeValueToJsonLDObject( @@ -2053,9 +2053,10 @@ sealed trait FileValueContentV2 extends ValueContentV2 { datatype = OntologyConstants.Xsd.Uri.toSmartIri, ), ) - val copyrightOption = fileValue.copyrightAttribution.map(mkJsonLdString).map((HasCopyrightAttribution, _)) - val licenseOption = fileValue.license.map(mkJsonLdString).map((HasLicense, _)) - knownValues ++ copyrightOption ++ licenseOption + val copyrightOption = fileValue.copyrightAttribution.map(mkJsonLdString).map((HasCopyrightAttribution, _)) + val licenseTextOption = fileValue.licenseText.map(mkJsonLdString).map((HasLicenseText, _)) + val licenseUriOption = fileValue.licenseUri.map(mkJsonLdUri).map((HasLicenseUri, _)) + knownValues ++ copyrightOption ++ licenseTextOption ++ licenseUriOption } } @@ -2143,15 +2144,17 @@ object StillImageFileValueContentV2 { def from(r: Resource, fileInfo: FileInfo): Either[String, StillImageFileValueContentV2] = for { comment <- objectCommentOption(r) meta = fileInfo.metadata - copyrightAttribution <- getCopyrightAttribution(r) - license <- getLicense(r) + copyrightAttribution <- r.objectStringOption(HasCopyrightAttribution, CopyrightAttribution.from) + licenseText <- r.objectStringOption(HasLicenseText, LicenseText.from) + licenseUri <- r.objectDataTypeOption(HasLicenseUri, XSD.anyURI.toString, LicenseUri.from) fileValue = FileValueV2( fileInfo.filename, meta.internalMimeType, meta.originalFilename, meta.originalMimeType, copyrightAttribution, - license, + licenseText, + licenseUri, ) } yield StillImageFileValueContentV2( ApiV2Complex, @@ -2162,22 +2165,6 @@ object StillImageFileValueContentV2 { ) } -def getCopyrightAttribution(resource: Resource): Either[String, Option[CopyrightAttribution]] = for { - str <- resource.objectStringOption(HasCopyrightAttribution) - copyrightAttribution <- str match { - case Some(str) => CopyrightAttribution.from(str).map(Some(_)) - case None => Right(None) - } -} yield copyrightAttribution - -def getLicense(resource: Resource): Either[String, Option[License]] = for { - str <- resource.objectStringOption(HasLicense) - copyrightAttribution <- str match { - case Some(str) => License.from(str).map(Some(_)) - case None => Right(None) - } -} yield copyrightAttribution - /** * Represents the external image file metadata. * @@ -2258,15 +2245,17 @@ object StillImageExternalFileValueContentV2 { externalUrlStr <- r.objectString(StillImageFileValueHasExternalUrl) iifUrl <- IiifImageRequestUrl.from(externalUrlStr) comment <- objectCommentOption(r) - copyrightAttribution <- getCopyrightAttribution(r) - license <- getLicense(r) + copyrightAttribution <- r.objectStringOption(HasCopyrightAttribution, CopyrightAttribution.from) + licenseText <- r.objectStringOption(HasLicenseText, LicenseText.from) + licenseUri <- r.objectDataTypeOption(HasLicenseUri, XSD.anyURI.toString, LicenseUri.from) fileValue = FileValueV2( "internalFilename", "internalMimeType", Some("originalFilename"), Some("originalMimeType"), copyrightAttribution, - license, + licenseText, + licenseUri, ) } yield StillImageExternalFileValueContentV2(ApiV2Complex, fileValue, iifUrl, comment) } @@ -2407,15 +2396,17 @@ object DocumentFileValueContentV2 { def from(r: Resource, info: FileInfo): Either[String, DocumentFileValueContentV2] = for { comment <- objectCommentOption(r) meta = info.metadata - copyrightAttribution <- getCopyrightAttribution(r) - license <- getLicense(r) + copyrightAttribution <- r.objectStringOption(HasCopyrightAttribution, CopyrightAttribution.from) + licenseText <- r.objectStringOption(HasLicenseText, LicenseText.from) + licenseUri <- r.objectDataTypeOption(HasLicenseUri, XSD.anyURI.toString, LicenseUri.from) fileValue = FileValueV2( info.filename, meta.internalMimeType, meta.originalFilename, meta.originalMimeType, copyrightAttribution, - license, + licenseText, + licenseUri, ) } yield DocumentFileValueContentV2(ApiV2Complex, fileValue, meta.numpages, meta.width, meta.height, comment) } @@ -2427,15 +2418,17 @@ object ArchiveFileValueContentV2 { def from(r: Resource, info: FileInfo): Either[String, ArchiveFileValueContentV2] = for { comment <- objectCommentOption(r) meta = info.metadata - copyrightAttribution <- getCopyrightAttribution(r) - license <- getLicense(r) + copyrightAttribution <- r.objectStringOption(HasCopyrightAttribution, CopyrightAttribution.from) + licenseText <- r.objectStringOption(HasLicenseText, LicenseText.from) + licenseUri <- r.objectDataTypeOption(HasLicenseUri, XSD.anyURI.toString, LicenseUri.from) fileValue = FileValueV2( info.filename, meta.internalMimeType, meta.originalFilename, meta.originalMimeType, copyrightAttribution, - license, + licenseText, + licenseUri, ) } yield ArchiveFileValueContentV2(ApiV2Complex, fileValue, comment) } @@ -2506,15 +2499,17 @@ object TextFileValueContentV2 { def from(r: Resource, info: FileInfo): Either[String, TextFileValueContentV2] = for { comment <- objectCommentOption(r) meta = info.metadata - copyrightAttribution <- getCopyrightAttribution(r) - license <- getLicense(r) + copyrightAttribution <- r.objectStringOption(HasCopyrightAttribution, CopyrightAttribution.from) + licenseText <- r.objectStringOption(HasLicenseText, LicenseText.from) + licenseUri <- r.objectDataTypeOption(HasLicenseUri, XSD.anyURI.toString, LicenseUri.from) fileValue = FileValueV2( info.filename, meta.internalMimeType, meta.originalFilename, meta.originalMimeType, copyrightAttribution, - license, + licenseText, + licenseUri, ) } yield TextFileValueContentV2(ApiV2Complex, fileValue, comment) } @@ -2585,8 +2580,9 @@ object AudioFileValueContentV2 { def from(r: Resource, info: FileInfo): Either[String, AudioFileValueContentV2] = for { comment <- objectCommentOption(r) meta = info.metadata - copyrightAttribution <- getCopyrightAttribution(r) - license <- getLicense(r) + copyrightAttribution <- r.objectStringOption(HasCopyrightAttribution, CopyrightAttribution.from) + licenseText <- r.objectStringOption(HasLicenseText, LicenseText.from) + licenseUri <- r.objectDataTypeOption(HasLicenseUri, XSD.anyURI.toString, LicenseUri.from) } yield AudioFileValueContentV2( ApiV2Complex, FileValueV2( @@ -2595,7 +2591,8 @@ object AudioFileValueContentV2 { meta.originalFilename, meta.originalMimeType, copyrightAttribution, - license, + licenseText, + licenseUri, ), comment, ) @@ -2669,8 +2666,9 @@ object MovingImageFileValueContentV2 { def from(r: Resource, info: FileInfo): Either[String, MovingImageFileValueContentV2] = for { comment <- objectCommentOption(r) meta = info.metadata - copyrightAttribution <- getCopyrightAttribution(r) - license <- getLicense(r) + copyrightAttribution <- r.objectStringOption(HasCopyrightAttribution, CopyrightAttribution.from) + licenseText <- r.objectStringOption(HasLicenseText, LicenseText.from) + licenseUri <- r.objectDataTypeOption(HasLicenseUri, XSD.anyURI.toString, LicenseUri.from) } yield MovingImageFileValueContentV2( ApiV2Complex, FileValueV2( @@ -2679,7 +2677,8 @@ object MovingImageFileValueContentV2 { meta.originalFilename, meta.originalMimeType, copyrightAttribution, - license, + licenseText, + licenseUri, ), comment, ) 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 0919d18093..8bab477d18 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,7 +9,8 @@ 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 +import org.knora.webapi.slice.admin.domain.model.KnoraProject.LicenseText +import org.knora.webapi.slice.admin.domain.model.KnoraProject.LicenseUri object ValueMessagesV2Optics { @@ -18,8 +19,9 @@ object ValueMessagesV2Optics { val copyrightAttributionOption: Lens[FileValueV2, Option[CopyrightAttribution]] = GenLens[FileValueV2](_.copyrightAttribution) - val licenseOption: Lens[FileValueV2, Option[License]] = - GenLens[FileValueV2](_.license) + val licenseTextOption: Lens[FileValueV2, Option[LicenseText]] = GenLens[FileValueV2](_.licenseText) + + val licenseUriOption: Lens[FileValueV2, Option[LicenseUri]] = GenLens[FileValueV2](_.licenseUri) } @@ -35,9 +37,11 @@ object ValueMessagesV2Optics { case vc: ArchiveFileValueContentV2 => vc.copy(fileValue = fv) case vc: TextFileValueContentV2 => vc.copy(fileValue = fv) }) - val copyRightAttributionOption: Lens[FileValueContentV2, Option[CopyrightAttribution]] = + val copyrightAttributionOption: Lens[FileValueContentV2, Option[CopyrightAttribution]] = fileValueV2.andThen(FileValueV2Optics.copyrightAttributionOption) - val licenseOption: Lens[FileValueContentV2, Option[License]] = - fileValueV2.andThen(FileValueV2Optics.licenseOption) + val licenseTextOption: Lens[FileValueContentV2, Option[LicenseText]] = + fileValueV2.andThen(FileValueV2Optics.licenseTextOption) + val licenseUriOption: Lens[FileValueContentV2, Option[LicenseUri]] = + fileValueV2.andThen(FileValueV2Optics.licenseUriOption) } } diff --git a/webapi/src/main/scala/org/knora/webapi/package.scala b/webapi/src/main/scala/org/knora/webapi/package.scala index 0e40b8f2d7..82c9853cf7 100644 --- a/webapi/src/main/scala/org/knora/webapi/package.scala +++ b/webapi/src/main/scala/org/knora/webapi/package.scala @@ -14,7 +14,7 @@ package object webapi { * The version of `knora-base` and of the other built-in ontologies that this version of Knora requires. * Must be the same as the object of `knora-base:ontologyVersion` in the `knora-base` ontology being used. */ - val KnoraBaseVersion: Int = 42 + val KnoraBaseVersion: Int = 43 val KnoraBaseVersionString: String = s"$versionPrefix$KnoraBaseVersion" /** 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 1deecb50cf..93c91d13cd 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 @@ -93,7 +93,8 @@ final case class ValuesResponderV2( submittedInternalValueContent = ValueContentV2 .replaceCopyrightAndLicenceIfMissing( - project.license, + project.licenseText, + project.licenseUri, project.copyrightAttribution, valueToCreate.valueContent, ) @@ -707,7 +708,12 @@ final case class ValuesResponderV2( project = resourceInfo.projectADM submittedInternalValueContent = ValueContentV2 - .replaceCopyrightAndLicenceIfMissing(project.license, project.copyrightAttribution, updateValue.valueContent) + .replaceCopyrightAndLicenceIfMissing( + project.licenseText, + project.licenseUri, + 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 @@ -1019,7 +1025,8 @@ final case class ValuesResponderV2( // Generate a SPARQL update. newValue: ValueContentV2 = ValueContentV2.replaceCopyrightAndLicenceIfMissing( - resourceInfo.projectADM.license, + resourceInfo.projectADM.licenseText, + resourceInfo.projectADM.licenseUri, resourceInfo.projectADM.copyrightAttribution, newValueVersion, ) 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 ad1931c97e..299f19ab7c 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,7 +6,6 @@ package org.knora.webapi.responders.v2.resources import com.typesafe.scalalogging.LazyLogging -import monocle.Optional import zio.* import java.time.Instant @@ -30,11 +29,8 @@ 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 @@ -90,27 +86,6 @@ 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] = @@ -197,7 +172,13 @@ final case class CreateResourceV2Handler( // Convert the resource to the internal ontology schema. internalCreateResource <- ZIO.attempt( - replaceCopyrightAttributionAndLicenseIfMissing(project)(createResourceRequestV2.createResource) + CreateResourceV2 + .replaceCopyrightAndLicenceIfMissing( + project.licenseText, + project.licenseUri, + project.copyrightAttribution, + createResourceRequestV2.createResource, + ) .toOntologySchema(InternalSchema), ) @@ -528,7 +509,8 @@ final case class CreateResourceV2Handler( dimX = dimX, dimY = dimY, fileValue.copyrightAttribution, - fileValue.license, + fileValue.licenseText, + fileValue.licenseUri, ), ) case StillImageExternalFileValueContentV2(_, fileValue, externalUrl, _) => @@ -540,7 +522,8 @@ final case class CreateResourceV2Handler( originalMimeType = fileValue.originalMimeType, externalUrl = externalUrl.value.toString(), fileValue.copyrightAttribution, - fileValue.license, + fileValue.licenseText, + fileValue.licenseUri, ), ) case DocumentFileValueContentV2(_, fileValue, pageCount, dimX, dimY, _) => @@ -554,7 +537,8 @@ final case class CreateResourceV2Handler( dimY = dimY, pageCount = pageCount, fileValue.copyrightAttribution, - fileValue.license, + fileValue.licenseText, + fileValue.licenseUri, ), ) case ArchiveFileValueContentV2(_, fileValue, _) => @@ -565,7 +549,8 @@ final case class CreateResourceV2Handler( originalFilename = fileValue.originalFilename, originalMimeType = fileValue.originalMimeType, fileValue.copyrightAttribution, - fileValue.license, + fileValue.licenseText, + fileValue.licenseUri, ), ) case TextFileValueContentV2(_, fileValue, _) => @@ -576,7 +561,8 @@ final case class CreateResourceV2Handler( originalFilename = fileValue.originalFilename, originalMimeType = fileValue.originalMimeType, fileValue.copyrightAttribution, - fileValue.license, + fileValue.licenseText, + fileValue.licenseUri, ), ) case AudioFileValueContentV2(_, fileValue, _) => @@ -587,7 +573,8 @@ final case class CreateResourceV2Handler( originalFilename = fileValue.originalFilename, originalMimeType = fileValue.originalMimeType, fileValue.copyrightAttribution, - fileValue.license, + fileValue.licenseText, + fileValue.licenseUri, ), ) case MovingImageFileValueContentV2(_, fileValue, _) => @@ -598,7 +585,8 @@ final case class CreateResourceV2Handler( originalFilename = fileValue.originalFilename, originalMimeType = fileValue.originalMimeType, fileValue.copyrightAttribution, - fileValue.license, + fileValue.licenseText, + fileValue.licenseUri, ), ) case LinkValueContentV2( diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Codecs.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Codecs.scala index d0486968d8..f3da1aa449 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Codecs.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Codecs.scala @@ -108,7 +108,8 @@ object Codecs { implicit val sparqlEncodedString: StringCodec[SparqlEncodedString] = stringCodec(SparqlEncodedString.from) implicit val status: StringCodec[Status] = booleanCodec(Status.from) implicit val copyrightAttribution: StringCodec[CopyrightAttribution] = stringCodec(CopyrightAttribution.from) - implicit val license: StringCodec[License] = stringCodec(License.from) + implicit val licenseText: StringCodec[LicenseText] = stringCodec(LicenseText.from) + implicit val licenseUri: StringCodec[LicenseUri] = stringCodec(LicenseUri.from) // user implicit val userIri: StringCodec[UserIri] = stringCodec(UserIri.from) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Examples.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Examples.scala index c76501dd20..bfc6d7a304 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Examples.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Examples.scala @@ -24,7 +24,8 @@ import org.knora.webapi.slice.admin.domain.model.GroupName import org.knora.webapi.slice.admin.domain.model.GroupSelfJoin import org.knora.webapi.slice.admin.domain.model.GroupStatus 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.LicenseText +import org.knora.webapi.slice.admin.domain.model.KnoraProject.LicenseUri import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.admin.domain.model.UserIri @@ -96,7 +97,8 @@ object Examples { ontologies = Seq.empty, selfjoin = false, copyrightAttribution = Some(CopyrightAttribution.unsafeFrom("2024, Example Project")), - license = Some(License.unsafeFrom("CC-BY-4.0")), + licenseText = Some(LicenseText.unsafeFrom("CC-BY-4.0")), + licenseUri = Some(LicenseUri.unsafeFrom("http://example.com/license")), ) private val group = Group( diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectsEndpointsRequestsAndResponses.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectsEndpointsRequestsAndResponses.scala index 109144d967..2119abbf07 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectsEndpointsRequestsAndResponses.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectsEndpointsRequestsAndResponses.scala @@ -28,7 +28,8 @@ object ProjectsEndpointsRequestsAndResponses { status: Status, selfjoin: SelfJoin, copyrightAttribution: Option[CopyrightAttribution], - license: Option[License], + licenseText: Option[LicenseText], + licenseUri: Option[LicenseUri], ) object ProjectCreateRequest { implicit val codec: JsonCodec[ProjectCreateRequest] = DeriveJsonCodec.gen[ProjectCreateRequest] @@ -43,7 +44,8 @@ object ProjectsEndpointsRequestsAndResponses { status: Option[Status] = None, selfjoin: Option[SelfJoin] = None, copyrightAttribution: Option[CopyrightAttribution] = None, - license: Option[License] = None, + licenseText: Option[LicenseText] = None, + licenseUri: Option[LicenseUri] = None, ) object ProjectUpdateRequest { implicit val codec: JsonCodec[ProjectUpdateRequest] = DeriveJsonCodec.gen[ProjectUpdateRequest] diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectsMessagesADM.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectsMessagesADM.scala index dcab052e59..c3d24874fc 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectsMessagesADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectsMessagesADM.scala @@ -12,7 +12,8 @@ import org.knora.webapi.IRI import org.knora.webapi.messages.admin.responder.AdminKnoraResponseADM import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2 import org.knora.webapi.slice.admin.api.Codecs.ZioJsonCodec.copyrightAttribution -import org.knora.webapi.slice.admin.api.Codecs.ZioJsonCodec.license +import org.knora.webapi.slice.admin.api.Codecs.ZioJsonCodec.licenseText +import org.knora.webapi.slice.admin.api.Codecs.ZioJsonCodec.licenseUri import org.knora.webapi.slice.admin.domain.model.KnoraProject.* import org.knora.webapi.slice.admin.domain.model.RestrictedView import org.knora.webapi.slice.admin.domain.model.User @@ -43,7 +44,8 @@ case class Project( status: Boolean, selfjoin: Boolean, copyrightAttribution: Option[CopyrightAttribution], - license: Option[License], + licenseText: Option[LicenseText], + licenseUri: Option[LicenseUri], ) extends Ordered[Project] { def projectIri: ProjectIri = ProjectIri.unsafeFrom(id) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraProject.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraProject.scala index 67ffcfef1e..ebc4f7a29e 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraProject.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraProject.scala @@ -7,6 +7,8 @@ package org.knora.webapi.slice.admin.domain.model import zio.NonEmptyChunk +import java.net.URI +import scala.util.Try import scala.util.matching.Regex import dsp.valueobjects.Iri.isIri @@ -35,7 +37,8 @@ case class KnoraProject( selfjoin: SelfJoin, restrictedView: RestrictedView, copyrightAttribution: Option[CopyrightAttribution], - license: Option[License], + licenseText: Option[LicenseText], + licenseUri: Option[LicenseUri], ) extends EntityWithId[ProjectIri] object KnoraProject { @@ -186,12 +189,18 @@ object KnoraProject { else Right(CopyrightAttribution(str)) } - final case class License private (override val value: String) extends StringValue - object License extends StringValueCompanion[License] { + final case class LicenseText private (override val value: String) extends StringValue + object LicenseText extends StringValueCompanion[LicenseText] { private val maxLength = 10_000 - def from(str: String): Either[String, License] = + def from(str: String): Either[String, LicenseText] = if (str.isEmpty) Left("License cannot be empty.") else if (str.length >= maxLength) Left(s"License may only be ${maxLength} characters long.") - else Right(License(str)) + else Right(LicenseText(str)) + } + + final case class LicenseUri private (override val value: String) extends StringValue + object LicenseUri extends StringValueCompanion[LicenseUri] { + def from(str: String): Either[String, LicenseUri] = + Try(URI.create(str)).toEither.left.map(_ => s"License URI '$str' is not a valid URI.").map(_ => LicenseUri(str)) } } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectRepo.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectRepo.scala index 2d82da3468..06c29481dc 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectRepo.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectRepo.scala @@ -49,6 +49,7 @@ object KnoraProjectRepo { RestrictedView.default, None, None, + None, ) val SystemProject: KnoraProject = makeBuiltIn( 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 b7b9859a53..b5bacb7bbb 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 @@ -53,7 +53,8 @@ final case class KnoraProjectService(knoraProjectRepo: KnoraProjectRepo, ontolog req.selfjoin, RestrictedView.default, req.copyrightAttribution, - req.license, + req.licenseText, + req.licenseUri, ) project <- knoraProjectRepo.save(project) } yield project @@ -100,7 +101,8 @@ final case class KnoraProjectService(knoraProjectRepo: KnoraProjectRepo, ontolog status = updateReq.status.getOrElse(project.status), selfjoin = updateReq.selfjoin.getOrElse(project.selfjoin), copyrightAttribution = updateReq.copyrightAttribution.orElse(project.copyrightAttribution), - license = updateReq.license.orElse(project.license), + licenseText = updateReq.licenseText.orElse(project.licenseText), + licenseUri = updateReq.licenseUri.orElse(project.licenseUri), ), ) } yield updated diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/ProjectService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/ProjectService.scala index e4af95ef9c..0597bd7e14 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/ProjectService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/ProjectService.scala @@ -57,7 +57,8 @@ final case class ProjectService( knoraProject.status.value, knoraProject.selfjoin.value, knoraProject.copyrightAttribution, - knoraProject.license, + knoraProject.licenseText, + knoraProject.licenseUri, ), ) @@ -76,7 +77,8 @@ final case class ProjectService( selfjoin = SelfJoin.from(project.selfjoin), restrictedView, project.copyrightAttribution, - project.license, + project.licenseText, + project.licenseUri, ) def setProjectRestrictedView(project: Project, settings: RestrictedView): Task[RestrictedView] = diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/rdf/RdfConversions.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/rdf/RdfConversions.scala index f01da22943..2909b094e9 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/rdf/RdfConversions.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/rdf/RdfConversions.scala @@ -43,7 +43,8 @@ object RdfConversions { implicit val descriptionConverter: LangString => Either[String, Description] = langString => Description.from(StringLiteralV2.from(langString.value, langString.lang)) implicit val copyrightAttributionConverter: String => Either[String, CopyrightAttribution] = CopyrightAttribution.from - implicit val licenseConverter: String => Either[String, License] = License.from + implicit val licenseTextConverter: String => Either[String, LicenseText] = LicenseText.from + implicit val licenseUriConverter: String => Either[String, LicenseUri] = LicenseUri.from // User properties implicit val usernameConverter: String => Either[String, Username] = Username.from diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala index 9268f786c1..a2f02ae5dc 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala @@ -7,15 +7,18 @@ package org.knora.webapi.slice.admin.repo.service import org.eclipse.rdf4j.common.net.ParsedIRI import org.eclipse.rdf4j.model.vocabulary.RDF +import org.eclipse.rdf4j.model.vocabulary.XSD import org.eclipse.rdf4j.sparqlbuilder.graphpattern.TriplePattern import org.eclipse.rdf4j.sparqlbuilder.rdf.Iri import org.eclipse.rdf4j.sparqlbuilder.rdf.Rdf +import org.eclipse.rdf4j.sparqlbuilder.rdf.Rdf.literalOfType import zio.* import org.knora.webapi.messages.OntologyConstants.KnoraAdmin import org.knora.webapi.messages.OntologyConstants.KnoraAdmin.* import org.knora.webapi.messages.OntologyConstants.KnoraBase.HasCopyrightAttribution -import org.knora.webapi.messages.OntologyConstants.KnoraBase.HasLicense +import org.knora.webapi.messages.OntologyConstants.KnoraBase.HasLicenseText +import org.knora.webapi.messages.OntologyConstants.KnoraBase.HasLicenseUri import org.knora.webapi.slice.admin.domain.model.KnoraProject import org.knora.webapi.slice.admin.domain.model.KnoraProject.* import org.knora.webapi.slice.admin.domain.model.RestrictedView @@ -51,7 +54,8 @@ final case class KnoraProjectRepoLive( Vocabulary.KnoraAdmin.projectRestrictedViewSize, Vocabulary.KnoraAdmin.projectRestrictedViewWatermark, Vocabulary.KnoraBase.hasCopyrightAttribution, - Vocabulary.KnoraBase.hasLicense, + Vocabulary.KnoraBase.hasLicenseText, + Vocabulary.KnoraBase.hasLicenseUri, ), ) @@ -105,7 +109,8 @@ object KnoraProjectRepoLive { status <- resource.getBooleanLiteralOrFail[Status](StatusProp) selfjoin <- resource.getBooleanLiteralOrFail[SelfJoin](HasSelfJoinEnabled) copyrightAttribution <- resource.getStringLiteral[CopyrightAttribution](HasCopyrightAttribution) - license <- resource.getStringLiteral[License](HasLicense) + licenseText <- resource.getStringLiteral[LicenseText](HasLicenseText) + licenseUri <- resource.getUriLiteral[LicenseUri](HasLicenseUri) restrictedView <- getRestrictedView } yield KnoraProject( id = ProjectIri.unsafeFrom(iri.value), @@ -119,7 +124,8 @@ object KnoraProjectRepoLive { selfjoin = selfjoin, restrictedView = restrictedView, copyrightAttribution = copyrightAttribution, - license = license, + licenseText = licenseText, + licenseUri = licenseUri, ) } @@ -148,7 +154,10 @@ object KnoraProjectRepoLive { project.copyrightAttribution.foreach(attr => pattern.andHas(Vocabulary.KnoraBase.hasCopyrightAttribution, attr.value), ) - project.license.foreach(license => pattern.andHas(Vocabulary.KnoraBase.hasLicense, license.value)) + project.licenseText.foreach(text => pattern.andHas(Vocabulary.KnoraBase.hasLicenseText, text.value)) + project.licenseUri.foreach(uri => + pattern.andHas(Vocabulary.KnoraBase.hasLicenseUri, literalOfType(uri.value, XSD.ANYURI)), + ) pattern } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/jena/ResourceOps.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/jena/ResourceOps.scala index 31b14a0470..78d542abea 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/jena/ResourceOps.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/jena/ResourceOps.scala @@ -5,6 +5,8 @@ package org.knora.webapi.slice.common.jena +import cats.instances.option.* +import cats.syntax.traverse.* import org.apache.jena.rdf.model.Property import org.apache.jena.rdf.model.Resource import org.apache.jena.rdf.model.Statement @@ -39,8 +41,14 @@ object ResourceOps { def objectInt(p: Property): Either[String, Int] = statement(p).flatMap(_.objectAsInt) def objectIntOption(p: Property): Either[String, Option[Int]] = fromStatement(p, _.objectAsInt) - def objectString(p: Property): Either[String, String] = statement(p).flatMap(_.objectAsString) - def objectStringOption(p: Property): Either[String, Option[String]] = fromStatement(p, _.objectAsString) + def objectString(p: Property): Either[String, String] = + statement(p).flatMap(_.objectAsString) + def objectString[A](p: Property, mapper: String => Either[String, A]): Either[String, A] = + objectString(p).flatMap(mapper) + def objectStringOption(p: Property): Either[String, Option[String]] = + fromStatement(p, _.objectAsString) + def objectStringOption[A](p: Property, mapper: String => Either[String, A]): Either[String, Option[A]] = + objectStringOption(p).flatMap(_.traverse(mapper)) def objectUri(p: Property): Either[String, String] = statement(p).flatMap(stmt => stmt.objectAsUri) def objectUriOption(p: Property): Either[String, Option[String]] = fromStatement(p, _.objectAsUri) @@ -52,6 +60,8 @@ object ResourceOps { statement(p).flatMap(stmt => stmt.objectAsDataType(dt)) def objectDataTypeOption(p: Property, dt: String): Either[String, Option[String]] = fromStatement(p, _.objectAsDataType(dt)) + def objectDataTypeOption[A](p: Property, dt: String, f: String => Either[String, A]): Either[String, Option[A]] = + objectDataTypeOption(p, dt).flatMap(_.traverse(f)) def rdfsType: Option[String] = Option(res.getPropertyResourceValue(RDF.`type`)).flatMap(_.uri) def uri: Option[String] = Option(res.getURI) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/repo/rdf/RdfModel.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/repo/rdf/RdfModel.scala index 084cd1793d..0beec8a01b 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/repo/rdf/RdfModel.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/repo/rdf/RdfModel.scala @@ -7,6 +7,7 @@ package org.knora.webapi.slice.common.repo.rdf import org.apache.jena.rdf.model.* import org.apache.jena.vocabulary.RDF +import org.eclipse.rdf4j.model.vocabulary.XSD import zio.* import java.io.StringReader @@ -115,6 +116,15 @@ final case class RdfResource(private val res: Resource) { domainObject <- ZIO.foreach(string)(str => ZIO.fromEither(mapper(str)).mapError(ConversionError.apply)) } yield domainObject + def getUriLiteral[A](propertyIri: String)(implicit mapper: String => Either[String, A]): IO[RdfError, Option[A]] = + import org.knora.webapi.slice.common.jena.ResourceOps.* + import org.knora.webapi.slice.common.jena.JenaConversions.given + import scala.language.implicitConversions + for { + prop <- ZIO.fromEither(res.objectDataTypeOption(propertyIri, XSD.ANYURI.toString)).mapError(ConversionError.apply) + domainObject <- ZIO.foreach(prop)(str => ZIO.fromEither(mapper(str)).mapError(ConversionError.apply)) + } yield domainObject + /** * Returns the value of a literal with a given predicate IRI as a domain object of type `A`, * provided an implicit function `String => Either[String, A]` to convert the string literal to the domain object. diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/repo/rdf/Vocabulary.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/repo/rdf/Vocabulary.scala index 471e2710a0..3ce5285908 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/repo/rdf/Vocabulary.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/repo/rdf/Vocabulary.scala @@ -109,7 +109,8 @@ object Vocabulary { val valueHasMaxStandoffStartIndex: Iri = iri(kb + "valueHasMaxStandoffStartIndex") val valueHasStandoff: Iri = iri(kb + "valueHasStandoff") val hasCopyrightAttribution: Iri = iri(kb + "hasCopyrightAttribution") - val hasLicense: Iri = iri(kb + "hasLicense") + val hasLicenseText: Iri = iri(kb + "hasLicenseText") + val hasLicenseUri: Iri = iri(kb + "hasLicenseUri") val internalFilename: Iri = iri(kb + "internalFilename") val internalMimeType: Iri = iri(kb + "internalMimeType") diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/repo/model/ResourceCreateModels.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/repo/model/ResourceCreateModels.scala index cbaddca6e5..e33aa9a2d6 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/repo/model/ResourceCreateModels.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/repo/model/ResourceCreateModels.scala @@ -11,7 +11,8 @@ import java.util.UUID import org.knora.webapi.messages.util.CalendarNameV2 import org.knora.webapi.messages.util.DatePrecisionV2 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.LicenseText +import org.knora.webapi.slice.admin.domain.model.KnoraProject.LicenseUri import org.knora.webapi.slice.resourceinfo.domain.InternalIri final case class ResourceReadyToCreate( @@ -50,7 +51,8 @@ sealed trait FileValueTypeSpecificInfo { def originalFilename: Option[String] def originalMimeType: Option[String] def copyrightAttribution: Option[CopyrightAttribution] - def license: Option[License] + def licenseText: Option[LicenseText] + def licenseUri: Option[LicenseUri] } enum TypeSpecificValueInfo { @@ -84,7 +86,8 @@ enum TypeSpecificValueInfo { dimX: Int, dimY: Int, copyrightAttribution: Option[CopyrightAttribution], - license: Option[License], + licenseText: Option[LicenseText], + licenseUri: Option[LicenseUri], ) extends TypeSpecificValueInfo with FileValueTypeSpecificInfo case StillImageExternalFileValueInfo( internalFilename: String, @@ -93,7 +96,8 @@ enum TypeSpecificValueInfo { originalMimeType: Option[String], externalUrl: String, copyrightAttribution: Option[CopyrightAttribution], - license: Option[License], + licenseText: Option[LicenseText], + licenseUri: Option[LicenseUri], ) extends TypeSpecificValueInfo with FileValueTypeSpecificInfo case DocumentFileValueInfo( internalFilename: String, @@ -104,7 +108,8 @@ enum TypeSpecificValueInfo { dimY: Option[Int], pageCount: Option[Int], copyrightAttribution: Option[CopyrightAttribution], - license: Option[License], + licenseText: Option[LicenseText], + licenseUri: Option[LicenseUri], ) extends TypeSpecificValueInfo with FileValueTypeSpecificInfo case OtherFileValueInfo( internalFilename: String, @@ -112,7 +117,8 @@ enum TypeSpecificValueInfo { originalFilename: Option[String], originalMimeType: Option[String], copyrightAttribution: Option[CopyrightAttribution], - license: Option[License], + licenseText: Option[LicenseText], + licenseUri: Option[LicenseUri], ) extends TypeSpecificValueInfo with FileValueTypeSpecificInfo case HierarchicalListValueInfo(valueHasListNode: InternalIri) case IntervalValueInfo(valueHasIntervalStart: BigDecimal, valueHasIntervalEnd: BigDecimal) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/repo/service/ResourcesRepoLive.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/repo/service/ResourcesRepoLive.scala index 7a0dc88f14..883d34351e 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/repo/service/ResourcesRepoLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/repo/service/ResourcesRepoLive.scala @@ -282,7 +282,8 @@ object ResourcesRepoLive { .andHasOptional(KB.originalFilename, v.originalFilename.map(literalOf)) .andHasOptional(KB.originalMimeType, v.originalMimeType.map(literalOf)) .andHasOptional(KB.hasCopyrightAttribution, v.copyrightAttribution.map(_.value).map(literalOf)) - .andHasOptional(KB.hasLicense, v.license.map(_.value).map(literalOf)) + .andHasOptional(KB.hasLicenseText, v.licenseText.map(_.value).map(literalOf)) + .andHasOptional(KB.hasLicenseUri, v.licenseUri.map(_.value).map(literalOfType(_, XSD.ANYURI))) v match { case _: OtherFileValueInfo => result diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/addValueVersion.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/addValueVersion.scala.txt index b1ccfef3f9..a06c833012 100644 --- a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/addValueVersion.scala.txt +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/addValueVersion.scala.txt @@ -241,9 +241,16 @@ DELETE { case None => {} } - @fileValueContentV2.fileValue.license match { - case Some(license) => { - <@newValueIri> knora-base:hasLicense """@license.value""" . + @fileValueContentV2.fileValue.licenseText match { + case Some(text) => { + <@newValueIri> knora-base:hasLicenseText """@text.value""" . + } + case None => {} + } + + @fileValueContentV2.fileValue.licenseUri match { + case Some(uri) => { + <@newValueIri> knora-base:hasLicenseUri """@uri.value"""^^xsd:anyURI . } case None => {} } diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/createValue.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/createValue.scala.txt index fa8a7b9486..0bb3c327c4 100644 --- a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/createValue.scala.txt +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/createValue.scala.txt @@ -235,9 +235,16 @@ DELETE { case None => {} } - @fileValueContentV2.fileValue.license match { - case Some(license) => { - <@newValueIri> knora-base:hasLicense """@license.value""" . + @fileValueContentV2.fileValue.licenseText match { + case Some(text) => { + <@newValueIri> knora-base:hasLicenseText """@text.value""" . + } + case None => {} + } + + @fileValueContentV2.fileValue.licenseUri match { + case Some(uri) => { + <@newValueIri> knora-base:hasLicenseUri """@uri.value"""^^xsd:anyURI . } case None => {} } diff --git a/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala b/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala index 982ff13950..21ea3787ee 100644 --- a/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala +++ b/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala @@ -100,6 +100,7 @@ object TestDataFactory { SelfJoin.CannotJoin, RestrictedView.default, Some(CopyrightAttribution.unsafeFrom("2024, Example Project")), - Some(License.unsafeFrom("CC-BY-4.0")), + Some(LicenseText.unsafeFrom("CC-BY-4.0")), + Some(LicenseUri.unsafeFrom("https://creativecommons.org/licenses/by/4.0/")), ) } diff --git a/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/model/KnoraProjectSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/model/KnoraProjectSpec.scala index 2dfd5a19c8..fe3c53b4e7 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/model/KnoraProjectSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/model/KnoraProjectSpec.scala @@ -28,6 +28,7 @@ object KnoraProjectSpec extends ZIOSpecDefault { logoTest, projectStatusTest, projectSelfJoinTest, + licenseUriTest, ) private val projectIriSuite = suite("ProjectIri")( @@ -192,4 +193,15 @@ object KnoraProjectSpec extends ZIOSpecDefault { assertTrue(SelfJoin.CanJoin.value, !SelfJoin.CannotJoin.value) }, ) + + private val licenseUriTest = suite("LicenseUri")( + test("pass a valid object and successfully create value object") { + val validUri = "https://www.apache.org/licenses/LICENSE-2.0.html" + assertTrue(LicenseUri.from(validUri).map(_.value).contains(validUri)) + }, + test("pass an invalid object and return an error") { + val invalidUri = "not a uri" + assertTrue(LicenseUri.from(invalidUri) == Left("License URI 'not a uri' is not a valid URI.")) + }, + ) } diff --git a/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectServiceSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectServiceSpec.scala index 030da89ce6..4d2d6c4f2e 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectServiceSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectServiceSpec.scala @@ -13,7 +13,8 @@ import org.knora.webapi.TestDataFactory import org.knora.webapi.messages.StringFormatter 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.LicenseText +import org.knora.webapi.slice.admin.domain.model.KnoraProject.LicenseUri import org.knora.webapi.slice.admin.domain.repo.KnoraProjectRepoInMemory import org.knora.webapi.slice.ontology.repo.service.OntologyRepoInMemory import org.knora.webapi.slice.resourceinfo.domain.IriConverter @@ -30,13 +31,15 @@ object KnoraProjectServiceSpec extends ZIOSpecDefault { _ <- repo(_.save(project)) updateRequest = ProjectUpdateRequest( copyrightAttribution = Some(CopyrightAttribution.unsafeFrom("Foo")), - license = Some(License.unsafeFrom("bar")), + licenseText = Some(LicenseText.unsafeFrom("CC BY 4.0")), + licenseUri = Some(LicenseUri.unsafeFrom("https://creativecommons.org/licenses/by/4.0/")), ) actual <- projectService(_.updateProject(project, updateRequest)) } yield assertTrue( actual == project.copy( copyrightAttribution = Some(CopyrightAttribution.unsafeFrom("Foo")), - license = Some(License.unsafeFrom("bar")), + licenseText = Some(LicenseText.unsafeFrom("CC BY 4.0")), + licenseUri = Some(LicenseUri.unsafeFrom("https://creativecommons.org/licenses/by/4.0/")), ), ) }, diff --git a/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/ProjectServiceSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/ProjectServiceSpec.scala index e6f842faa6..974404c509 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/ProjectServiceSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/ProjectServiceSpec.scala @@ -36,7 +36,8 @@ object ProjectServiceSpec extends ZIOSpecDefault { status = true, selfjoin = true, copyrightAttribution = None, - license = None, + licenseText = None, + licenseUri = None, ) assertTrue( ProjectService.projectDataNamedGraphV2(p).value == s"http://www.knora.org/data/$shortcode/$shortname", @@ -58,7 +59,8 @@ object ProjectServiceSpec extends ZIOSpecDefault { selfjoin = SelfJoin.CanJoin, restrictedView = RestrictedView.default, copyrightAttribution = None, - license = None, + licenseText = None, + licenseUri = None, ) assertTrue( ProjectService diff --git a/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLiveSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLiveSpec.scala index 1fd1ed296c..45f1cba405 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLiveSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLiveSpec.scala @@ -21,7 +21,8 @@ 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.Description import org.knora.webapi.slice.admin.domain.model.KnoraProject.Keyword -import org.knora.webapi.slice.admin.domain.model.KnoraProject.License +import org.knora.webapi.slice.admin.domain.model.KnoraProject.LicenseText +import org.knora.webapi.slice.admin.domain.model.KnoraProject.LicenseUri import org.knora.webapi.slice.admin.domain.model.KnoraProject.Logo import org.knora.webapi.slice.admin.domain.model.KnoraProject.Longname import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri @@ -48,13 +49,15 @@ object KnoraProjectRepoLiveSpec extends ZIOSpecDefault { SelfJoin.CannotJoin, RestrictedView.default, Some(CopyrightAttribution.unsafeFrom("2024, Example Project")), - Some(License.unsafeFrom("Apache-2.0")), + Some(LicenseText.unsafeFrom("Apache-2.0")), + Some(LicenseUri.unsafeFrom("https://www.apache.org/licenses/LICENSE-2.0.html")), ) private val someProjectTrig = s"""|@prefix owl: . |@prefix knora-base: . |@prefix knora-admin: . + |@prefix xsd: . | |<${AdminConstants.adminDataNamedGraph.value}> { | a knora-admin:knoraProject ; @@ -68,7 +71,8 @@ object KnoraProjectRepoLiveSpec extends ZIOSpecDefault { | knora-admin:hasSelfJoinEnabled false ; | knora-admin:projectRestrictedViewSize "!128,128" ; | knora-base:hasCopyrightAttribution "2024, Example Project" ; - | knora-base:hasLicense "Apache-2.0" . + | knora-base:hasLicenseText "Apache-2.0" ; + | knora-base:hasLicenseUri "https://www.apache.org/licenses/LICENSE-2.0.html"^^xsd:anyURI . |} |""".stripMargin @@ -80,8 +84,8 @@ object KnoraProjectRepoLiveSpec extends ZIOSpecDefault { test("save a project") { for { saved <- KnoraProjectRepo(_.save(someProject)) - project <- KnoraProjectRepo(_.findById(someProject.id)) - } yield assertTrue(project.contains(someProject), saved == someProject) + project <- KnoraProjectRepo(_.findById(someProject.id)).someOrFail(Exception("Project not found")) + } yield assertTrue(project == someProject, saved == someProject) }, test("die for built in projects") { check(Gen.fromIterable(builtInProjects)) { project => @@ -110,9 +114,10 @@ object KnoraProjectRepoLiveSpec extends ZIOSpecDefault { suite("findById")( test("return project if it exists") { for { - _ <- TriplestoreServiceInMemory.setDataSetFromTriG(someProjectTrig) - project <- KnoraProjectRepo(_.findById(ProjectIri.unsafeFrom("http://rdfh.ch/projects/1234"))) - } yield assertTrue(project == Some(someProject)) + _ <- TriplestoreServiceInMemory.setDataSetFromTriG(someProjectTrig) + actual <- KnoraProjectRepo(_.findById(ProjectIri.unsafeFrom("http://rdfh.ch/projects/1234"))) + .someOrFail(Exception("Project not found")) + } yield assertTrue(actual == someProject) }, test("return None if project does not exist") { for { @@ -122,17 +127,18 @@ object KnoraProjectRepoLiveSpec extends ZIOSpecDefault { test("should find all built in projects") { check(Gen.fromIterable(builtInProjects)) { project => for { - found <- KnoraProjectRepo(_.findById(project.id)) - } yield assertTrue(found.contains(project)) + actual <- KnoraProjectRepo(_.findById(project.id)).someOrFail(Exception("Project not found")) + } yield assertTrue(actual == project) } }, ), suite("find by Shortcode")( test("return project if it exists") { for { - _ <- TriplestoreServiceInMemory.setDataSetFromTriG(someProjectTrig) - project <- KnoraProjectRepo(_.findByShortcode(Shortcode.unsafeFrom("1234"))) - } yield assertTrue(project.contains(someProject)) + _ <- TriplestoreServiceInMemory.setDataSetFromTriG(someProjectTrig) + actual <- KnoraProjectRepo(_.findByShortcode(Shortcode.unsafeFrom("1234"))) + .someOrFail(Exception("Project not found")) + } yield assertTrue(actual == someProject) }, test("return None if project does not exist") { for { @@ -142,17 +148,19 @@ object KnoraProjectRepoLiveSpec extends ZIOSpecDefault { test("should find all built in projects") { check(Gen.fromIterable(builtInProjects)) { project => for { - found <- KnoraProjectRepo(_.findByShortcode(project.shortcode)) - } yield assertTrue(found.contains(project)) + actual <- KnoraProjectRepo(_.findByShortcode(project.shortcode)) + .someOrFail(Exception("Project not found")) + } yield assertTrue(actual == project) } }, ), suite("find by Shortname")( test("return project if it exists") { for { - _ <- TriplestoreServiceInMemory.setDataSetFromTriG(someProjectTrig) - project <- KnoraProjectRepo(_.findByShortname(Shortname.unsafeFrom("project1"))) - } yield assertTrue(project.contains(someProject)) + _ <- TriplestoreServiceInMemory.setDataSetFromTriG(someProjectTrig) + actual <- KnoraProjectRepo(_.findByShortname(Shortname.unsafeFrom("project1"))) + .someOrFail(Exception("Project not found")) + } yield assertTrue(actual == someProject) }, test("return None if project does not exist") { for { @@ -162,8 +170,9 @@ object KnoraProjectRepoLiveSpec extends ZIOSpecDefault { test("should find all built in projects") { check(Gen.fromIterable(builtInProjects)) { project => for { - found <- KnoraProjectRepo(_.findByShortname(project.shortname)) - } yield assertTrue(found.contains(project)) + actual <- + KnoraProjectRepo(_.findByShortname(project.shortname)).someOrFail(Exception("Project not found")) + } yield assertTrue(actual == project) } }, ), diff --git a/webapi/src/test/scala/org/knora/webapi/slice/common/ApiComplexV2JsonLdRequestParserSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/common/ApiComplexV2JsonLdRequestParserSpec.scala index 7715a6b827..f9817a1b2c 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/common/ApiComplexV2JsonLdRequestParserSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/common/ApiComplexV2JsonLdRequestParserSpec.scala @@ -83,6 +83,7 @@ object ApiComplexV2JsonLdRequestParserSpec extends ZIOSpecDefault { Some("originalMimeType"), None, None, + None, ) private val configureSipiServiceMock = for { @@ -475,6 +476,7 @@ object ApiComplexV2JsonLdRequestParserSpec extends ZIOSpecDefault { Some("originalMimeType"), None, None, + None, ), IiifImageRequestUrl.unsafeFrom("http://www.example.org/prefix1/abcd1234/full/0/native.jpg"), None, diff --git a/webapi/src/test/scala/org/knora/webapi/slice/resources/repo/service/ResourcesRepoLiveSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/resources/repo/service/ResourcesRepoLiveSpec.scala index 804eecf0ff..4e04b0ad32 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/resources/repo/service/ResourcesRepoLiveSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/resources/repo/service/ResourcesRepoLiveSpec.scala @@ -276,6 +276,7 @@ object TestData { dimY = 60, None, None, + None, ), permissions = valuePermissions, creator = valueCreator, @@ -300,6 +301,7 @@ object TestData { externalUrl = "http://example.com/foo.jpg", None, None, + None, ), permissions = valuePermissions, creator = valueCreator, @@ -326,6 +328,7 @@ object TestData { pageCount = Some(10), None, None, + None, ), permissions = valuePermissions, creator = valueCreator, @@ -349,6 +352,7 @@ object TestData { originalMimeType = Some("application/zip"), None, None, + None, ), permissions = valuePermissions, creator = valueCreator,