From 56846ff11803294ef8c488634b0a1d52e803ab0b Mon Sep 17 00:00:00 2001 From: Balduin Landolt <33053745+BalduinLandolt@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:58:04 +0200 Subject: [PATCH] refactor: Build queries with query builder (#3276) --- .../twirl/SparqlTemplateLinkUpdate.scala | 107 -- .../standoffmessages/StandoffMessagesV2.scala | 2 +- .../resources/CreateResourceV2Handler.scala | 97 +- .../slice/admin/repo/rdf/Vocabulary.scala | 3 - .../slice/common/repo/rdf/Vocabulary.scala | 74 + .../repo/model/ResourceCreateModels.scala | 136 ++ .../repo/service/ResourcesRepoLive.scala | 349 +++- .../sparql/v2/createNewResource.scala.txt | 257 --- .../repo/service/ResourcesRepoLiveSpec.scala | 1526 ++++++++++++++--- 9 files changed, 1852 insertions(+), 699 deletions(-) create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/common/repo/rdf/Vocabulary.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/resources/repo/model/ResourceCreateModels.scala delete mode 100644 webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/createNewResource.scala.txt diff --git a/webapi/src/main/scala/org/knora/webapi/messages/twirl/SparqlTemplateLinkUpdate.scala b/webapi/src/main/scala/org/knora/webapi/messages/twirl/SparqlTemplateLinkUpdate.scala index 04e1614589..8b3cf35c19 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/twirl/SparqlTemplateLinkUpdate.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/twirl/SparqlTemplateLinkUpdate.scala @@ -5,13 +5,8 @@ package org.knora.webapi.messages.twirl -import java.time.Instant -import java.util.UUID - import org.knora.webapi.IRI import org.knora.webapi.messages.SmartIri -import org.knora.webapi.messages.util.CalendarNameV2 -import org.knora.webapi.messages.util.DatePrecisionV2 /** * Contains instructions that can be given to a SPARQL template for updating direct links and `knora-base:LinkValue` @@ -51,105 +46,3 @@ case class SparqlTemplateLinkUpdate( newLinkValueCreator: IRI, newLinkValuePermissions: String, ) - -final case class NewLinkValueInfo( - linkPropertyIri: IRI, - newLinkValueIri: IRI, - linkTargetIri: IRI, - newReferenceCount: Int, - newLinkValueCreator: IRI, - newLinkValuePermissions: String, - valueUuid: String, -) - -final case class NewValueInfo( - resourceIri: IRI, - propertyIri: IRI, - valueIri: IRI, - valueTypeIri: IRI, - valueUUID: UUID, - value: TypeSpecificValueInfo, - valuePermissions: String, - valueCreator: IRI, - creationDate: Instant, - valueHasOrder: Int, - valueHasString: String, - comment: Option[String], -) - -final case class StandoffAttribute( - propertyIri: IRI, - value: String, -) - -final case class StandoffTagInfo( - standoffTagClassIri: IRI, - standoffTagInstanceIri: IRI, - startParentIri: Option[IRI], - endParentIri: Option[IRI], - uuid: UUID, - originalXMLID: Option[String], - startIndex: Int, - endIndex: Option[Int], - startPosition: Int, - endPosition: Int, - attributes: Seq[StandoffAttribute], -) - -enum TypeSpecificValueInfo { - case LinkValueInfo(referredResourceIri: IRI) - case UnformattedTextValueInfo(valueHasLanguage: Option[String]) - case FormattedTextValueInfo( - valueHasLanguage: Option[String], - mappingIri: IRI, - maxStandoffStartIndex: Int, - standoff: Seq[StandoffTagInfo], - ) - case IntegerValueInfo(valueHasInteger: Int) - case DecimalValueInfo(valueHasDecimal: BigDecimal) - case BooleanValueInfo(valueHasBoolean: Boolean) - case UriValueInfo(valueHasUri: String) - case DateValueInfo( - valueHasStartJDN: Int, - valueHasEndJDN: Int, - valueHasStartPrecision: DatePrecisionV2, - valueHasEndPrecision: DatePrecisionV2, - valueHasCalendar: CalendarNameV2, - ) - case ColorValueInfo(valueHasColor: String) - case GeomValueInfo(valueHasGeometry: String) - case StillImageFileValueInfo( - internalFilename: String, - internalMimeType: String, - originalFilename: Option[String], - originalMimeType: Option[String], - dimX: Int, - dimY: Int, - ) - case StillImageExternalFileValueInfo( - internalFilename: String, - internalMimeType: String, - originalFilename: Option[String], - originalMimeType: Option[String], - externalUrl: String, - ) - case DocumentFileValueInfo( - internalFilename: String, - internalMimeType: String, - originalFilename: Option[String], - originalMimeType: Option[String], - dimX: Option[Int], - dimY: Option[Int], - pageCount: Option[Int], - ) - case OtherFileValueInfo( - internalFilename: String, - internalMimeType: String, - originalFilename: Option[String], - originalMimeType: Option[String], - ) - case HierarchicalListValueInfo(valueHasListNode: IRI) - case IntervalValueInfo(valueHasIntervalStart: BigDecimal, valueHasIntervalEnd: BigDecimal) - case TimeValueInfo(valueHasTimeStamp: Instant) - case GeonameValueInfo(valueHasGeonameCode: String) -} diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/standoffmessages/StandoffMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/standoffmessages/StandoffMessagesV2.scala index 49a387fa78..257f1550eb 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/standoffmessages/StandoffMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/standoffmessages/StandoffMessagesV2.scala @@ -358,7 +358,7 @@ object StandoffProperties { /** * A trait representing an attribute attached to a standoff tag. */ -trait StandoffTagAttributeV2 extends KnoraContentV2[StandoffTagAttributeV2] { +sealed trait StandoffTagAttributeV2 extends KnoraContentV2[StandoffTagAttributeV2] { implicit protected val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance def standoffPropertyIri: SmartIri 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 c9411c859f..0b839cad53 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 @@ -21,11 +21,6 @@ import org.knora.webapi.messages.admin.responder.permissionsmessages.DefaultObje import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionADM import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionType import org.knora.webapi.messages.admin.responder.permissionsmessages.ResourceCreateOperation -import org.knora.webapi.messages.twirl.NewLinkValueInfo -import org.knora.webapi.messages.twirl.NewValueInfo -import org.knora.webapi.messages.twirl.StandoffAttribute -import org.knora.webapi.messages.twirl.StandoffTagInfo -import org.knora.webapi.messages.twirl.TypeSpecificValueInfo.* import org.knora.webapi.messages.util.* import org.knora.webapi.messages.util.PermissionUtilADM.AGreaterThanB import org.knora.webapi.messages.util.PermissionUtilADM.PermissionComparisonResult @@ -33,6 +28,7 @@ import org.knora.webapi.messages.util.standoff.StandoffTagUtilV2 import org.knora.webapi.messages.v2.responder.ontologymessages.* import org.knora.webapi.messages.v2.responder.ontologymessages.OwlCardinality.* import org.knora.webapi.messages.v2.responder.resourcemessages.* +import org.knora.webapi.messages.v2.responder.standoffmessages.* import org.knora.webapi.messages.v2.responder.valuemessages.* import org.knora.webapi.responders.IriLocker import org.knora.webapi.responders.IriService @@ -51,7 +47,14 @@ import org.knora.webapi.slice.ontology.domain.model.Cardinality.ZeroOrOne import org.knora.webapi.slice.ontology.domain.service.OntologyRepo import org.knora.webapi.slice.ontology.domain.service.OntologyService import org.knora.webapi.slice.ontology.domain.service.OntologyServiceLive -import org.knora.webapi.slice.resources.repo.service.ResourceReadyToCreate +import org.knora.webapi.slice.resourceinfo.domain.InternalIri +import org.knora.webapi.slice.resources.repo.model.ResourceReadyToCreate +import org.knora.webapi.slice.resources.repo.model.StandoffAttribute +import org.knora.webapi.slice.resources.repo.model.StandoffAttributeValue +import org.knora.webapi.slice.resources.repo.model.StandoffLinkValueInfo +import org.knora.webapi.slice.resources.repo.model.StandoffTagInfo +import org.knora.webapi.slice.resources.repo.model.TypeSpecificValueInfo.* +import org.knora.webapi.slice.resources.repo.model.ValueInfo import org.knora.webapi.slice.resources.repo.service.ResourcesRepo import org.knora.webapi.util.ZioHelper @@ -260,13 +263,13 @@ final case class CreateResourceV2Handler( _ <- resourcesRepo.createNewResource( dataGraphIri = dataNamedGraph, resource = resourceReadyToCreate, - projectIri = createResourceRequestV2.createResource.projectADM.id, - userIri = createResourceRequestV2.requestingUser.id, + projectIri = InternalIri(createResourceRequestV2.createResource.projectADM.id), + userIri = InternalIri(createResourceRequestV2.requestingUser.id), ) // Verify that the resource was created. previewOfCreatedResource <- verifyResource( - resourceIri = resourceReadyToCreate.resourceIri, + resourceIri = resourceReadyToCreate.resourceIri.value, requestingUser = createResourceRequestV2.requestingUser, ) } yield previewOfCreatedResource @@ -478,26 +481,44 @@ final case class CreateResourceV2Handler( val standoffInfo = tv .prepareForSparqlInsert(newValueIri) .map(standoffTag => + val attributes = standoffTag.standoffNode.attributes.map { attr => + val v = attr match + case StandoffTagIriAttributeV2(_, value, _) => + StandoffAttributeValue.IriAttribute(InternalIri(value)) + case StandoffTagUriAttributeV2(_, value) => StandoffAttributeValue.UriAttribute(value) + case StandoffTagInternalReferenceAttributeV2(_, value) => + StandoffAttributeValue.InternalReferenceAttribute(InternalIri(value)) + case StandoffTagStringAttributeV2(_, value) => StandoffAttributeValue.StringAttribute(value) + case StandoffTagIntegerAttributeV2(_, value) => StandoffAttributeValue.IntegerAttribute(value) + case StandoffTagDecimalAttributeV2(_, value) => StandoffAttributeValue.DecimalAttribute(value) + case StandoffTagBooleanAttributeV2(_, value) => StandoffAttributeValue.BooleanAttribute(value) + case StandoffTagTimeAttributeV2(_, value) => StandoffAttributeValue.TimeAttribute(value) + StandoffAttribute(InternalIri(attr.standoffPropertyIri.toString()), v) + } StandoffTagInfo( - standoffTagClassIri = standoffTag.standoffNode.standoffTagClassIri.toString(), - standoffTagInstanceIri = standoffTag.standoffTagInstanceIri, - startParentIri = standoffTag.startParentIri, - endParentIri = standoffTag.endParentIri, + standoffTagClassIri = InternalIri(standoffTag.standoffNode.standoffTagClassIri.toString()), + standoffTagInstanceIri = InternalIri(standoffTag.standoffTagInstanceIri), + startParentIri = standoffTag.startParentIri.map(InternalIri.apply), + endParentIri = standoffTag.endParentIri.map(InternalIri.apply), uuid = standoffTag.standoffNode.uuid, originalXMLID = standoffTag.standoffNode.originalXMLID, startIndex = standoffTag.standoffNode.startIndex, endIndex = standoffTag.standoffNode.endIndex, startPosition = standoffTag.standoffNode.startPosition, endPosition = standoffTag.standoffNode.endPosition, - attributes = standoffTag.standoffNode.attributes - .map(attr => StandoffAttribute(attr.standoffPropertyIri.toString(), attr.rdfValue)), + attributes = attributes, ), ) ZIO .fromOption(tv.computedMaxStandoffStartIndex) .orElseFail(StandoffInternalException("Max standoff start index not computed")) .map(standoffStartIndex => - FormattedTextValueInfo(valueHasLanguage, mappingIri, standoffStartIndex, standoffInfo), + FormattedTextValueInfo( + valueHasLanguage, + InternalIri(mappingIri), + standoffStartIndex, + standoffInfo, + ), ) case IntegerValueContentV2(_, valueHasInteger, _) => ZIO.succeed(IntegerValueInfo(valueHasInteger)) @@ -512,7 +533,7 @@ final case class CreateResourceV2Handler( case TimeValueContentV2(_, valueHasTimeStamp, _) => ZIO.succeed(TimeValueInfo(valueHasTimeStamp)) case HierarchicalListValueContentV2(_, valueHasListNode, listNodeLabel, _) => - ZIO.succeed(HierarchicalListValueInfo(valueHasListNode)) + ZIO.succeed(HierarchicalListValueInfo(InternalIri(valueHasListNode))) case ColorValueContentV2(_, valueHasColor, _) => ZIO.succeed(ColorValueInfo(valueHasColor)) case UriValueContentV2(_, valueHasUri, _) => @@ -596,32 +617,32 @@ final case class CreateResourceV2Handler( nestedResource, _, ) => - ZIO.succeed(LinkValueInfo(referredResourceIri)) + ZIO.succeed(LinkValueInfo(InternalIri(referredResourceIri))) case _: DeletedValueContentV2 => ZIO.fail(BadRequestException("Deleted values cannot be created")) - } yield NewValueInfo( - resourceIri = resourceIri, - propertyIri = propertyIri.toIri, + } yield ValueInfo( + resourceIri = InternalIri(resourceIri), + propertyIri = InternalIri(propertyIri.toIri), value = valueInfo, - valueIri = newValueIri, - valueTypeIri = valueToCreate.valueContent.valueType.toString(), + valueIri = InternalIri(newValueIri), + valueTypeIri = InternalIri(valueToCreate.valueContent.valueType.toString()), valueUUID = newValueUUID, - valueCreator = requestingUser.id, - valuePermissions = valueToCreate.permissions, + creator = InternalIri(requestingUser.id), + permissions = valueToCreate.permissions, creationDate = valueCreationDate, valueHasOrder = valueHasOrder, - valueHasString = valueToCreate.valueContent.valueHasString, + valueHasString = valueToCreate.valueContent.unescape.valueHasString, comment = valueToCreate.valueContent.comment, ) } } yield ResourceReadyToCreate( - resourceIri = resourceIri, - resourceClassIri = internalCreateResource.resourceClassIri.toString, + resourceIri = InternalIri(resourceIri), + resourceClassIri = InternalIri(internalCreateResource.resourceClassIri.toString), resourceLabel = internalCreateResource.label, creationDate = creationDate, permissions = resourcePermissions, - newValueInfos = newValueInfos, - linkUpdates = linkUpdates, + valueInfos = newValueInfos, + standoffLinks = linkUpdates, ) } @@ -947,7 +968,7 @@ final case class CreateResourceV2Handler( private def generateInsertSparqlForStandoffLinksInMultipleValues( resourceIri: IRI, values: Iterable[GenerateSparqlForValueInNewResourceV2], - ): Task[Seq[NewLinkValueInfo]] = { + ): Task[Seq[StandoffLinkValueInfo]] = { // To create LinkValues for the standoff links in the values to be created, we need to compute // the initial reference count of each LinkValue. This is equal to the number of TextValues in the resource // that have standoff links to a particular target resource. @@ -983,23 +1004,23 @@ final case class CreateResourceV2Handler( // For each standoff link target IRI, construct a SparqlTemplateLinkUpdate to create a hasStandoffLinkTo property // and one LinkValue with its initial reference count. - val standoffLinkUpdatesFutures: Seq[Task[NewLinkValueInfo]] = initialReferenceCounts.toSeq.map { + val standoffLinkUpdatesFutures: Seq[Task[StandoffLinkValueInfo]] = initialReferenceCounts.toSeq.map { case (targetIri, initialReferenceCount) => for { newValueIri <- makeUnusedValueIri(resourceIri) - } yield NewLinkValueInfo( - linkPropertyIri = OntologyConstants.KnoraBase.HasStandoffLinkTo, - newLinkValueIri = newValueIri, - linkTargetIri = targetIri, + } yield StandoffLinkValueInfo( + linkPropertyIri = InternalIri(OntologyConstants.KnoraBase.HasStandoffLinkTo), + newLinkValueIri = InternalIri(newValueIri), + linkTargetIri = InternalIri(targetIri), newReferenceCount = initialReferenceCount, - newLinkValueCreator = KnoraUserRepo.builtIn.SystemUser.id.value, + newLinkValueCreator = InternalIri(KnoraUserRepo.builtIn.SystemUser.id.value), newLinkValuePermissions = standoffLinkValuePermissions, valueUuid = UuidUtil.makeRandomBase64EncodedUuid, ) } ZIO.collectAll(standoffLinkUpdatesFutures) } else { - ZIO.succeed(Seq.empty[NewLinkValueInfo]) + ZIO.succeed(Seq.empty[StandoffLinkValueInfo]) } } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/rdf/Vocabulary.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/rdf/Vocabulary.scala index 7133d976d4..386f31b707 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/rdf/Vocabulary.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/rdf/Vocabulary.scala @@ -65,9 +65,6 @@ object Vocabulary { object KnoraBase { val NS: Namespace = new SimpleNamespace("knora-base", KnoraBasePrefixExpansion) - // property IRIs - val attachedToProject: Iri = Rdf.iri(KnoraBasePrefixExpansion, "attachedToProject") - val hasPermissions: Iri = Rdf.iri(KnoraBasePrefixExpansion, "hasPermissions") } 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 new file mode 100644 index 0000000000..26bcaea090 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/repo/rdf/Vocabulary.scala @@ -0,0 +1,74 @@ +/* + * Copyright © 2021 - 2024 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.common.repo.rdf + +import org.eclipse.rdf4j.model.Namespace +import org.eclipse.rdf4j.model.impl.SimpleNamespace +import org.eclipse.rdf4j.sparqlbuilder.rdf.Rdf.iri + +object Vocabulary { + + object KnoraBase { + private val kb = "http://www.knora.org/ontology/knora-base#" + + val NS: Namespace = new SimpleNamespace("knora-base", kb) + + val linkValue = iri(kb + "LinkValue") + + val isDeleted = iri(kb + "isDeleted") + val attachedToUser = iri(kb + "attachedToUser") + val attachedToProject = iri(kb + "attachedToProject") + val hasPermissions = iri(kb + "hasPermissions") + val creationDate = iri(kb + "creationDate") + + val valueHasString = iri(kb + "valueHasString") + val valueHasUUID = iri(kb + "valueHasUUID") + val valueHasComment = iri(kb + "valueHasComment") + val valueHasOrder = iri(kb + "valueHasOrder") + val valueCreationDate = iri(kb + "valueCreationDate") + + val valueHasInteger = iri(kb + "valueHasInteger") + val valueHasBoolean = iri(kb + "valueHasBoolean") + val valueHasDecimal = iri(kb + "valueHasDecimal") + val valueHasUri = iri(kb + "valueHasUri") + val valueHasStartJDN = iri(kb + "valueHasStartJDN") + val valueHasEndJDN = iri(kb + "valueHasEndJDN") + val valueHasStartPrecision = iri(kb + "valueHasStartPrecision") + val valueHasEndPrecision = iri(kb + "valueHasEndPrecision") + val valueHasCalendar = iri(kb + "valueHasCalendar") + val valueHasColor = iri(kb + "valueHasColor") + val valueHasGeometry = iri(kb + "valueHasGeometry") + val valueHasListNode = iri(kb + "valueHasListNode") + val valueHasIntervalStart = iri(kb + "valueHasIntervalStart") + val valueHasIntervalEnd = iri(kb + "valueHasIntervalEnd") + val valueHasTimeStamp = iri(kb + "valueHasTimeStamp") + val valueHasGeonameCode = iri(kb + "valueHasGeonameCode") + val valueHasRefCount = iri(kb + "valueHasRefCount") + val valueHasLanguage = iri(kb + "valueHasLanguage") + val valueHasMapping = iri(kb + "valueHasMapping") + val valueHasMaxStandoffStartIndex = iri(kb + "valueHasMaxStandoffStartIndex") + val valueHasStandoff = iri(kb + "valueHasStandoff") + + val internalFilename = iri(kb + "internalFilename") + val internalMimeType = iri(kb + "internalMimeType") + val originalFilename = iri(kb + "originalFilename") + val originalMimeType = iri(kb + "originalMimeType") + val dimX = iri(kb + "dimX") + val dimY = iri(kb + "dimY") + val externalUrl = iri(kb + "externalUrl") + val pageCount = iri(kb + "pageCount") + + val standoffTagHasStartIndex = iri(kb + "standoffTagHasStartIndex") + val standoffTagHasEndIndex = iri(kb + "standoffTagHasEndIndex") + val standoffTagHasStartParent = iri(kb + "standoffTagHasStartParent") + val standoffTagHasEndParent = iri(kb + "standoffTagHasEndParent") + val standoffTagHasOriginalXMLID = iri(kb + "standoffTagHasOriginalXMLID") + val standoffTagHasUUID = iri(kb + "standoffTagHasUUID") + val standoffTagHasStart = iri(kb + "standoffTagHasStart") + val standoffTagHasEnd = iri(kb + "standoffTagHasEnd") + + } +} 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 new file mode 100644 index 0000000000..fedb8380bd --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/repo/model/ResourceCreateModels.scala @@ -0,0 +1,136 @@ +/* + * Copyright © 2021 - 2024 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.resources.repo.model + +import java.time.Instant +import java.util.UUID + +import org.knora.webapi.messages.util.CalendarNameV2 +import org.knora.webapi.messages.util.DatePrecisionV2 +import org.knora.webapi.slice.resourceinfo.domain.InternalIri + +final case class ResourceReadyToCreate( + resourceIri: InternalIri, + resourceClassIri: InternalIri, + resourceLabel: String, + creationDate: Instant, + permissions: String, + valueInfos: Seq[ValueInfo], + standoffLinks: Seq[StandoffLinkValueInfo], +) + +final case class ValueInfo( + resourceIri: InternalIri, + propertyIri: InternalIri, + valueIri: InternalIri, + valueTypeIri: InternalIri, + valueUUID: UUID, + value: TypeSpecificValueInfo, + permissions: String, + creator: InternalIri, + creationDate: Instant, + valueHasOrder: Int, + valueHasString: String, + comment: Option[String], +) + +enum TypeSpecificValueInfo { + case LinkValueInfo(referredResourceIri: InternalIri) + case UnformattedTextValueInfo(valueHasLanguage: Option[String]) + case FormattedTextValueInfo( + valueHasLanguage: Option[String], + mappingIri: InternalIri, + maxStandoffStartIndex: Int, + standoff: Seq[StandoffTagInfo], + ) + case IntegerValueInfo(valueHasInteger: Int) + case DecimalValueInfo(valueHasDecimal: BigDecimal) + case BooleanValueInfo(valueHasBoolean: Boolean) + case UriValueInfo(valueHasUri: String) + case DateValueInfo( + valueHasStartJDN: Int, + valueHasEndJDN: Int, + valueHasStartPrecision: DatePrecisionV2, + valueHasEndPrecision: DatePrecisionV2, + valueHasCalendar: CalendarNameV2, + ) + case ColorValueInfo(valueHasColor: String) + case GeomValueInfo(valueHasGeometry: String) + case StillImageFileValueInfo( + internalFilename: String, + internalMimeType: String, + originalFilename: Option[String], + originalMimeType: Option[String], + dimX: Int, + dimY: Int, + ) + case StillImageExternalFileValueInfo( + internalFilename: String, + internalMimeType: String, + originalFilename: Option[String], + originalMimeType: Option[String], + externalUrl: String, + ) + case DocumentFileValueInfo( + internalFilename: String, + internalMimeType: String, + originalFilename: Option[String], + originalMimeType: Option[String], + dimX: Option[Int], + dimY: Option[Int], + pageCount: Option[Int], + ) + case OtherFileValueInfo( + internalFilename: String, + internalMimeType: String, + originalFilename: Option[String], + originalMimeType: Option[String], + ) + case HierarchicalListValueInfo(valueHasListNode: InternalIri) + case IntervalValueInfo(valueHasIntervalStart: BigDecimal, valueHasIntervalEnd: BigDecimal) + case TimeValueInfo(valueHasTimeStamp: Instant) + case GeonameValueInfo(valueHasGeonameCode: String) +} + +final case class StandoffLinkValueInfo( + linkPropertyIri: InternalIri, + newLinkValueIri: InternalIri, + linkTargetIri: InternalIri, + newReferenceCount: Int, + newLinkValueCreator: InternalIri, + newLinkValuePermissions: String, + valueUuid: String, +) + +enum StandoffAttributeValue { + case IriAttribute(value: InternalIri) + case UriAttribute(value: String) + case InternalReferenceAttribute(value: InternalIri) + case StringAttribute(value: String) + case IntegerAttribute(value: Int) + case DecimalAttribute(value: BigDecimal) + case BooleanAttribute(value: Boolean) + case TimeAttribute(value: Instant) +} + +final case class StandoffAttribute( + propertyIri: InternalIri, + value: StandoffAttributeValue, +) + +final case class StandoffTagInfo( + standoffTagClassIri: InternalIri, + standoffTagInstanceIri: InternalIri, + startParentIri: Option[InternalIri], + endParentIri: Option[InternalIri], + uuid: UUID, + originalXMLID: Option[String], + startIndex: Int, + endIndex: Option[Int], + startPosition: Int, + endPosition: Int, + attributes: Seq[StandoffAttribute], +) 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 9d5f1f96c4..87a755cde5 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 @@ -5,34 +5,44 @@ package org.knora.webapi.slice.resources.repo.service +import org.eclipse.rdf4j.model.Namespace +import org.eclipse.rdf4j.model.vocabulary.RDF +import org.eclipse.rdf4j.model.vocabulary.RDFS +import org.eclipse.rdf4j.model.vocabulary.XSD +import org.eclipse.rdf4j.sparqlbuilder.core.query.InsertDataQuery +import org.eclipse.rdf4j.sparqlbuilder.core.query.Queries +import org.eclipse.rdf4j.sparqlbuilder.graphpattern.TriplePattern +import org.eclipse.rdf4j.sparqlbuilder.rdf.Iri +import org.eclipse.rdf4j.sparqlbuilder.rdf.Rdf.iri +import org.eclipse.rdf4j.sparqlbuilder.rdf.Rdf.literalOf +import org.eclipse.rdf4j.sparqlbuilder.rdf.Rdf.literalOfType +import org.eclipse.rdf4j.sparqlbuilder.rdf.Rdf.predicateObjectList +import org.eclipse.rdf4j.sparqlbuilder.rdf.RdfObject +import org.eclipse.rdf4j.sparqlbuilder.rdf.RdfPredicate +import org.eclipse.rdf4j.sparqlbuilder.rdf.RdfPredicateObjectList import zio.* import java.time.Instant -import dsp.constants.SalsahGui.IRI -import org.knora.webapi.messages.twirl.NewLinkValueInfo -import org.knora.webapi.messages.twirl.NewValueInfo -import org.knora.webapi.messages.twirl.queries.sparql +import dsp.valueobjects.UuidUtil +import org.knora.webapi.slice.common.repo.rdf.Vocabulary.KnoraBase as KB import org.knora.webapi.slice.resourceinfo.domain.InternalIri +import org.knora.webapi.slice.resources.repo.model.ResourceReadyToCreate +import org.knora.webapi.slice.resources.repo.model.StandoffAttribute +import org.knora.webapi.slice.resources.repo.model.StandoffAttributeValue +import org.knora.webapi.slice.resources.repo.model.StandoffLinkValueInfo +import org.knora.webapi.slice.resources.repo.model.TypeSpecificValueInfo +import org.knora.webapi.slice.resources.repo.model.TypeSpecificValueInfo.* +import org.knora.webapi.slice.resources.repo.model.ValueInfo import org.knora.webapi.store.triplestore.api.TriplestoreService import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Update -case class ResourceReadyToCreate( - resourceIri: IRI, - resourceClassIri: IRI, - resourceLabel: String, - creationDate: Instant, - permissions: String, - newValueInfos: Seq[NewValueInfo], - linkUpdates: Seq[NewLinkValueInfo], -) - trait ResourcesRepo { def createNewResource( dataGraphIri: InternalIri, resource: ResourceReadyToCreate, - userIri: IRI, - projectIri: IRI, + userIri: InternalIri, + projectIri: InternalIri, ): Task[Unit] } @@ -41,17 +51,10 @@ final case class ResourcesRepoLive(triplestore: TriplestoreService) extends Reso def createNewResource( dataGraphIri: InternalIri, resource: ResourceReadyToCreate, - userIri: IRI, - projectIri: IRI, + userIri: InternalIri, + projectIri: InternalIri, ): Task[Unit] = - triplestore.query( - ResourcesRepoLive.createNewResourceQuery( - dataGraphIri, - resource, - projectIri, - userIri, - ), - ) + triplestore.query(ResourcesRepoLive.createNewResourceQuery(dataGraphIri, resource, projectIri, userIri)) } @@ -61,21 +64,283 @@ object ResourcesRepoLive { private[service] def createNewResourceQuery( dataGraphIri: InternalIri, resourceToCreate: ResourceReadyToCreate, - projectIri: IRI, - creatorIri: IRI, - ): Update = - Update( - sparql.v2.txt.createNewResource( - dataNamedGraph = dataGraphIri.value, - projectIri = projectIri, - creatorIri = creatorIri, - creationDate = resourceToCreate.creationDate, - resourceIri = resourceToCreate.resourceIri, - resourceClassIri = resourceToCreate.resourceClassIri, - resourceLabel = resourceToCreate.resourceLabel, - permissions = resourceToCreate.permissions, - linkUpdates = resourceToCreate.linkUpdates, - newValueInfos = resourceToCreate.newValueInfos, - ), + projectIri: InternalIri, + creatorIri: InternalIri, + ) = { + val query: InsertDataQuery = + Queries + .INSERT_DATA() + .into(iri(dataGraphIri.value)) + .prefix(KB.NS, RDF.NS, RDFS.NS, XSD.NS) + + val resourcePattern = CreateResourceQueryBuilder.buildResourcePattern(resourceToCreate, projectIri, creatorIri) + query.insertData(resourcePattern) + + val valuePatterns = resourceToCreate.valueInfos.flatMap( + CreateResourceQueryBuilder.buildValuePattern(_, resourceToCreate.resourceIri), ) + query.insertData(valuePatterns: _*) + + val standoffLinkPatterns = resourceToCreate.standoffLinks.flatMap { standoffLink => + CreateResourceQueryBuilder.buildStandoffLinkPattern( + standoffLink, + resourceToCreate.resourceIri, + resourceToCreate.creationDate, + ) + } + query.insertData(standoffLinkPatterns: _*) + + Update(query.getQueryString()) + } + + private object CreateResourceQueryBuilder { + def buildResourcePattern( + resource: ResourceReadyToCreate, + projectIri: InternalIri, + creatorIri: InternalIri, + ): TriplePattern = + iri(resource.resourceIri.value) + .isA(iri(resource.resourceClassIri.value)) + .andHas(RDFS.LABEL, literalOf(resource.resourceLabel)) + .andHas(KB.isDeleted, literalOf(false)) + .andHas(KB.attachedToUser, iri(creatorIri.value)) + .andHas(KB.attachedToProject, iri(projectIri.value)) + .andHas(KB.hasPermissions, literalOf(resource.permissions)) + .andHas(KB.creationDate, literalOfType(resource.creationDate.toString(), XSD.DATETIME)) + + def buildStandoffLinkPattern( + standoffLink: StandoffLinkValueInfo, + resourceIri: InternalIri, + resourceCreationDate: Instant, + ): List[TriplePattern] = + List( + iri(resourceIri.value) + .has(iri(standoffLink.linkPropertyIri.value), iri(standoffLink.linkTargetIri.value)) + .andHas(iri(standoffLink.linkPropertyIri.value + "Value"), iri(standoffLink.newLinkValueIri.value)), + buildStandoffLinkPatternContent(standoffLink, resourceIri, resourceCreationDate), + ) + + private def buildStandoffLinkPatternContent( + standoffLink: StandoffLinkValueInfo, + resourceIri: InternalIri, + resourceCreationDate: Instant, + ): TriplePattern = + iri(standoffLink.newLinkValueIri.value) + .isA(KB.linkValue) + .andHas(RDF.SUBJECT, iri(resourceIri.value)) + .andHas(RDF.PREDICATE, iri(standoffLink.linkPropertyIri.value)) + .andHas(RDF.OBJECT, iri(standoffLink.linkTargetIri.value)) + .andHas(KB.valueHasString, literalOf(standoffLink.linkTargetIri.value)) + .andHas(KB.valueHasRefCount, literalOf(standoffLink.newReferenceCount)) + .andHas(KB.isDeleted, literalOf(false)) + .andHas(KB.valueCreationDate, literalOfType(resourceCreationDate.toString(), XSD.DATETIME)) + .andHas(KB.attachedToUser, iri(standoffLink.newLinkValueCreator.value)) + .andHas(KB.hasPermissions, literalOf(standoffLink.newLinkValuePermissions)) + .andHas(KB.valueHasUUID, literalOf(standoffLink.valueUuid)) + + def buildValuePattern(valueInfo: ValueInfo, resourceIri: InternalIri): List[TriplePattern] = + List( + iri(resourceIri.value).has(iri(valueInfo.propertyIri.value), iri(valueInfo.valueIri.value)), + buildGeneralValuePattern(valueInfo), + ) ::: + buildTypeSpecificValuePattern( + valueInfo.value, + valueInfo.valueIri.value, + valueInfo.propertyIri.value, + resourceIri.value, + ) + + private def buildGeneralValuePattern(valueInfo: ValueInfo): TriplePattern = + iri(valueInfo.valueIri.value) + .isA(iri(valueInfo.valueTypeIri.value)) + .andHas(KB.isDeleted, literalOf(false)) + .andHas(KB.valueHasString, literalOf(valueInfo.valueHasString)) + .andHas(KB.valueHasUUID, literalOf(UuidUtil.base64Encode(valueInfo.valueUUID))) + .andHas(KB.attachedToUser, iri(valueInfo.creator.value)) + .andHas(KB.hasPermissions, literalOf(valueInfo.permissions)) + .andHas(KB.valueHasOrder, literalOf(valueInfo.valueHasOrder)) + .andHas(KB.valueCreationDate, literalOfType(valueInfo.creationDate.toString(), XSD.DATETIME)) + .andHasOptional(KB.valueHasComment, valueInfo.comment.map(literalOf)) + + private def buildTypeSpecificValuePattern( + value: TypeSpecificValueInfo, + valueIri: String, + propertyIri: String, + resourceIri: String, + ): List[TriplePattern] = + value match + case v: LinkValueInfo => + buildLinkValuePatterns(v, valueIri, propertyIri, resourceIri) + case UnformattedTextValueInfo(valueHasLanguage) => + iri(valueIri).hasOptional(KB.valueHasLanguage, valueHasLanguage.map(literalOf)).toList + case v: FormattedTextValueInfo => + buildFormattedTextValuePatterns(v, valueIri) + case IntegerValueInfo(valueHasInteger) => + List(iri(valueIri).has(KB.valueHasInteger, literalOf(valueHasInteger))) + case DecimalValueInfo(valueHasDecimal) => + List(iri(valueIri).has(KB.valueHasDecimal, literalOf(valueHasDecimal))) + case BooleanValueInfo(valueHasBoolean) => + List(iri(valueIri).has(KB.valueHasBoolean, literalOf(valueHasBoolean))) + case UriValueInfo(valueHasUri) => + List(iri(valueIri).has(KB.valueHasUri, literalOfType(valueHasUri, XSD.ANYURI))) + case v: DateValueInfo => + buildDateValuePattern(v, valueIri) + case ColorValueInfo(valueHasColor) => + List(iri(valueIri).has(KB.valueHasColor, literalOf(valueHasColor))) + case GeomValueInfo(valueHasGeometry) => + List(iri(valueIri).has(KB.valueHasGeometry, literalOf(valueHasGeometry))) + case v: StillImageFileValueInfo => + buildStillImageFileValuePattern(v, valueIri) + case v: StillImageExternalFileValueInfo => + buildStillImageExternalFileValuePattern(v, valueIri) + case v: DocumentFileValueInfo => + buildDocumentFileValuePattern(v, valueIri) + case v: OtherFileValueInfo => + buildOtherFileValuePattern(v, valueIri) + case HierarchicalListValueInfo(valueHasListNode) => + List(iri(valueIri).has(KB.valueHasListNode, iri(valueHasListNode.value))) + case IntervalValueInfo(valueHasIntervalStart, valueHasIntervalEnd) => + List( + iri(valueIri) + .has(KB.valueHasIntervalStart, literalOf(valueHasIntervalStart)) + .andHas(KB.valueHasIntervalEnd, literalOf(valueHasIntervalEnd)), + ) + case TimeValueInfo(valueHasTimeStamp) => + List(iri(valueIri).has(KB.valueHasTimeStamp, literalOfType(valueHasTimeStamp.toString(), XSD.DATETIME))) + case GeonameValueInfo(valueHasGeonameCode) => + List(iri(valueIri).has(KB.valueHasGeonameCode, literalOf(valueHasGeonameCode))) + + private def buildLinkValuePatterns( + v: LinkValueInfo, + valueIri: String, + propertyIri: String, + resourceIri: String, + ): List[TriplePattern] = + List( + iri(resourceIri).has(iri(propertyIri.stripSuffix("Value")), iri(v.referredResourceIri.value)), + iri(valueIri) + .has(RDF.SUBJECT, iri(resourceIri)) + .andHas(RDF.PREDICATE, iri(propertyIri.stripSuffix("Value"))) + .andHas(RDF.OBJECT, iri(v.referredResourceIri.value)) + .andHas(KB.valueHasRefCount, literalOf(1)), + ) + + private def buildFormattedTextValuePatterns(v: FormattedTextValueInfo, valueIri: String): List[TriplePattern] = + val valuePattern = + iri(valueIri) + .has(KB.valueHasMapping, iri(v.mappingIri.value)) + .andHas(KB.valueHasMaxStandoffStartIndex, literalOf(v.maxStandoffStartIndex)) + .andHasOptional(KB.valueHasLanguage, v.valueHasLanguage.map(literalOf)) + List(valuePattern) ::: v.standoff.map { standoffTagInfo => + valuePattern.andHas(KB.valueHasStandoff, iri(standoffTagInfo.standoffTagInstanceIri.value)) + iri(standoffTagInfo.standoffTagInstanceIri.value) + .isA(iri(standoffTagInfo.standoffTagClassIri.value)) + .andHasOptional(KB.standoffTagHasEndIndex, standoffTagInfo.endIndex.map(i => literalOf(i))) + .andHasOptional(KB.standoffTagHasStartParent, standoffTagInfo.startParentIri.map(v => iri(v.value))) + .andHasOptional(KB.standoffTagHasEndParent, standoffTagInfo.endParentIri.map(v => iri(v.value))) + .andHasOptional(KB.standoffTagHasOriginalXMLID, standoffTagInfo.originalXMLID.map(literalOf)) + .andHas(standoffAttributeLiterals(standoffTagInfo.attributes): _*) + .andHas(KB.standoffTagHasStartIndex, literalOf(standoffTagInfo.startIndex)) + .andHas(KB.standoffTagHasUUID, literalOf(UuidUtil.base64Encode(standoffTagInfo.uuid))) + .andHas(KB.standoffTagHasStart, literalOf(standoffTagInfo.startPosition)) + .andHas(KB.standoffTagHasEnd, literalOf(standoffTagInfo.endPosition)) + }.toList + + private def standoffAttributeLiterals(attributes: Seq[StandoffAttribute]): List[RdfPredicateObjectList] = + attributes.map { attribute => + val v = attribute.value match + case StandoffAttributeValue.IriAttribute(value) => iri(value.value) + case StandoffAttributeValue.UriAttribute(value) => literalOfType(value, XSD.ANYURI) + case StandoffAttributeValue.InternalReferenceAttribute(value) => iri(value.value) + case StandoffAttributeValue.StringAttribute(value) => literalOf(value) + case StandoffAttributeValue.IntegerAttribute(value) => literalOf(value) + case StandoffAttributeValue.DecimalAttribute(value) => literalOf(value) + case StandoffAttributeValue.BooleanAttribute(value) => literalOf(value) + case StandoffAttributeValue.TimeAttribute(value) => literalOfType(value.toString(), XSD.DATETIME) + val p = iri(attribute.propertyIri.value) + predicateObjectList(p, v) + }.toList + + private def buildDateValuePattern(v: DateValueInfo, valueIri: String): List[TriplePattern] = + List( + iri(valueIri) + .has(KB.valueHasStartJDN, literalOf(v.valueHasStartJDN)) + .andHas(KB.valueHasEndJDN, literalOf(v.valueHasEndJDN)) + .andHas(KB.valueHasStartPrecision, literalOf(v.valueHasStartPrecision.toString())) + .andHas(KB.valueHasEndPrecision, literalOf(v.valueHasEndPrecision.toString())) + .andHas(KB.valueHasCalendar, literalOf(v.valueHasCalendar.toString())), + ) + + private def buildStillImageFileValuePattern(v: StillImageFileValueInfo, valueIri: String): List[TriplePattern] = + List( + iri(valueIri) + .has(KB.internalFilename, literalOf(v.internalFilename)) + .andHas(KB.internalMimeType, literalOf(v.internalMimeType)) + .andHas(KB.dimX, literalOf(v.dimX)) + .andHas(KB.dimY, literalOf(v.dimY)) + .andHasOptional(KB.originalFilename, v.originalFilename.map(literalOf)) + .andHasOptional(KB.originalMimeType, v.originalMimeType.map(literalOf)), + ) + + private def buildStillImageExternalFileValuePattern( + v: StillImageExternalFileValueInfo, + valueIri: String, + ): List[TriplePattern] = + List( + iri(valueIri) + .has(KB.internalFilename, literalOf(v.internalFilename)) + .andHas(KB.internalMimeType, literalOf(v.internalMimeType)) + .andHas(KB.externalUrl, literalOf(v.externalUrl)) + .andHasOptional(KB.originalFilename, v.originalFilename.map(literalOf)) + .andHasOptional(KB.originalMimeType, v.originalMimeType.map(literalOf)), + ) + + private def buildDocumentFileValuePattern(v: DocumentFileValueInfo, valueIri: String): List[TriplePattern] = + List( + iri(valueIri) + .has(KB.internalFilename, literalOf(v.internalFilename)) + .andHas(KB.internalMimeType, literalOf(v.internalMimeType)) + .andHasOptional(KB.originalFilename, v.originalFilename.map(literalOf)) + .andHasOptional(KB.originalMimeType, v.originalMimeType.map(literalOf)) + .andHasOptional(KB.dimX, v.dimX.map(i => literalOf(i))) + .andHasOptional(KB.dimY, v.dimY.map(i => literalOf(i))) + .andHasOptional(KB.pageCount, v.pageCount.map(i => literalOf(i))), + ) + + private def buildOtherFileValuePattern(v: OtherFileValueInfo, valueIri: String): List[TriplePattern] = + List( + iri(valueIri) + .has(KB.internalFilename, literalOf(v.internalFilename)) + .andHas(KB.internalMimeType, literalOf(v.internalMimeType)) + .andHasOptional(KB.originalFilename, v.originalFilename.map(literalOf)) + .andHasOptional(KB.originalMimeType, v.originalMimeType.map(literalOf)), + ) + + } } + +/** + * Extends the `TriplePattern` class to add the `andHasOptional` method. + * + * This method allows adding an optional triple pattern to the existing triple pattern. + * + * @param p The RDF predicate of the optional triple pattern. + * @param o The RDF object of the optional triple pattern. + * @return A new `TriplePattern` object that represents the combined triple pattern. + */ +extension (tp: TriplePattern) + def andHasOptional(p: RdfPredicate, o: Option[RdfObject]): TriplePattern = + o.fold(tp)(o => tp.andHas(p, o)) + +/** + * Extends the `Iri` class to add the `hasOptional` method. + * + * This method allows optionally creating a triple if the object is defined. + * + * @param p The RDF predicate of the optional triple pattern. + * @param o The RDF object of the optional triple pattern. + * @return An optional `TriplePattern` object that represents the combined triple pattern. + */ +extension (iri: Iri) + def hasOptional(p: RdfPredicate, o: Option[RdfObject]): Option[TriplePattern] = + o.map(o => iri.has(p, o)) diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/createNewResource.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/createNewResource.scala.txt deleted file mode 100644 index 01362fb2e6..0000000000 --- a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/createNewResource.scala.txt +++ /dev/null @@ -1,257 +0,0 @@ -@* - * Copyright © 2021 - 2024 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - *@ - -@import java.time.Instant -@import org.knora.webapi.IRI -@import org.knora.webapi.messages.twirl._ -@import org.knora.webapi.messages.twirl.TypeSpecificValueInfo._ -@import dsp.valueobjects.UuidUtil - -@** - * Creates a new resource. - *@ -@(dataNamedGraph: IRI, - projectIri: IRI, - creatorIri: IRI, - creationDate: Instant, - resourceIri: IRI, - resourceClassIri: IRI, - resourceLabel: String, - permissions: String, - linkUpdates: Seq[NewLinkValueInfo], - newValueInfos: Seq[NewValueInfo] -) - -PREFIX rdf: -PREFIX rdfs: -PREFIX owl: -PREFIX xsd: -PREFIX knora-base: - -INSERT DATA { - GRAPH <@dataNamedGraph> { - <@resourceIri> rdf:type <@resourceClassIri> ; - knora-base:isDeleted false ; - knora-base:attachedToUser <@creatorIri> ; - knora-base:attachedToProject <@projectIri> ; - rdfs:label """@resourceLabel""" ; - knora-base:hasPermissions "@permissions" ; - knora-base:creationDate "@creationDate"^^xsd:dateTime . - - @for(newValueInfo <- newValueInfos) { - # Value: @newValueInfo.valueIri - # Property: @newValueInfo.propertyIri - - @* Construct the value. *@ - <@newValueInfo.valueIri> rdf:type <@newValueInfo.valueTypeIri> ; - knora-base:isDeleted false ; - knora-base:valueHasString """@newValueInfo.valueHasString""" ; - knora-base:valueHasUUID "@{UuidUtil.base64Encode(newValueInfo.valueUUID)}" . - - @newValueInfo.value match { - - case linkValue: LinkValueInfo => { - <@newValueInfo.resourceIri> <@newValueInfo.propertyIri.stripSuffix("Value")> <@linkValue.referredResourceIri> . - - <@newValueInfo.valueIri> rdf:subject <@newValueInfo.resourceIri> ; - rdf:predicate <@newValueInfo.propertyIri.stripSuffix("Value")> ; - rdf:object <@linkValue.referredResourceIri> ; - knora-base:valueHasRefCount 1 . - } - - case textValue: UnformattedTextValueInfo => { - @textValue.valueHasLanguage.map { language => - <@newValueInfo.valueIri> knora-base:valueHasLanguage """@language""" . - } - } - - case textValue: FormattedTextValueInfo => { - @textValue.valueHasLanguage.map { language => - <@newValueInfo.valueIri> knora-base:valueHasLanguage """@language""" . - } - <@newValueInfo.valueIri> knora-base:valueHasMapping <@textValue.mappingIri> . - <@newValueInfo.valueIri> knora-base:valueHasMaxStandoffStartIndex @textValue.maxStandoffStartIndex . - - @* Create a Standoff node for each standoff tag. *@ - @for(createStandoff: StandoffTagInfo <- textValue.standoff) { - - <@newValueInfo.valueIri> knora-base:valueHasStandoff <@createStandoff.standoffTagInstanceIri> . - - <@createStandoff.standoffTagInstanceIri> - @* - Check for optional standoff properties - *@ - @createStandoff.endIndex.map { index => - knora-base:standoffTagHasEndIndex @index ; - } - @createStandoff.startParentIri.map { iri => - knora-base:standoffTagHasStartParent <@iri> ; - } - @createStandoff.endParentIri.map { iri => - knora-base:standoffTagHasEndParent <@iri> ; - } - @createStandoff.originalXMLID.map { id => - knora-base:standoffTagHasOriginalXMLID """@id""" ; - } - - @* - Handle standoff class specific standoff properties - *@ - @for(createProperty <- createStandoff.attributes) { - <@createProperty.propertyIri> @createProperty.value ; - } - - knora-base:standoffTagHasStartIndex @createStandoff.startIndex ; - knora-base:standoffTagHasUUID "@{UuidUtil.base64Encode(createStandoff.uuid)}" ; - knora-base:standoffTagHasStart @createStandoff.startPosition ; - knora-base:standoffTagHasEnd @createStandoff.endPosition ; - rdf:type <@createStandoff.standoffTagClassIri> . - } - } - - - case intValue: IntegerValueInfo => { - <@newValueInfo.valueIri> knora-base:valueHasInteger @intValue.valueHasInteger . - } - - case decimalValue: DecimalValueInfo => { - <@newValueInfo.valueIri> knora-base:valueHasDecimal "@decimalValue.valueHasDecimal"^^xsd:decimal . - } - - case booleanValue: BooleanValueInfo => { - <@newValueInfo.valueIri> knora-base:valueHasBoolean @booleanValue.valueHasBoolean . - } - - case uriValue: UriValueInfo => { - <@newValueInfo.valueIri> knora-base:valueHasUri """@uriValue.valueHasUri"""^^xsd:anyURI . - } - - case dateValue: DateValueInfo => { - <@newValueInfo.valueIri> knora-base:valueHasStartJDN @dateValue.valueHasStartJDN ; - knora-base:valueHasEndJDN @dateValue.valueHasEndJDN ; - knora-base:valueHasStartPrecision "@dateValue.valueHasStartPrecision" ; - knora-base:valueHasEndPrecision "@dateValue.valueHasEndPrecision" ; - knora-base:valueHasCalendar "@dateValue.valueHasCalendar" . - } - - case colorValue: ColorValueInfo => { - <@newValueInfo.valueIri> knora-base:valueHasColor """@colorValue.valueHasColor""" . - } - - case geometryValue: GeomValueInfo => { - <@newValueInfo.valueIri> knora-base:valueHasGeometry """@geometryValue.valueHasGeometry""" . - } - - case fileValue: StillImageFileValueInfo => { - <@newValueInfo.valueIri> knora-base:internalFilename """@fileValue.internalFilename""" ; - knora-base:internalMimeType """@fileValue.internalMimeType""" ; - knora-base:dimX @fileValue.dimX ; - knora-base:dimY @fileValue.dimY . - @fileValue.originalFilename.map { originalFilename => - <@newValueInfo.valueIri> knora-base:originalFilename """@originalFilename""" . - } - @fileValue.originalMimeType.map { originalMimeType => - <@newValueInfo.valueIri> knora-base:originalMimeType """@originalMimeType""" . - } - } - - case fileValue: StillImageExternalFileValueInfo => { - <@newValueInfo.valueIri> knora-base:internalFilename """@fileValue.internalFilename""" ; - knora-base:internalMimeType """@fileValue.internalMimeType""" ; - knora-base:externalUrl """@fileValue.externalUrl""" . - @fileValue.originalFilename.map { originalFilename => - <@newValueInfo.valueIri> knora-base:originalFilename """@originalFilename""" . - } - @fileValue.originalMimeType.map { originalMimeType => - <@newValueInfo.valueIri> knora-base:originalMimeType """@originalMimeType""" . - } - } - - case fileValue: DocumentFileValueInfo => { - <@newValueInfo.valueIri> knora-base:internalFilename """@fileValue.internalFilename""" ; - knora-base:internalMimeType """@fileValue.internalMimeType""" . - @fileValue.originalFilename.map { originalFilename => - <@newValueInfo.valueIri> knora-base:originalFilename """@originalFilename""" . - } - @fileValue.originalMimeType.map { originalMimeType => - <@newValueInfo.valueIri> knora-base:originalMimeType """@originalMimeType""" . - } - @fileValue.dimX.map { dimX => - <@newValueInfo.valueIri> knora-base:dimX @dimX . - } - @fileValue.dimY.map { dimY => - <@newValueInfo.valueIri> knora-base:dimY @dimY . - } - @fileValue.pageCount.map { pageCount => - <@newValueInfo.valueIri> knora-base:pageCount @pageCount . - } - } - - case fileValue: OtherFileValueInfo => { - <@newValueInfo.valueIri> knora-base:internalFilename """@fileValue.internalFilename""" ; - knora-base:internalMimeType """@fileValue.internalMimeType""" . - @fileValue.originalFilename.map { originalFilename => - <@newValueInfo.valueIri> knora-base:originalFilename """@originalFilename""" . - } - @fileValue.originalMimeType.map { originalMimeType => - <@newValueInfo.valueIri> knora-base:originalMimeType """@originalMimeType""" . - } - } - - case listValue: HierarchicalListValueInfo => { - <@newValueInfo.valueIri> knora-base:valueHasListNode <@listValue.valueHasListNode> . - } - - case intervalValue: IntervalValueInfo => { - <@newValueInfo.valueIri> knora-base:valueHasIntervalStart "@intervalValue.valueHasIntervalStart"^^xsd:decimal ; - knora-base:valueHasIntervalEnd "@intervalValue.valueHasIntervalEnd"^^xsd:decimal . - } - - case timeValue: TimeValueInfo => { - <@newValueInfo.valueIri> knora-base:valueHasTimeStamp "@timeValue.valueHasTimeStamp"^^xsd:dateTime . - } - - case geonameValue: GeonameValueInfo => { - <@newValueInfo.valueIri> knora-base:valueHasGeonameCode """@geonameValue.valueHasGeonameCode""" . - } - } - - @* Insert the value's comment, if given. *@ - @newValueInfo.comment.map { commentStr => - <@newValueInfo.valueIri> knora-base:valueHasComment """@commentStr""" . - } - - <@newValueInfo.valueIri> knora-base:attachedToUser <@newValueInfo.valueCreator> ; - knora-base:hasPermissions "@newValueInfo.valuePermissions"^^xsd:string ; - knora-base:valueHasOrder @newValueInfo.valueHasOrder ; - knora-base:valueCreationDate "@newValueInfo.creationDate"^^xsd:dateTime . - - @* Attach the value to the resource. *@ - <@newValueInfo.resourceIri> <@newValueInfo.propertyIri> <@newValueInfo.valueIri> . - } - - - @for((linkUpdate, linkValueIndex) <- linkUpdates.zipWithIndex) { - - <@resourceIri> <@linkUpdate.linkPropertyIri> <@linkUpdate.linkTargetIri> . - - @* Insert a LinkValue for the resource reference. *@ - <@linkUpdate.newLinkValueIri> rdf:type knora-base:LinkValue ; - rdf:subject <@resourceIri> ; - rdf:predicate <@linkUpdate.linkPropertyIri> ; - rdf:object <@linkUpdate.linkTargetIri> ; - knora-base:valueHasString "@linkUpdate.linkTargetIri" ; - knora-base:valueHasRefCount @linkUpdate.newReferenceCount ; - knora-base:isDeleted false ; - knora-base:valueCreationDate "@creationDate"^^xsd:dateTime ; - knora-base:attachedToUser <@linkUpdate.newLinkValueCreator> ; - knora-base:hasPermissions "@linkUpdate.newLinkValuePermissions" ; - knora-base:valueHasUUID "@linkUpdate.valueUuid" . - - @* Attach the new LinkValue to its containing resource. *@ - <@resourceIri> <@{linkUpdate.linkPropertyIri}Value> <@linkUpdate.newLinkValueIri> . - } - } -} 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 28833bf13f..94368c55e8 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 @@ -5,6 +5,7 @@ package org.knora.webapi.slice.resources.repo.service +import org.apache.jena.update.UpdateFactory import zio.* import zio.test.* @@ -14,264 +15,1287 @@ import java.util.UUID import dsp.valueobjects.UuidUtil import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.StringFormatter -import org.knora.webapi.messages.twirl.NewLinkValueInfo -import org.knora.webapi.messages.twirl.NewValueInfo -import org.knora.webapi.messages.twirl.TypeSpecificValueInfo +import org.knora.webapi.messages.util.CalendarNameGregorian +import org.knora.webapi.messages.util.DatePrecisionDay import org.knora.webapi.slice.resourceinfo.domain.InternalIri +import org.knora.webapi.slice.resources.repo.model.ResourceReadyToCreate +import org.knora.webapi.slice.resources.repo.model.StandoffAttribute +import org.knora.webapi.slice.resources.repo.model.StandoffAttributeValue +import org.knora.webapi.slice.resources.repo.model.StandoffLinkValueInfo +import org.knora.webapi.slice.resources.repo.model.StandoffTagInfo +import org.knora.webapi.slice.resources.repo.model.TypeSpecificValueInfo +import org.knora.webapi.slice.resources.repo.model.ValueInfo import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Update -object ResourcesRepoLiveSpec extends ZIOSpecDefault { - def spec: Spec[Environment & (TestEnvironment & Scope), Any] = tests.provide(StringFormatter.test) +object TestData { - val tests: Spec[StringFormatter, Nothing] = - suite("ResourcesRepoLiveSpec")( - test("Create new resource query without values") { - val tripleQuotes = "\"\"\"" - - val graphIri = InternalIri("fooGraph") - val projectIri = "fooProject" - val userIri = "fooUser" - val resourceIri = "fooResource" - val resourceClassIri = "fooClass" - val label = "fooLabel" - val creationDate = Instant.parse("2024-01-01T10:00:00.673298Z") - val permissions = "fooPermissions" - - val resourceDefinition = ResourceReadyToCreate( - resourceIri = resourceIri, - resourceClassIri = resourceClassIri, - resourceLabel = label, - creationDate = creationDate, - permissions = permissions, - newValueInfos = Seq.empty, - linkUpdates = Seq.empty, - ) - - val expected = - Update(s"""| - |PREFIX rdf: - |PREFIX rdfs: - |PREFIX owl: - |PREFIX xsd: - |PREFIX knora-base: - | - |INSERT DATA { - | GRAPH <${graphIri.value}> { - | <$resourceIri> rdf:type <$resourceClassIri> ; - | knora-base:isDeleted false ; - | knora-base:attachedToUser <$userIri> ; - | knora-base:attachedToProject <$projectIri> ; - | rdfs:label $tripleQuotes$label$tripleQuotes ; - | knora-base:hasPermissions "$permissions" ; - | knora-base:creationDate "$creationDate"^^xsd:dateTime . - | - | - | - | - | - | } - |} - |""".stripMargin) - val result = ResourcesRepoLive.createNewResourceQuery( - dataGraphIri = graphIri, - resourceToCreate = resourceDefinition, - projectIri = projectIri, - creatorIri = userIri, - ) - - assertTrue(expected.sparql == result.sparql) - }, - test("Create new resource query with values") { - val graphIri = InternalIri("fooGraph") - val projectIri = "fooProject" - val userIri = "fooUser" - val resourceIri = "fooResource" - val resourceClassIri = "fooClass" - val label = "fooLabel" - val creationDate = Instant.parse("2024-01-01T10:00:00.673298Z") - val permissions = "fooPermissions" - - val valueIri = "fooValue" - val valueCreator = "fooCreator" - val valuePermissions = "fooValuePermissions" - val valueCreationDate = Instant.parse("2024-01-01T10:00:00.673298Z") - - val uuid = UUID.randomUUID() - val uuidEncoded = UuidUtil.base64Encode(uuid) - - val resourceDefinition = ResourceReadyToCreate( - resourceIri = resourceIri, - resourceClassIri = resourceClassIri, - resourceLabel = label, - creationDate = creationDate, - permissions = permissions, - newValueInfos = Seq( - NewValueInfo( - resourceIri = resourceIri, - propertyIri = "fooProperty", - valueIri = valueIri, - valueTypeIri = OntologyConstants.KnoraBase.IntValue, - valueUUID = uuid, - value = TypeSpecificValueInfo.IntegerValueInfo(42), - valuePermissions = valuePermissions, - valueCreator = valueCreator, - creationDate = valueCreationDate, - valueHasOrder = 1, - valueHasString = "42", - comment = None, + val graphIri = InternalIri("foo:Graph") + val projectIri = InternalIri("foo:ProjectIri") + val userIri = InternalIri("foo:UserIri") + val resourceIri = InternalIri("foo:ResourceInstanceIri") + val resourceClassIri = InternalIri("foo:ClassIri") + val label = "foo Label" + val creationDate = Instant.parse("2024-01-01T10:00:00.673298Z") + val permissions = "fooPermissionsString" + val valueCreator = InternalIri("foo:ValueCreatorIri") + val valuePermissions = "fooValuePermissions" + val valueCreationDate = Instant.parse("2024-01-01T12:00:00.673298Z") + + val resourceDefinition = ResourceReadyToCreate( + resourceIri = resourceIri, + resourceClassIri = resourceClassIri, + resourceLabel = label, + creationDate = creationDate, + permissions = permissions, + valueInfos = Seq.empty, + standoffLinks = Seq.empty, + ) + + val linkValueDefinition = ValueInfo( + resourceIri = resourceIri, + propertyIri = InternalIri("foo:hasLinkToValue"), + valueIri = InternalIri("foo:LinkValueIri"), + valueTypeIri = InternalIri(OntologyConstants.KnoraBase.LinkValue), + valueUUID = UUID.randomUUID(), + value = TypeSpecificValueInfo.LinkValueInfo(InternalIri("foo:LinkTargetIri")), + permissions = valuePermissions, + creator = valueCreator, + creationDate = valueCreationDate, + valueHasOrder = 1, + valueHasString = "foo:LinkValueIri", + comment = None, + ) + + val unformattedTextValueDefinition = ValueInfo( + resourceIri = resourceIri, + propertyIri = InternalIri("foo:hasUnformattedTextValue"), + valueIri = InternalIri("foo:UnformattedTextValueIri"), + valueTypeIri = InternalIri(OntologyConstants.KnoraBase.TextValue), + valueUUID = UUID.randomUUID(), + value = TypeSpecificValueInfo.UnformattedTextValueInfo(Some("en")), + permissions = valuePermissions, + creator = valueCreator, + creationDate = valueCreationDate, + valueHasOrder = 1, + valueHasString = "this is a text without formatting", + comment = None, + ) + + val standoffTagUuid = UUID.randomUUID() + val formattedTextValueDefinition = ValueInfo( + resourceIri = resourceIri, + propertyIri = InternalIri("foo:hasFormattedTextValue"), + valueIri = InternalIri("foo:FormattedTextValueIri"), + valueTypeIri = InternalIri(OntologyConstants.KnoraBase.TextValue), + valueUUID = UUID.randomUUID(), + value = TypeSpecificValueInfo.FormattedTextValueInfo( + valueHasLanguage = Some("en"), + mappingIri = InternalIri("foo:MappingIri"), + maxStandoffStartIndex = 0, + standoff = List( + StandoffTagInfo( + standoffTagClassIri = InternalIri("foo:StandoffTagClassIri"), + standoffTagInstanceIri = InternalIri("foo:StandoffTagInstanceIri"), + startParentIri = Some(InternalIri("foo:StartParentIri")), + endParentIri = Some(InternalIri("foo:EndParentIri")), + uuid = standoffTagUuid, + originalXMLID = Some("xml-id"), + startIndex = 0, + endIndex = Some(3), + startPosition = 0, + endPosition = 3, + attributes = List( + StandoffAttribute( + InternalIri("foo:attributePropertyIri"), + StandoffAttributeValue.IriAttribute(InternalIri("foo:standoffAttributeIri")), ), - ), - linkUpdates = Seq.empty, - ) - - val expected = - Update(s"""| - |PREFIX rdf: - |PREFIX rdfs: - |PREFIX owl: - |PREFIX xsd: - |PREFIX knora-base: - | - |INSERT DATA { - | GRAPH { - | rdf:type ; - | knora-base:isDeleted false ; - | knora-base:attachedToUser ; - | knora-base:attachedToProject ; - | rdfs:label \"\"\"fooLabel\"\"\" ; - | knora-base:hasPermissions \"fooPermissions\" ; - | knora-base:creationDate \"2024-01-01T10:00:00.673298Z\"^^xsd:dateTime . - | - | - | # Value: fooValue - | # Property: fooProperty - | - | - | rdf:type ; - | knora-base:isDeleted false ; - | knora-base:valueHasString \"\"\"42\"\"\" ; - | knora-base:valueHasUUID \"$uuidEncoded\" . - | - | - | knora-base:valueHasInteger 42 . - | - | - | - | - | - | knora-base:attachedToUser ; - | knora-base:hasPermissions \"fooValuePermissions\"^^xsd:string ; - | knora-base:valueHasOrder 1 ; - | knora-base:valueCreationDate \"2024-01-01T10:00:00.673298Z\"^^xsd:dateTime . - | - | - | . - | - | - | - | - | } - |} - |""".stripMargin) - val result = ResourcesRepoLive.createNewResourceQuery( - dataGraphIri = graphIri, - resourceToCreate = resourceDefinition, - projectIri = projectIri, - creatorIri = userIri, - ) - - assertTrue(expected.sparql == result.sparql) - }, - test("Create new resource query with links") { - val graphIri = InternalIri("fooGraph") - val projectIri = "fooProject" - val userIri = "fooUser" - val resourceIri = "fooResource" - val resourceClassIri = "fooClass" - val label = "fooLabel" - val creationDate = Instant.parse("2024-01-01T10:00:00.673298Z") - val permissions = "fooPermissions" - - val linkPropertyIri = "fooLinkProperty" - val linkTargetIri = "fooLinkTarget" - val linkValueIri = "fooLinkValue" - val linkCreator = "fooLinkCreator" - val linkPermissions = "fooLinkPermissions" - val valueUuid = UuidUtil.makeRandomBase64EncodedUuid - - val resourceDefinition = ResourceReadyToCreate( - resourceIri = resourceIri, - resourceClassIri = resourceClassIri, - resourceLabel = label, - creationDate = creationDate, - permissions = permissions, - newValueInfos = Seq.empty, - linkUpdates = Seq( - NewLinkValueInfo( - linkPropertyIri = linkPropertyIri, - newLinkValueIri = linkValueIri, - linkTargetIri = linkTargetIri, - newReferenceCount = 1, - newLinkValueCreator = linkCreator, - newLinkValuePermissions = linkPermissions, - valueUuid = valueUuid, + StandoffAttribute( + InternalIri("foo:attributePropertyIri"), + StandoffAttributeValue.UriAttribute("http://example.com"), + ), + StandoffAttribute( + InternalIri("foo:attributePropertyIri"), + StandoffAttributeValue.InternalReferenceAttribute(InternalIri("foo:internalRef")), + ), + StandoffAttribute( + InternalIri("foo:attributePropertyIri"), + StandoffAttributeValue.StringAttribute("attribute value"), + ), + StandoffAttribute(InternalIri("foo:attributePropertyIri"), StandoffAttributeValue.IntegerAttribute(42)), + StandoffAttribute( + InternalIri("foo:attributePropertyIri"), + StandoffAttributeValue.DecimalAttribute(BigDecimal(42.42)), + ), + StandoffAttribute(InternalIri("foo:attributePropertyIri"), StandoffAttributeValue.BooleanAttribute(true)), + StandoffAttribute( + InternalIri("foo:attributePropertyIri"), + StandoffAttributeValue.TimeAttribute(Instant.parse("1024-01-01T10:00:00.673298Z")), ), ), - ) - - val expected = - Update(s"""| - |PREFIX rdf: - |PREFIX rdfs: - |PREFIX owl: - |PREFIX xsd: - |PREFIX knora-base: - | - |INSERT DATA { - | GRAPH { - | rdf:type ; - | knora-base:isDeleted false ; - | knora-base:attachedToUser ; - | knora-base:attachedToProject ; - | rdfs:label \"\"\"fooLabel\"\"\" ; - | knora-base:hasPermissions "fooPermissions" ; - | knora-base:creationDate "2024-01-01T10:00:00.673298Z"^^xsd:dateTime . - | - | - | - | - | - | - | . - | - | - | rdf:type knora-base:LinkValue ; - | rdf:subject ; - | rdf:predicate ; - | rdf:object ; - | knora-base:valueHasString "fooLinkTarget" ; - | knora-base:valueHasRefCount 1 ; - | knora-base:isDeleted false ; - | knora-base:valueCreationDate "2024-01-01T10:00:00.673298Z"^^xsd:dateTime ; - | knora-base:attachedToUser ; - | knora-base:hasPermissions "fooLinkPermissions" ; - | knora-base:valueHasUUID "$valueUuid" . - | - | - | . - | - | } - |} - |""".stripMargin) - val result = ResourcesRepoLive.createNewResourceQuery( - dataGraphIri = graphIri, - resourceToCreate = resourceDefinition, - projectIri = projectIri, - creatorIri = userIri, - ) - - assertTrue(expected.sparql == result.sparql) - }, + ), + ), + ), + permissions = valuePermissions, + creator = valueCreator, + creationDate = valueCreationDate, + valueHasOrder = 1, + valueHasString = "this is a text with formatting", + comment = None, + ) + + val intValueDefinition = + ValueInfo( + resourceIri = resourceIri, + propertyIri = InternalIri("foo:hasInt"), + valueIri = InternalIri("foo:IntValueIri"), + valueTypeIri = InternalIri(OntologyConstants.KnoraBase.IntValue), + valueUUID = UUID.randomUUID(), + value = TypeSpecificValueInfo.IntegerValueInfo(42), + permissions = valuePermissions, + creator = valueCreator, + creationDate = valueCreationDate, + valueHasOrder = 1, + valueHasString = "42", + comment = None, + ) + + val boolValueDefinition = + ValueInfo( + resourceIri = resourceIri, + propertyIri = InternalIri("foo:hasBoolean"), + valueIri = InternalIri("foo:BooleanValueIri"), + valueTypeIri = InternalIri(OntologyConstants.KnoraBase.BooleanValue), + valueUUID = UUID.randomUUID(), + value = TypeSpecificValueInfo.BooleanValueInfo(true), + permissions = valuePermissions, + creator = valueCreator, + creationDate = valueCreationDate, + valueHasOrder = 1, + valueHasString = "true", + comment = None, ) + val decimalValueDefinition = + ValueInfo( + resourceIri = resourceIri, + propertyIri = InternalIri("foo:hasDecimal"), + valueIri = InternalIri("foo:DecimalValueIri"), + valueTypeIri = InternalIri(OntologyConstants.KnoraBase.DecimalValue), + valueUUID = UUID.randomUUID(), + value = TypeSpecificValueInfo.DecimalValueInfo(BigDecimal(42.42)), + permissions = valuePermissions, + creator = valueCreator, + creationDate = valueCreationDate, + valueHasOrder = 1, + valueHasString = "42.42", + comment = None, + ) + + val uriValueDefinition = + ValueInfo( + resourceIri = resourceIri, + propertyIri = InternalIri("foo:hasUri"), + valueIri = InternalIri("foo:UriValueIri"), + valueTypeIri = InternalIri(OntologyConstants.KnoraBase.UriValue), + valueUUID = UUID.randomUUID(), + value = TypeSpecificValueInfo.UriValueInfo("http://example.com"), + permissions = valuePermissions, + creator = valueCreator, + creationDate = valueCreationDate, + valueHasOrder = 1, + valueHasString = "http://example.com", + comment = None, + ) + + val dateValueDefinition = + ValueInfo( + resourceIri = resourceIri, + propertyIri = InternalIri("foo:hasDate"), + valueIri = InternalIri("foo:DateValueIri"), + valueTypeIri = InternalIri(OntologyConstants.KnoraBase.DateValue), + valueUUID = UUID.randomUUID(), + value = TypeSpecificValueInfo.DateValueInfo(0, 0, DatePrecisionDay, DatePrecisionDay, CalendarNameGregorian), + permissions = valuePermissions, + creator = valueCreator, + creationDate = valueCreationDate, + valueHasOrder = 1, + valueHasString = "2024-01-01", + comment = None, + ) + + val colorValueDefinition = + ValueInfo( + resourceIri = resourceIri, + propertyIri = InternalIri("foo:hasColor"), + valueIri = InternalIri("foo:ColorValueIri"), + valueTypeIri = InternalIri(OntologyConstants.KnoraBase.ColorValue), + valueUUID = UUID.randomUUID(), + value = TypeSpecificValueInfo.ColorValueInfo("#ff0000"), + permissions = valuePermissions, + creator = valueCreator, + creationDate = valueCreationDate, + valueHasOrder = 1, + valueHasString = "#ff0000", + comment = None, + ) + + val geometryValueDefinition = + ValueInfo( + resourceIri = resourceIri, + propertyIri = InternalIri("foo:hasGeom"), + valueIri = InternalIri("foo:GeomValueIri"), + valueTypeIri = InternalIri(OntologyConstants.KnoraBase.GeomValue), + valueUUID = UUID.randomUUID(), + value = TypeSpecificValueInfo.GeomValueInfo( + """{"status":"active","lineColor":"#33ff33","lineWidth":2,"points":[{"x":0.20226843100189035,"y":0.3090909090909091},{"x":0.6389413988657845,"y":0.3594405594405594}],"type":"rectangle","original_index":0}""", + ), + permissions = valuePermissions, + creator = valueCreator, + creationDate = valueCreationDate, + valueHasOrder = 1, + valueHasString = + """{"status":"active","lineColor":"#33ff33","lineWidth":2,"points":[{"x":0.20226843100189035,"y":0.3090909090909091},{"x":0.6389413988657845,"y":0.3594405594405594}],"type":"rectangle","original_index":0}""", + comment = None, + ) + + val stillImageFileValueDefinition = + ValueInfo( + resourceIri = resourceIri, + propertyIri = InternalIri("foo:hasStillImage"), + valueIri = InternalIri("foo:StillImageFileValueIri"), + valueTypeIri = InternalIri(OntologyConstants.KnoraBase.StillImageFileValue), + valueUUID = UUID.randomUUID(), + value = TypeSpecificValueInfo.StillImageFileValueInfo( + internalFilename = "24159oO1pNg-ByLN1NLlMSJ.jp2", + internalMimeType = "image/jp2", + originalFilename = Some("foo.png"), + originalMimeType = Some("image/png"), + dimX = 100, + dimY = 60, + ), + permissions = valuePermissions, + creator = valueCreator, + creationDate = valueCreationDate, + valueHasOrder = 1, + valueHasString = "foo.jpg", + comment = None, + ) + + val stillImageExternalFileValueDefinition = + ValueInfo( + resourceIri = resourceIri, + propertyIri = InternalIri("foo:hasStillImageExternal"), + valueIri = InternalIri("foo:StillImageExternalFileValueIri"), + valueTypeIri = InternalIri(OntologyConstants.KnoraBase.StillImageFileValue), + valueUUID = UUID.randomUUID(), + value = TypeSpecificValueInfo.StillImageExternalFileValueInfo( + internalFilename = "24159oO1pNg-ByLN1NLlMSJ.jp2", + internalMimeType = "image/jp2", + originalFilename = None, + originalMimeType = None, + externalUrl = "http://example.com/foo.jpg", + ), + permissions = valuePermissions, + creator = valueCreator, + creationDate = valueCreationDate, + valueHasOrder = 1, + valueHasString = "foo.jpg", + comment = None, + ) + + val documentFileValueDefinition = + ValueInfo( + resourceIri = resourceIri, + propertyIri = InternalIri("foo:hasDocument"), + valueIri = InternalIri("foo:DocumentFileValueIri"), + valueTypeIri = InternalIri(OntologyConstants.KnoraBase.DocumentFileValue), + valueUUID = UUID.randomUUID(), + value = TypeSpecificValueInfo.DocumentFileValueInfo( + internalFilename = "24159oO1pNg-ByLN1NLlMSJ.pdf", + internalMimeType = "application/pdf", + originalFilename = Some("foo.pdf"), + originalMimeType = Some("application/pdf"), + dimX = Some(100), + dimY = Some(60), + pageCount = Some(10), + ), + permissions = valuePermissions, + creator = valueCreator, + creationDate = valueCreationDate, + valueHasOrder = 1, + valueHasString = "foo.pdf", + comment = None, + ) + + val otherFileValueDefinition = + ValueInfo( + resourceIri = resourceIri, + propertyIri = InternalIri("foo:hasOtherFile"), + valueIri = InternalIri("foo:OtherFileValueIri"), + valueTypeIri = InternalIri(OntologyConstants.KnoraBase.ArchiveFileValue), + valueUUID = UUID.randomUUID(), + value = TypeSpecificValueInfo.OtherFileValueInfo( + internalFilename = "24159oO1pNg-ByLN1NLlMSJ.zip", + internalMimeType = "application/zip", + originalFilename = Some("foo.zip"), + originalMimeType = Some("application/zip"), + ), + permissions = valuePermissions, + creator = valueCreator, + creationDate = valueCreationDate, + valueHasOrder = 1, + valueHasString = "foo.zip", + comment = None, + ) + + val hierarchicalListValueDefinition = + ValueInfo( + resourceIri = resourceIri, + propertyIri = InternalIri("foo:hasList"), + valueIri = InternalIri("foo:ListNodeIri"), + valueTypeIri = InternalIri(OntologyConstants.KnoraBase.ListValue), + valueUUID = UUID.randomUUID(), + value = TypeSpecificValueInfo.HierarchicalListValueInfo(InternalIri("foo:ListNodeIri")), + permissions = valuePermissions, + creator = valueCreator, + creationDate = valueCreationDate, + valueHasOrder = 1, + valueHasString = "foo list", + comment = None, + ) + + val intervalValueDefinition = + ValueInfo( + resourceIri = resourceIri, + propertyIri = InternalIri("foo:hasInterval"), + valueIri = InternalIri("foo:IntervalValueIri"), + valueTypeIri = InternalIri(OntologyConstants.KnoraBase.IntervalValue), + valueUUID = UUID.randomUUID(), + value = TypeSpecificValueInfo.IntervalValueInfo( + valueHasIntervalStart = BigDecimal(0.0), + valueHasIntervalEnd = BigDecimal(100.0), + ), + permissions = valuePermissions, + creator = valueCreator, + creationDate = valueCreationDate, + valueHasOrder = 1, + valueHasString = "0.0 - 100.0", + comment = None, + ) + + val timeValueDefinition = + ValueInfo( + resourceIri = resourceIri, + propertyIri = InternalIri("foo:hasTime"), + valueIri = InternalIri("foo:TimeValueIri"), + valueTypeIri = InternalIri(OntologyConstants.KnoraBase.TimeValue), + valueUUID = UUID.randomUUID(), + value = TypeSpecificValueInfo.TimeValueInfo(Instant.parse("1024-01-01T10:00:00.673298Z")), + permissions = valuePermissions, + creator = valueCreator, + creationDate = valueCreationDate, + valueHasOrder = 1, + valueHasString = "1024-01-01T10:00:00.673298Z", + comment = None, + ) + + val geonameValueDefinition = + ValueInfo( + resourceIri = resourceIri, + propertyIri = InternalIri("foo:hasGeoname"), + valueIri = InternalIri("foo:GeonameValueIri"), + valueTypeIri = InternalIri(OntologyConstants.KnoraBase.GeonameValue), + valueUUID = UUID.randomUUID(), + value = TypeSpecificValueInfo.GeonameValueInfo("geoname_code"), + permissions = valuePermissions, + creator = valueCreator, + creationDate = valueCreationDate, + valueHasOrder = 1, + valueHasString = "geoname_code", + comment = None, + ) + + val standoffLinkValue = + StandoffLinkValueInfo( + linkPropertyIri = InternalIri("foo:hasStandoffLinkTo"), + newLinkValueIri = InternalIri("foo:StandoffLinkValueIri"), + linkTargetIri = InternalIri("foo:StandoffLinkTargetIri"), + newReferenceCount = 2, + newLinkValueCreator = valueCreator, + newLinkValuePermissions = valuePermissions, + valueUuid = UuidUtil.base64Encode(UUID.randomUUID()), + ) +} + +object ResourcesRepoLiveSpec extends ZIOSpecDefault { + import TestData.* + + private def assertUpdateQueriesEqual(expected: Update, actual: Update) = { + val parsedExpected = UpdateFactory.create(expected.sparql) + val parsedActual = UpdateFactory.create(actual.sparql) + if (parsedExpected.equalTo(parsedActual)) { + assertTrue(true) + } else { + val expectedStr = parsedExpected.toString() + val actualStr = parsedActual.toString() + assertTrue(actualStr == expectedStr) + } + } + + def spec: Spec[Environment & (TestEnvironment & Scope), Any] = tests.provide(StringFormatter.test) + + private val createResourceWithoutValuesTest = test("Create a new resource query without values") { + val expected = + Update(s"""| + |PREFIX rdf: + |PREFIX rdfs: + |PREFIX xsd: + |PREFIX knora-base: + | + |INSERT DATA { + | GRAPH <${graphIri.value}> { + | <${resourceIri.value}> rdf:type <${resourceClassIri.value}> ; + | rdfs:label "$label" ; + | knora-base:isDeleted false ; + | knora-base:attachedToUser <${userIri.value}> ; + | knora-base:attachedToProject <${projectIri.value}> ; + | knora-base:hasPermissions "$permissions" ; + | knora-base:creationDate "$creationDate"^^xsd:dateTime . + | } + |} + |""".stripMargin) + val result = ResourcesRepoLive.createNewResourceQuery(graphIri, resourceDefinition, projectIri, userIri) + assertUpdateQueriesEqual(expected, result) + } + + private val createResourceWithValueSuite = suite("Create new resource with any type of value")( + test("Create a new resource with a link value") { + val resource = resourceDefinition.copy(valueInfos = List(linkValueDefinition)) + val expected = Update( + s"""| + |PREFIX rdf: + |PREFIX rdfs: + |PREFIX xsd: + |PREFIX knora-base: + | + |INSERT DATA { + | GRAPH <${graphIri.value}> { + | <${resourceIri.value}> rdf:type <${resourceClassIri.value}> ; + | rdfs:label "$label" ; + | knora-base:isDeleted false ; + | knora-base:attachedToUser <${userIri.value}> ; + | knora-base:attachedToProject <${projectIri.value}> ; + | knora-base:hasPermissions "$permissions" ; + | knora-base:creationDate "$creationDate"^^xsd:dateTime ; + | . + | rdf:type ; + | knora-base:isDeleted false ; + | knora-base:valueHasString "foo:LinkValueIri" ; + | knora-base:valueHasUUID "${UuidUtil.base64Encode(linkValueDefinition.valueUUID)}" ; + | knora-base:attachedToUser <${valueCreator.value}> ; + | knora-base:hasPermissions "$valuePermissions" ; + | knora-base:valueHasOrder 1 ; + | knora-base:valueCreationDate "$valueCreationDate"^^xsd:dateTime . + | <${resourceIri.value}> . + | rdf:subject <${resourceIri.value}> ; + | rdf:predicate ; + | rdf:object ; + | knora-base:valueHasRefCount 1 . + | } + |} + |""".stripMargin, + ) + val result = ResourcesRepoLive.createNewResourceQuery(graphIri, resource, projectIri, userIri) + assertUpdateQueriesEqual(expected, result) + }, + test("Create a new resource with an unformatted text value") { + val resource = resourceDefinition.copy(valueInfos = List(unformattedTextValueDefinition)) + val expected = Update( + s"""| + |PREFIX rdf: + |PREFIX rdfs: + |PREFIX xsd: + |PREFIX knora-base: + | + |INSERT DATA { + | GRAPH <${graphIri.value}> { + | <${resourceIri.value}> rdf:type <${resourceClassIri.value}> ; + | rdfs:label "$label" ; + | knora-base:isDeleted false ; + | knora-base:attachedToUser <${userIri.value}> ; + | knora-base:attachedToProject <${projectIri.value}> ; + | knora-base:hasPermissions "$permissions" ; + | knora-base:creationDate "$creationDate"^^xsd:dateTime ; + | . + | rdf:type ; + | knora-base:isDeleted false ; + | knora-base:valueHasString "this is a text without formatting" ; + | knora-base:valueHasUUID "${UuidUtil.base64Encode(unformattedTextValueDefinition.valueUUID)}" ; + | knora-base:attachedToUser <${valueCreator.value}> ; + | knora-base:hasPermissions "$valuePermissions" ; + | knora-base:valueHasOrder 1 ; + | knora-base:valueCreationDate "$valueCreationDate"^^xsd:dateTime ; + | knora-base:valueHasLanguage "en" . + | } + |} + |""".stripMargin, + ) + val result = ResourcesRepoLive.createNewResourceQuery(graphIri, resource, projectIri, userIri) + assertUpdateQueriesEqual(expected, result) + }, + test("Create a new resource with an formatted text value") { + val resource = resourceDefinition.copy(valueInfos = List(formattedTextValueDefinition)) + val expected = Update( + s"""| + |PREFIX rdf: + |PREFIX rdfs: + |PREFIX xsd: + |PREFIX knora-base: + | + |INSERT DATA { + | GRAPH <${graphIri.value}> { + | <${resourceIri.value}> rdf:type <${resourceClassIri.value}> ; + | rdfs:label "$label" ; + | knora-base:isDeleted false ; + | knora-base:attachedToUser <${userIri.value}> ; + | knora-base:attachedToProject <${projectIri.value}> ; + | knora-base:hasPermissions "$permissions" ; + | knora-base:creationDate "$creationDate"^^xsd:dateTime ; + | . + | rdf:type ; + | knora-base:isDeleted false ; + | knora-base:valueHasString "this is a text with formatting" ; + | knora-base:valueHasUUID "${UuidUtil.base64Encode(formattedTextValueDefinition.valueUUID)}" ; + | knora-base:attachedToUser <${valueCreator.value}> ; + | knora-base:hasPermissions "$valuePermissions" ; + | knora-base:valueHasOrder 1 ; + | knora-base:valueCreationDate "$valueCreationDate"^^xsd:dateTime ; + | knora-base:valueHasMapping ; + | knora-base:valueHasMaxStandoffStartIndex 0 ; + | knora-base:valueHasLanguage "en" ; + | knora-base:valueHasStandoff . + | rdf:type ; + | knora-base:standoffTagHasEndIndex 3 ; + | knora-base:standoffTagHasStartParent ; + | knora-base:standoffTagHasEndParent ; + | knora-base:standoffTagHasOriginalXMLID "xml-id" ; + | ; + | "http://example.com"^^xsd:anyURI ; + | ; + | "attribute value" ; + | 42 ; + | 42.42 ; + | true ; + | "1024-01-01T10:00:00.673298Z"^^xsd:dateTime ; + | knora-base:standoffTagHasStartIndex 0 ; + | knora-base:standoffTagHasUUID "${UuidUtil.base64Encode(standoffTagUuid)}" ; + | knora-base:standoffTagHasStart 0 ; + | knora-base:standoffTagHasEnd 3 . + | } + |} + |""".stripMargin, + ) + val result = ResourcesRepoLive.createNewResourceQuery(graphIri, resource, projectIri, userIri) + assertUpdateQueriesEqual(expected, result) + }, + test("Create a new resource with an integer value") { + val resource = resourceDefinition.copy(valueInfos = List(intValueDefinition)) + val expected = Update( + s"""| + |PREFIX rdf: + |PREFIX rdfs: + |PREFIX xsd: + |PREFIX knora-base: + | + |INSERT DATA { + | GRAPH <${graphIri.value}> { + | <${resourceIri.value}> rdf:type <${resourceClassIri.value}> ; + | rdfs:label "$label" ; + | knora-base:isDeleted false ; + | knora-base:attachedToUser <${userIri.value}> ; + | knora-base:attachedToProject <${projectIri.value}> ; + | knora-base:hasPermissions "$permissions" ; + | knora-base:creationDate "$creationDate"^^xsd:dateTime ; + | . + | rdf:type ; + | knora-base:isDeleted false ; + | knora-base:valueHasString "42" ; + | knora-base:valueHasUUID "${UuidUtil.base64Encode(intValueDefinition.valueUUID)}" ; + | knora-base:attachedToUser <${valueCreator.value}> ; + | knora-base:hasPermissions "$valuePermissions" ; + | knora-base:valueHasOrder 1 ; + | knora-base:valueCreationDate "$valueCreationDate"^^xsd:dateTime ; + | knora-base:valueHasInteger 42 . + | } + |} + |""".stripMargin, + ) + val result = ResourcesRepoLive.createNewResourceQuery(graphIri, resource, projectIri, userIri) + assertUpdateQueriesEqual(expected, result) + }, + test("Create a new resource with a boolean value") { + val resource = resourceDefinition.copy(valueInfos = List(boolValueDefinition)) + val expected = Update( + s"""| + |PREFIX rdf: + |PREFIX rdfs: + |PREFIX xsd: + |PREFIX knora-base: + | + |INSERT DATA { + | GRAPH <${graphIri.value}> { + | <${resourceIri.value}> rdf:type <${resourceClassIri.value}> ; + | rdfs:label "$label" ; + | knora-base:isDeleted false ; + | knora-base:attachedToUser <${userIri.value}> ; + | knora-base:attachedToProject <${projectIri.value}> ; + | knora-base:hasPermissions "$permissions" ; + | knora-base:creationDate "$creationDate"^^xsd:dateTime ; + | . + | rdf:type ; + | knora-base:isDeleted false ; + | knora-base:valueHasString "true" ; + | knora-base:valueHasUUID "${UuidUtil.base64Encode(boolValueDefinition.valueUUID)}" ; + | knora-base:attachedToUser <${valueCreator.value}> ; + | knora-base:hasPermissions "$valuePermissions" ; + | knora-base:valueHasOrder 1 ; + | knora-base:valueCreationDate "$valueCreationDate"^^xsd:dateTime ; + | knora-base:valueHasBoolean true . + | } + |} + |""".stripMargin, + ) + val result = ResourcesRepoLive.createNewResourceQuery(graphIri, resource, projectIri, userIri) + assertUpdateQueriesEqual(expected, result) + }, + test("Create a new resource with a decimal value") { + val resource = resourceDefinition.copy(valueInfos = List(decimalValueDefinition)) + val expected = Update( + s"""| + |PREFIX rdf: + |PREFIX rdfs: + |PREFIX xsd: + |PREFIX knora-base: + | + |INSERT DATA { + | GRAPH <${graphIri.value}> { + | <${resourceIri.value}> rdf:type <${resourceClassIri.value}> ; + | rdfs:label "$label" ; + | knora-base:isDeleted false ; + | knora-base:attachedToUser <${userIri.value}> ; + | knora-base:attachedToProject <${projectIri.value}> ; + | knora-base:hasPermissions "$permissions" ; + | knora-base:creationDate "$creationDate"^^xsd:dateTime ; + | . + | rdf:type ; + | knora-base:isDeleted false ; + | knora-base:valueHasString "42.42" ; + | knora-base:valueHasUUID "${UuidUtil.base64Encode(decimalValueDefinition.valueUUID)}" ; + | knora-base:attachedToUser <${valueCreator.value}> ; + | knora-base:hasPermissions "$valuePermissions" ; + | knora-base:valueHasOrder 1 ; + | knora-base:valueCreationDate "$valueCreationDate"^^xsd:dateTime ; + | knora-base:valueHasDecimal 42.42 . + | } + |} + |""".stripMargin, + ) + val result = ResourcesRepoLive.createNewResourceQuery(graphIri, resource, projectIri, userIri) + assertUpdateQueriesEqual(expected, result) + }, + test("Create a new resource with a URI value") { + val resource = resourceDefinition.copy(valueInfos = List(uriValueDefinition)) + val expected = Update( + s"""| + |PREFIX rdf: + |PREFIX rdfs: + |PREFIX xsd: + |PREFIX knora-base: + | + |INSERT DATA { + | GRAPH <${graphIri.value}> { + | <${resourceIri.value}> rdf:type <${resourceClassIri.value}> ; + | rdfs:label "$label" ; + | knora-base:isDeleted false ; + | knora-base:attachedToUser <${userIri.value}> ; + | knora-base:attachedToProject <${projectIri.value}> ; + | knora-base:hasPermissions "$permissions" ; + | knora-base:creationDate "$creationDate"^^xsd:dateTime ; + | . + | rdf:type ; + | knora-base:isDeleted false ; + | knora-base:valueHasString "http://example.com" ; + | knora-base:valueHasUUID "${UuidUtil.base64Encode(uriValueDefinition.valueUUID)}" ; + | knora-base:attachedToUser <${valueCreator.value}> ; + | knora-base:hasPermissions "$valuePermissions" ; + | knora-base:valueHasOrder 1 ; + | knora-base:valueCreationDate "$valueCreationDate"^^xsd:dateTime ; + | knora-base:valueHasUri "http://example.com"^^xsd:anyURI . + | } + |} + |""".stripMargin, + ) + val result = ResourcesRepoLive.createNewResourceQuery(graphIri, resource, projectIri, userIri) + assertUpdateQueriesEqual(expected, result) + }, + test("Create a new resource with a date value") { + val resource = resourceDefinition.copy(valueInfos = List(dateValueDefinition)) + val expected = Update( + s"""| + |PREFIX rdf: + |PREFIX rdfs: + |PREFIX xsd: + |PREFIX knora-base: + | + |INSERT DATA { + | GRAPH <${graphIri.value}> { + | <${resourceIri.value}> rdf:type <${resourceClassIri.value}> ; + | rdfs:label "$label" ; + | knora-base:isDeleted false ; + | knora-base:attachedToUser <${userIri.value}> ; + | knora-base:attachedToProject <${projectIri.value}> ; + | knora-base:hasPermissions "$permissions" ; + | knora-base:creationDate "$creationDate"^^xsd:dateTime ; + | . + | rdf:type ; + | knora-base:isDeleted false ; + | knora-base:valueHasString "2024-01-01" ; + | knora-base:valueHasUUID "${UuidUtil.base64Encode(dateValueDefinition.valueUUID)}" ; + | knora-base:attachedToUser <${valueCreator.value}> ; + | knora-base:hasPermissions "$valuePermissions" ; + | knora-base:valueHasOrder 1 ; + | knora-base:valueCreationDate "$valueCreationDate"^^xsd:dateTime ; + | knora-base:valueHasStartJDN 0 ; + | knora-base:valueHasEndJDN 0 ; + | knora-base:valueHasStartPrecision "DAY" ; + | knora-base:valueHasEndPrecision "DAY" ; + | knora-base:valueHasCalendar "GREGORIAN" . + | } + |} + |""".stripMargin, + ) + val result = ResourcesRepoLive.createNewResourceQuery(graphIri, resource, projectIri, userIri) + assertUpdateQueriesEqual(expected, result) + }, + test("Create a new resource with a color value") { + val resource = resourceDefinition.copy(valueInfos = List(colorValueDefinition)) + val expected = Update( + s"""| + |PREFIX rdf: + |PREFIX rdfs: + |PREFIX xsd: + |PREFIX knora-base: + | + |INSERT DATA { + | GRAPH <${graphIri.value}> { + | <${resourceIri.value}> rdf:type <${resourceClassIri.value}> ; + | rdfs:label "$label" ; + | knora-base:isDeleted false ; + | knora-base:attachedToUser <${userIri.value}> ; + | knora-base:attachedToProject <${projectIri.value}> ; + | knora-base:hasPermissions "$permissions" ; + | knora-base:creationDate "$creationDate"^^xsd:dateTime ; + | . + | rdf:type ; + | knora-base:isDeleted false ; + | knora-base:valueHasString "#ff0000" ; + | knora-base:valueHasUUID "${UuidUtil.base64Encode(colorValueDefinition.valueUUID)}" ; + | knora-base:attachedToUser <${valueCreator.value}> ; + | knora-base:hasPermissions "$valuePermissions" ; + | knora-base:valueHasOrder 1 ; + | knora-base:valueCreationDate "$valueCreationDate"^^xsd:dateTime ; + | knora-base:valueHasColor "#ff0000" . + | } + |} + |""".stripMargin, + ) + + val result = ResourcesRepoLive.createNewResourceQuery(graphIri, resource, projectIri, userIri) + assertUpdateQueriesEqual(expected, result) + }, + test("Create a new resource with a geometry value") { + val resource = resourceDefinition.copy(valueInfos = List(geometryValueDefinition)) + val expected = Update( + s"""| + |PREFIX rdf: + |PREFIX rdfs: + |PREFIX xsd: + |PREFIX knora-base: + | + |INSERT DATA { + | GRAPH <${graphIri.value}> { + | <${resourceIri.value}> rdf:type <${resourceClassIri.value}> ; + | rdfs:label "$label" ; + | knora-base:isDeleted false ; + | knora-base:attachedToUser <${userIri.value}> ; + | knora-base:attachedToProject <${projectIri.value}> ; + | knora-base:hasPermissions "$permissions" ; + | knora-base:creationDate "$creationDate"^^xsd:dateTime ; + | . + | rdf:type ; + | knora-base:isDeleted false ; + | knora-base:valueHasString "{\\"status\\":\\"active\\",\\"lineColor\\":\\"#33ff33\\",\\"lineWidth\\":2,\\"points\\":[{\\"x\\":0.20226843100189035,\\"y\\":0.3090909090909091},{\\"x\\":0.6389413988657845,\\"y\\":0.3594405594405594}],\\"type\\":\\"rectangle\\",\\"original_index\\":0}" ; + | knora-base:valueHasUUID "${UuidUtil.base64Encode(geometryValueDefinition.valueUUID)}" ; + | knora-base:attachedToUser <${valueCreator.value}> ; + | knora-base:hasPermissions "$valuePermissions" ; + | knora-base:valueHasOrder 1 ; + | knora-base:valueCreationDate "$valueCreationDate"^^xsd:dateTime ; + | knora-base:valueHasGeometry "{\\"status\\":\\"active\\",\\"lineColor\\":\\"#33ff33\\",\\"lineWidth\\":2,\\"points\\":[{\\"x\\":0.20226843100189035,\\"y\\":0.3090909090909091},{\\"x\\":0.6389413988657845,\\"y\\":0.3594405594405594}],\\"type\\":\\"rectangle\\",\\"original_index\\":0}" . + | } + |} + |""".stripMargin, + ) + val result = ResourcesRepoLive.createNewResourceQuery(graphIri, resource, projectIri, userIri) + assertUpdateQueriesEqual(expected, result) + }, + test("Create a new resource with a still image file value") { + val resource = resourceDefinition.copy(valueInfos = List(stillImageFileValueDefinition)) + val expected = Update( + s"""| + |PREFIX rdf: + |PREFIX rdfs: + |PREFIX xsd: + |PREFIX knora-base: + | + |INSERT DATA { + | GRAPH <${graphIri.value}> { + | <${resourceIri.value}> rdf:type <${resourceClassIri.value}> ; + | rdfs:label "$label" ; + | knora-base:isDeleted false ; + | knora-base:attachedToUser <${userIri.value}> ; + | knora-base:attachedToProject <${projectIri.value}> ; + | knora-base:hasPermissions "$permissions" ; + | knora-base:creationDate "$creationDate"^^xsd:dateTime ; + | . + | rdf:type ; + | knora-base:isDeleted false ; + | knora-base:valueHasString "foo.jpg" ; + | knora-base:valueHasUUID "${UuidUtil.base64Encode(stillImageFileValueDefinition.valueUUID)}" ; + | knora-base:attachedToUser <${valueCreator.value}> ; + | knora-base:hasPermissions "$valuePermissions" ; + | knora-base:valueHasOrder 1 ; + | knora-base:valueCreationDate "$valueCreationDate"^^xsd:dateTime ; + | knora-base:internalFilename "24159oO1pNg-ByLN1NLlMSJ.jp2" ; + | knora-base:internalMimeType "image/jp2" ; + | knora-base:dimX 100 ; + | knora-base:dimY 60 ; + | knora-base:originalFilename "foo.png" ; + | knora-base:originalMimeType "image/png" . + | } + |} + |""".stripMargin, + ) + val result = ResourcesRepoLive.createNewResourceQuery(graphIri, resource, projectIri, userIri) + assertUpdateQueriesEqual(expected, result) + }, + test("Create a new resource with a still image external file value") { + val resource = resourceDefinition.copy(valueInfos = List(stillImageExternalFileValueDefinition)) + val uuidEncoded = UuidUtil.base64Encode(stillImageExternalFileValueDefinition.valueUUID) + val expected = Update( + s"""| + |PREFIX rdf: + |PREFIX rdfs: + |PREFIX xsd: + |PREFIX knora-base: + | + |INSERT DATA { + | GRAPH <${graphIri.value}> { + | <${resourceIri.value}> rdf:type <${resourceClassIri.value}> ; + | rdfs:label "$label" ; + | knora-base:isDeleted false ; + | knora-base:attachedToUser <${userIri.value}> ; + | knora-base:attachedToProject <${projectIri.value}> ; + | knora-base:hasPermissions "$permissions" ; + | knora-base:creationDate "$creationDate"^^xsd:dateTime ; + | . + | rdf:type ; + | knora-base:isDeleted false ; + | knora-base:valueHasString "foo.jpg" ; + | knora-base:valueHasUUID "$uuidEncoded" ; + | knora-base:attachedToUser <${valueCreator.value}> ; + | knora-base:hasPermissions "$valuePermissions" ; + | knora-base:valueHasOrder 1 ; + | knora-base:valueCreationDate "$valueCreationDate"^^xsd:dateTime ; + | knora-base:internalFilename "24159oO1pNg-ByLN1NLlMSJ.jp2" ; + | knora-base:internalMimeType "image/jp2" ; + | knora-base:externalUrl "http://example.com/foo.jpg" . + | } + |} + |""".stripMargin, + ) + val result = ResourcesRepoLive.createNewResourceQuery(graphIri, resource, projectIri, userIri) + assertUpdateQueriesEqual(expected, result) + }, + test("Create a new resource with a document file value") { + val resource = resourceDefinition.copy(valueInfos = List(documentFileValueDefinition)) + val expected = Update( + s"""| + |PREFIX rdf: + |PREFIX rdfs: + |PREFIX xsd: + |PREFIX knora-base: + | + |INSERT DATA { + | GRAPH <${graphIri.value}> { + | <${resourceIri.value}> rdf:type <${resourceClassIri.value}> ; + | rdfs:label "$label" ; + | knora-base:isDeleted false ; + | knora-base:attachedToUser <${userIri.value}> ; + | knora-base:attachedToProject <${projectIri.value}> ; + | knora-base:hasPermissions "$permissions" ; + | knora-base:creationDate "$creationDate"^^xsd:dateTime ; + | . + | rdf:type ; + | knora-base:isDeleted false ; + | knora-base:valueHasString "foo.pdf" ; + | knora-base:valueHasUUID "${UuidUtil.base64Encode(documentFileValueDefinition.valueUUID)}" ; + | knora-base:attachedToUser <${valueCreator.value}> ; + | knora-base:hasPermissions "$valuePermissions" ; + | knora-base:valueHasOrder 1 ; + | knora-base:valueCreationDate "$valueCreationDate"^^xsd:dateTime ; + | knora-base:internalFilename "24159oO1pNg-ByLN1NLlMSJ.pdf" ; + | knora-base:internalMimeType "application/pdf" ; + | knora-base:originalFilename "foo.pdf" ; + | knora-base:originalMimeType "application/pdf" ; + | knora-base:dimX 100 ; + | knora-base:dimY 60 ; + | knora-base:pageCount 10 . + | } + |} + |""".stripMargin, + ) + val result = ResourcesRepoLive.createNewResourceQuery(graphIri, resource, projectIri, userIri) + assertUpdateQueriesEqual(expected, result) + }, + test("Create a new resource with another file value") { + val resource = resourceDefinition.copy(valueInfos = List(otherFileValueDefinition)) + val expected = Update( + s"""| + |PREFIX rdf: + |PREFIX rdfs: + |PREFIX xsd: + |PREFIX knora-base: + | + |INSERT DATA { + | GRAPH <${graphIri.value}> { + | <${resourceIri.value}> rdf:type <${resourceClassIri.value}> ; + | rdfs:label "$label" ; + | knora-base:isDeleted false ; + | knora-base:attachedToUser <${userIri.value}> ; + | knora-base:attachedToProject <${projectIri.value}> ; + | knora-base:hasPermissions "$permissions" ; + | knora-base:creationDate "$creationDate"^^xsd:dateTime ; + | . + | rdf:type ; + | knora-base:isDeleted false ; + | knora-base:valueHasString "foo.zip" ; + | knora-base:valueHasUUID "${UuidUtil.base64Encode(otherFileValueDefinition.valueUUID)}" ; + | knora-base:attachedToUser <${valueCreator.value}> ; + | knora-base:hasPermissions "$valuePermissions" ; + | knora-base:valueHasOrder 1 ; + | knora-base:valueCreationDate "$valueCreationDate"^^xsd:dateTime ; + | knora-base:internalFilename "24159oO1pNg-ByLN1NLlMSJ.zip" ; + | knora-base:internalMimeType "application/zip" ; + | knora-base:originalFilename "foo.zip" ; + | knora-base:originalMimeType "application/zip" . + | } + |} + |""".stripMargin, + ) + val result = ResourcesRepoLive.createNewResourceQuery(graphIri, resource, projectIri, userIri) + assertUpdateQueriesEqual(expected, result) + }, + test("Create a new resource with a list value") { + val resource = resourceDefinition.copy(valueInfos = List(hierarchicalListValueDefinition)) + val expected = Update( + s"""| + |PREFIX rdf: + |PREFIX rdfs: + |PREFIX xsd: + |PREFIX knora-base: + | + |INSERT DATA { + | GRAPH <${graphIri.value}> { + | <${resourceIri.value}> rdf:type <${resourceClassIri.value}> ; + | rdfs:label "$label" ; + | knora-base:isDeleted false ; + | knora-base:attachedToUser <${userIri.value}> ; + | knora-base:attachedToProject <${projectIri.value}> ; + | knora-base:hasPermissions "$permissions" ; + | knora-base:creationDate "$creationDate"^^xsd:dateTime ; + | . + | rdf:type ; + | knora-base:isDeleted false ; + | knora-base:valueHasString "foo list" ; + | knora-base:valueHasUUID "${UuidUtil.base64Encode(hierarchicalListValueDefinition.valueUUID)}" ; + | knora-base:attachedToUser <${valueCreator.value}> ; + | knora-base:hasPermissions "$valuePermissions" ; + | knora-base:valueHasOrder 1 ; + | knora-base:valueCreationDate "$valueCreationDate"^^xsd:dateTime ; + | knora-base:valueHasListNode . + | } + |} + |""".stripMargin, + ) + val result = ResourcesRepoLive.createNewResourceQuery(graphIri, resource, projectIri, userIri) + assertUpdateQueriesEqual(expected, result) + }, + test("Create a new resource with an interval value") { + val resource = resourceDefinition.copy(valueInfos = List(intervalValueDefinition)) + val expected = Update( + s"""| + |PREFIX rdf: + |PREFIX rdfs: + |PREFIX xsd: + |PREFIX knora-base: + | + |INSERT DATA { + | GRAPH <${graphIri.value}> { + | <${resourceIri.value}> rdf:type <${resourceClassIri.value}> ; + | rdfs:label "$label" ; + | knora-base:isDeleted false ; + | knora-base:attachedToUser <${userIri.value}> ; + | knora-base:attachedToProject <${projectIri.value}> ; + | knora-base:hasPermissions "$permissions" ; + | knora-base:creationDate "$creationDate"^^xsd:dateTime ; + | . + | rdf:type ; + | knora-base:isDeleted false ; + | knora-base:valueHasString "0.0 - 100.0" ; + | knora-base:valueHasUUID "${UuidUtil.base64Encode(intervalValueDefinition.valueUUID)}" ; + | knora-base:attachedToUser <${valueCreator.value}> ; + | knora-base:hasPermissions "$valuePermissions" ; + | knora-base:valueHasOrder 1 ; + | knora-base:valueCreationDate "$valueCreationDate"^^xsd:dateTime ; + | knora-base:valueHasIntervalStart 0.0 ; + | knora-base:valueHasIntervalEnd 100.0 ; + | } + |} + |""".stripMargin, + ) + + val result = ResourcesRepoLive.createNewResourceQuery(graphIri, resource, projectIri, userIri) + assertUpdateQueriesEqual(expected, result) + }, + test("Create a new resource with a time value") { + val resource = resourceDefinition.copy(valueInfos = List(timeValueDefinition)) + val expected = Update( + s"""| + |PREFIX rdf: + |PREFIX rdfs: + |PREFIX xsd: + |PREFIX knora-base: + | + |INSERT DATA { + | GRAPH <${graphIri.value}> { + | <${resourceIri.value}> rdf:type <${resourceClassIri.value}> ; + | rdfs:label "$label" ; + | knora-base:isDeleted false ; + | knora-base:attachedToUser <${userIri.value}> ; + | knora-base:attachedToProject <${projectIri.value}> ; + | knora-base:hasPermissions "$permissions" ; + | knora-base:creationDate "$creationDate"^^xsd:dateTime ; + | . + | rdf:type ; + | knora-base:isDeleted false ; + | knora-base:valueHasString "1024-01-01T10:00:00.673298Z" ; + | knora-base:valueHasUUID "${UuidUtil.base64Encode(timeValueDefinition.valueUUID)}" ; + | knora-base:attachedToUser <${valueCreator.value}> ; + | knora-base:hasPermissions "$valuePermissions" ; + | knora-base:valueHasOrder 1 ; + | knora-base:valueCreationDate "$valueCreationDate"^^xsd:dateTime ; + | knora-base:valueHasTimeStamp "1024-01-01T10:00:00.673298Z"^^xsd:dateTime . + | } + |} + |""".stripMargin, + ) + + val result = ResourcesRepoLive.createNewResourceQuery(graphIri, resource, projectIri, userIri) + assertUpdateQueriesEqual(expected, result) + }, + test("Create a new resource with a geoname value") { + val resource = resourceDefinition.copy(valueInfos = List(geonameValueDefinition)) + val expected = Update( + s"""| + |PREFIX rdf: + |PREFIX rdfs: + |PREFIX xsd: + |PREFIX knora-base: + | + |INSERT DATA { + | GRAPH <${graphIri.value}> { + | <${resourceIri.value}> rdf:type <${resourceClassIri.value}> ; + | rdfs:label "$label" ; + | knora-base:isDeleted false ; + | knora-base:attachedToUser <${userIri.value}> ; + | knora-base:attachedToProject <${projectIri.value}> ; + | knora-base:hasPermissions "$permissions" ; + | knora-base:creationDate "$creationDate"^^xsd:dateTime ; + | . + | rdf:type ; + | knora-base:isDeleted false ; + | knora-base:valueHasString "geoname_code" ; + | knora-base:valueHasUUID "${UuidUtil.base64Encode(geonameValueDefinition.valueUUID)}" ; + | knora-base:attachedToUser <${valueCreator.value}> ; + | knora-base:hasPermissions "$valuePermissions" ; + | knora-base:valueHasOrder 1 ; + | knora-base:valueCreationDate "$valueCreationDate"^^xsd:dateTime ; + | knora-base:valueHasGeonameCode "geoname_code" . + | } + |} + |""".stripMargin, + ) + + val result = ResourcesRepoLive.createNewResourceQuery(graphIri, resource, projectIri, userIri) + assertUpdateQueriesEqual(expected, result) + }, + ) + + private val createResourceWithMultipleValuesTest = test("Create a resource with multiple values") { + val resource = resourceDefinition.copy(valueInfos = List(intValueDefinition, boolValueDefinition)) + val expected = Update( + s"""| + |PREFIX rdf: + |PREFIX rdfs: + |PREFIX xsd: + |PREFIX knora-base: + | + |INSERT DATA { + | GRAPH <${graphIri.value}> { + | <${resourceIri.value}> rdf:type <${resourceClassIri.value}> ; + | rdfs:label "$label" ; + | knora-base:isDeleted false ; + | knora-base:attachedToUser <${userIri.value}> ; + | knora-base:attachedToProject <${projectIri.value}> ; + | knora-base:hasPermissions "$permissions" ; + | knora-base:creationDate "$creationDate"^^xsd:dateTime ; + | . + | rdf:type ; + | knora-base:isDeleted false ; + | knora-base:valueHasString "42" ; + | knora-base:valueHasUUID "${UuidUtil.base64Encode(intValueDefinition.valueUUID)}" ; + | knora-base:attachedToUser <${valueCreator.value}> ; + | knora-base:hasPermissions "$valuePermissions" ; + | knora-base:valueHasOrder 1 ; + | knora-base:valueCreationDate "$valueCreationDate"^^xsd:dateTime ; + | knora-base:valueHasInteger 42 . + | <${resourceIri.value}> . + | rdf:type ; + | knora-base:isDeleted false ; + | knora-base:valueHasString "true" ; + | knora-base:valueHasUUID "${UuidUtil.base64Encode(boolValueDefinition.valueUUID)}" ; + | knora-base:attachedToUser <${valueCreator.value}> ; + | knora-base:hasPermissions "$valuePermissions" ; + | knora-base:valueHasOrder 1 ; + | knora-base:valueCreationDate "$valueCreationDate"^^xsd:dateTime ; + | knora-base:valueHasBoolean true . + | } + |} + |""".stripMargin, + ) + + val result = ResourcesRepoLive.createNewResourceQuery(graphIri, resource, projectIri, userIri) + assertUpdateQueriesEqual(expected, result) + } + + private val createResourceWithStandoffLinkTest = test("Create a resource with a standoff link value") { + val resource = resourceDefinition.copy(standoffLinks = List(standoffLinkValue)) + val expected = Update( + s"""| + |PREFIX rdf: + |PREFIX rdfs: + |PREFIX xsd: + |PREFIX knora-base: + | + |INSERT DATA { + | GRAPH <${graphIri.value}> { + | <${resourceIri.value}> rdf:type <${resourceClassIri.value}> ; + | rdfs:label "$label" ; + | knora-base:isDeleted false ; + | knora-base:attachedToUser <${userIri.value}> ; + | knora-base:attachedToProject <${projectIri.value}> ; + | knora-base:hasPermissions "$permissions" ; + | knora-base:creationDate "$creationDate"^^xsd:dateTime ; + | ; + | . + | rdf:type ; + | rdf:subject <${resourceIri.value}> ; + | rdf:predicate ; + | rdf:object ; + | knora-base:valueHasString "foo:StandoffLinkTargetIri" ; + | knora-base:valueHasRefCount 2; + | knora-base:isDeleted false ; + | knora-base:valueCreationDate "$creationDate"^^xsd:dateTime ; + | knora-base:attachedToUser <${valueCreator.value}> ; + | knora-base:hasPermissions "$valuePermissions" ; + | knora-base:valueHasUUID "${standoffLinkValue.valueUuid}" . + | } + |} + |""".stripMargin, + ) + val result = ResourcesRepoLive.createNewResourceQuery(graphIri, resource, projectIri, userIri) + assertUpdateQueriesEqual(expected, result) + } + + private val createResourceWithValueAndStandoffLinkTest = + test("Create a resource with a value and a standoff link value") { + val resource = resourceDefinition.copy( + standoffLinks = List(standoffLinkValue), + valueInfos = List(intValueDefinition), + ) + val expected = Update( + s"""| + |PREFIX rdf: + |PREFIX rdfs: + |PREFIX xsd: + |PREFIX knora-base: + | + |INSERT DATA { + | GRAPH <${graphIri.value}> { + | <${resourceIri.value}> rdf:type <${resourceClassIri.value}> ; + | rdfs:label "$label" ; + | knora-base:isDeleted false ; + | knora-base:attachedToUser <${userIri.value}> ; + | knora-base:attachedToProject <${projectIri.value}> ; + | knora-base:hasPermissions "$permissions" ; + | knora-base:creationDate "$creationDate"^^xsd:dateTime ; + | . + | rdf:type ; + | knora-base:isDeleted false ; + | knora-base:valueHasString "42" ; + | knora-base:valueHasUUID "${UuidUtil.base64Encode(intValueDefinition.valueUUID)}" ; + | knora-base:attachedToUser <${valueCreator.value}> ; + | knora-base:hasPermissions "$valuePermissions" ; + | knora-base:valueHasOrder 1 ; + | knora-base:valueCreationDate "$valueCreationDate"^^xsd:dateTime ; + | knora-base:valueHasInteger 42 . + | <${resourceIri.value}> ; + | . + | rdf:type ; + | rdf:subject <${resourceIri.value}> ; + | rdf:predicate ; + | rdf:object ; + | knora-base:valueHasString "foo:StandoffLinkTargetIri" ; + | knora-base:valueHasRefCount 2; + | knora-base:isDeleted false ; + | knora-base:valueCreationDate "$creationDate"^^xsd:dateTime ; + | knora-base:attachedToUser <${valueCreator.value}> ; + | knora-base:hasPermissions "$valuePermissions" ; + | knora-base:valueHasUUID "${standoffLinkValue.valueUuid}" . + | } + |} + |""".stripMargin, + ) + val result = ResourcesRepoLive.createNewResourceQuery(graphIri, resource, projectIri, userIri) + assertUpdateQueriesEqual(expected, result) + } + + val tests: Spec[StringFormatter, Nothing] = + suite("ResourcesRepoLiveSpec")( + createResourceWithoutValuesTest, + createResourceWithValueSuite, + createResourceWithMultipleValuesTest, + createResourceWithStandoffLinkTest, + createResourceWithValueAndStandoffLinkTest, + ) }