From 43b342e6642bc5985e5df87084fe5b27e8865ea2 Mon Sep 17 00:00:00 2001 From: Balduin Landolt <33053745+BalduinLandolt@users.noreply.github.com> Date: Sat, 15 Jun 2024 10:05:18 +0200 Subject: [PATCH 1/5] refactor: Replace reused datastructures with specific ones for createNewResource.scala.txt template (#3277) --- .../twirl/SparqlTemplateLinkUpdate.scala | 91 ++++- .../resources/CreateResourceV2Handler.scala | 161 ++++++++- .../sparql/v2/createNewResource.scala.txt | 310 +++++++----------- .../repo/service/ResourcesRepoLiveSpec.scala | 22 +- 4 files changed, 377 insertions(+), 207 deletions(-) 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 68248028ec..04e1614589 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 @@ -10,7 +10,8 @@ import java.util.UUID import org.knora.webapi.IRI import org.knora.webapi.messages.SmartIri -import org.knora.webapi.messages.v2.responder.valuemessages.ValueContentV2 +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` @@ -64,11 +65,91 @@ final case class NewLinkValueInfo( final case class NewValueInfo( resourceIri: IRI, propertyIri: IRI, - value: ValueContentV2, - newValueIri: IRI, - newValueUUID: UUID, - valueCreator: 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/responders/v2/resources/CreateResourceV2Handler.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/resources/CreateResourceV2Handler.scala index c418d7f235..c9411c859f 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 @@ -16,7 +16,6 @@ import org.knora.webapi.* import org.knora.webapi.config.AppConfig import org.knora.webapi.core.MessageRelay import org.knora.webapi.messages.* -import org.knora.webapi.messages.IriConversions.* import org.knora.webapi.messages.admin.responder.permissionsmessages.DefaultObjectAccessPermissionsStringForResourceClassGetADM import org.knora.webapi.messages.admin.responder.permissionsmessages.DefaultObjectAccessPermissionsStringResponseADM import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionADM @@ -24,6 +23,9 @@ import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionT 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 @@ -449,16 +451,167 @@ final case class CreateResourceV2Handler( // Make a creation date for the value. If a custom creation date is given for a value, consider that otherwise // use resource creation date for the value. valueCreationDate: Instant = valueToCreate.customValueCreationDate.getOrElse(creationDate) + + valueInfo <- + valueToCreate.valueContent match + case DateValueContentV2( + _, + valueHasStartJDN, + valueHasEndJDN, + valueHasStartPrecision, + valueHasEndPrecision, + valueHasCalendar, + _, + ) => + ZIO.succeed( + DateValueInfo( + valueHasStartJDN = valueHasStartJDN, + valueHasEndJDN = valueHasEndJDN, + valueHasStartPrecision = valueHasStartPrecision, + valueHasEndPrecision = valueHasEndPrecision, + valueHasCalendar = valueHasCalendar, + ), + ) + case TextValueContentV2(_, _, valueHasLanguage, _, None, _, _, _) => + ZIO.succeed(UnformattedTextValueInfo(valueHasLanguage)) + case tv @ TextValueContentV2(_, _, valueHasLanguage, _, Some(mappingIri), _, _, _) => + val standoffInfo = tv + .prepareForSparqlInsert(newValueIri) + .map(standoffTag => + StandoffTagInfo( + standoffTagClassIri = standoffTag.standoffNode.standoffTagClassIri.toString(), + standoffTagInstanceIri = standoffTag.standoffTagInstanceIri, + startParentIri = standoffTag.startParentIri, + endParentIri = standoffTag.endParentIri, + 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)), + ), + ) + ZIO + .fromOption(tv.computedMaxStandoffStartIndex) + .orElseFail(StandoffInternalException("Max standoff start index not computed")) + .map(standoffStartIndex => + FormattedTextValueInfo(valueHasLanguage, mappingIri, standoffStartIndex, standoffInfo), + ) + case IntegerValueContentV2(_, valueHasInteger, _) => + ZIO.succeed(IntegerValueInfo(valueHasInteger)) + case DecimalValueContentV2(_, valueHasDecimal, _) => + ZIO.succeed(DecimalValueInfo(valueHasDecimal)) + case BooleanValueContentV2(_, valueHasBoolean, _) => + ZIO.succeed(BooleanValueInfo(valueHasBoolean)) + case GeomValueContentV2(_, valueHasGeometry, _) => + ZIO.succeed(GeomValueInfo(valueHasGeometry)) + case IntervalValueContentV2(_, valueHasIntervalStart, valueHasIntervalEnd, _) => + ZIO.succeed(IntervalValueInfo(valueHasIntervalStart, valueHasIntervalEnd)) + case TimeValueContentV2(_, valueHasTimeStamp, _) => + ZIO.succeed(TimeValueInfo(valueHasTimeStamp)) + case HierarchicalListValueContentV2(_, valueHasListNode, listNodeLabel, _) => + ZIO.succeed(HierarchicalListValueInfo(valueHasListNode)) + case ColorValueContentV2(_, valueHasColor, _) => + ZIO.succeed(ColorValueInfo(valueHasColor)) + case UriValueContentV2(_, valueHasUri, _) => + ZIO.succeed(UriValueInfo(valueHasUri)) + case GeonameValueContentV2(_, valueHasGeonameCode, _) => + ZIO.succeed(GeonameValueInfo(valueHasGeonameCode)) + case StillImageFileValueContentV2(_, fileValue, dimX, dimY, _) => + ZIO.succeed( + StillImageFileValueInfo( + internalFilename = fileValue.internalFilename, + internalMimeType = fileValue.internalMimeType, + originalFilename = fileValue.originalFilename, + originalMimeType = fileValue.originalMimeType, + dimX = dimX, + dimY = dimY, + ), + ) + case StillImageExternalFileValueContentV2(_, fileValue, externalUrl, _) => + ZIO.succeed( + StillImageExternalFileValueInfo( + internalFilename = fileValue.internalFilename, + internalMimeType = fileValue.internalMimeType, + originalFilename = fileValue.originalFilename, + originalMimeType = fileValue.originalMimeType, + externalUrl = externalUrl.value.toString(), + ), + ) + case DocumentFileValueContentV2(_, fileValue, pageCount, dimX, dimY, _) => + ZIO.succeed( + DocumentFileValueInfo( + internalFilename = fileValue.internalFilename, + internalMimeType = fileValue.internalMimeType, + originalFilename = fileValue.originalFilename, + originalMimeType = fileValue.originalMimeType, + dimX = dimX, + dimY = dimY, + pageCount = pageCount, + ), + ) + case ArchiveFileValueContentV2(_, fileValue, _) => + ZIO.succeed( + OtherFileValueInfo( + internalFilename = fileValue.internalFilename, + internalMimeType = fileValue.internalMimeType, + originalFilename = fileValue.originalFilename, + originalMimeType = fileValue.originalMimeType, + ), + ) + case TextFileValueContentV2(_, fileValue, _) => + ZIO.succeed( + OtherFileValueInfo( + internalFilename = fileValue.internalFilename, + internalMimeType = fileValue.internalMimeType, + originalFilename = fileValue.originalFilename, + originalMimeType = fileValue.originalMimeType, + ), + ) + case AudioFileValueContentV2(_, fileValue, _) => + ZIO.succeed( + OtherFileValueInfo( + internalFilename = fileValue.internalFilename, + internalMimeType = fileValue.internalMimeType, + originalFilename = fileValue.originalFilename, + originalMimeType = fileValue.originalMimeType, + ), + ) + case MovingImageFileValueContentV2(_, fileValue, _) => + ZIO.succeed( + OtherFileValueInfo( + internalFilename = fileValue.internalFilename, + internalMimeType = fileValue.internalMimeType, + originalFilename = fileValue.originalFilename, + originalMimeType = fileValue.originalMimeType, + ), + ) + case LinkValueContentV2( + _, + referredResourceIri, + referredResourceExists, + isIncomingLink, + nestedResource, + _, + ) => + ZIO.succeed(LinkValueInfo(referredResourceIri)) + case _: DeletedValueContentV2 => ZIO.fail(BadRequestException("Deleted values cannot be created")) + } yield NewValueInfo( resourceIri = resourceIri, propertyIri = propertyIri.toIri, - value = valueToCreate.valueContent, - newValueIri = newValueIri, - newValueUUID = newValueUUID, + value = valueInfo, + valueIri = newValueIri, + valueTypeIri = valueToCreate.valueContent.valueType.toString(), + valueUUID = newValueUUID, valueCreator = requestingUser.id, valuePermissions = valueToCreate.permissions, creationDate = valueCreationDate, valueHasOrder = valueHasOrder, + valueHasString = valueToCreate.valueContent.valueHasString, + comment = valueToCreate.valueContent.comment, ) } } yield ResourceReadyToCreate( 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 index f09e9782d9..01362fb2e6 100644 --- 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 @@ -5,12 +5,9 @@ @import java.time.Instant @import org.knora.webapi.IRI -@import org.knora.webapi.messages.v2.responder.valuemessages._ -@import org.knora.webapi.messages.twirl.NewLinkValueInfo -@import org.knora.webapi.messages.twirl.NewValueInfo +@import org.knora.webapi.messages.twirl._ +@import org.knora.webapi.messages.twirl.TypeSpecificValueInfo._ @import dsp.valueobjects.UuidUtil -@import dsp.errors.SparqlGenerationException -@import java.util.UUID @** * Creates a new resource. @@ -44,254 +41,195 @@ INSERT DATA { knora-base:creationDate "@creationDate"^^xsd:dateTime . @for(newValueInfo <- newValueInfos) { - # Value: @newValueInfo.newValueIri + # Value: @newValueInfo.valueIri # Property: @newValueInfo.propertyIri @* Construct the value. *@ - <@newValueInfo.newValueIri> rdf:type <@newValueInfo.value.valueType> ; + <@newValueInfo.valueIri> rdf:type <@newValueInfo.valueTypeIri> ; knora-base:isDeleted false ; - knora-base:valueHasString """@newValueInfo.value.valueHasString""" ; - knora-base:valueHasUUID "@{UuidUtil.base64Encode(newValueInfo.newValueUUID)}" . - + knora-base:valueHasString """@newValueInfo.valueHasString""" ; + knora-base:valueHasUUID "@{UuidUtil.base64Encode(newValueInfo.valueUUID)}" . @newValueInfo.value match { - case linkValueContentV2: LinkValueContentV2 => { - - <@newValueInfo.resourceIri> <@newValueInfo.propertyIri.stripSuffix("Value")> <@linkValueContentV2.referredResourceIri> . + case linkValue: LinkValueInfo => { + <@newValueInfo.resourceIri> <@newValueInfo.propertyIri.stripSuffix("Value")> <@linkValue.referredResourceIri> . - <@newValueInfo.newValueIri> rdf:subject <@newValueInfo.resourceIri> ; + <@newValueInfo.valueIri> rdf:subject <@newValueInfo.resourceIri> ; rdf:predicate <@newValueInfo.propertyIri.stripSuffix("Value")> ; - rdf:object <@linkValueContentV2.referredResourceIri> ; + rdf:object <@linkValue.referredResourceIri> ; knora-base:valueHasRefCount 1 . - } - case textValue: TextValueContentV2 => { - - @if(!textValue.valueHasLanguage.isEmpty) { - <@newValueInfo.newValueIri> knora-base:valueHasLanguage """@textValue.valueHasLanguage.get""" . + case textValue: UnformattedTextValueInfo => { + @textValue.valueHasLanguage.map { language => + <@newValueInfo.valueIri> knora-base:valueHasLanguage """@language""" . } + } - @if(textValue.standoff.nonEmpty) { - - @* Create a Standoff node for each standoff tag. *@ - - @textValue.mappingIri match { - case Some(definedMappingIri) => { - <@newValueInfo.newValueIri> knora-base:valueHasMapping <@definedMappingIri> . - } - - case None => {} - } - - <@newValueInfo.newValueIri> knora-base:valueHasMaxStandoffStartIndex @textValue.computedMaxStandoffStartIndex.get . - - @for((createStandoff: CreateStandoffTagV2InTriplestore, standoffNodeIndex) <- textValue.prepareForSparqlInsert(newValueInfo.newValueIri).zipWithIndex) { - - <@newValueInfo.newValueIri> knora-base:valueHasStandoff <@createStandoff.standoffTagInstanceIri> . - - <@createStandoff.standoffTagInstanceIri> - - @* - - Check for optional standoff properties - - *@ - - @if(createStandoff.standoffNode.endIndex.isDefined) { - knora-base:standoffTagHasEndIndex @createStandoff.standoffNode.endIndex.get ; - } - - @if(createStandoff.startParentIri.isDefined) { - knora-base:standoffTagHasStartParent <@createStandoff.startParentIri.get> ; - } - - @if(createStandoff.endParentIri.isDefined) { - knora-base:standoffTagHasEndParent <@createStandoff.endParentIri.get> ; - } - - @if(createStandoff.standoffNode.originalXMLID.isDefined) { - knora-base:standoffTagHasOriginalXMLID """@createStandoff.standoffNode.originalXMLID.get""" ; - } - - @* - - Handle standoff class specific standoff properties - - *@ - @for(createProperty <- createStandoff.standoffNode.attributes) { + 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 . - <@createProperty.standoffPropertyIri> @createProperty.rdfValue ; + @* Create a Standoff node for each standoff tag. *@ + @for(createStandoff: StandoffTagInfo <- textValue.standoff) { - } + <@newValueInfo.valueIri> knora-base:valueHasStandoff <@createStandoff.standoffTagInstanceIri> . - knora-base:standoffTagHasStartIndex @createStandoff.standoffNode.startIndex ; - knora-base:standoffTagHasUUID "@{UuidUtil.base64Encode(createStandoff.standoffNode.uuid)}" ; - knora-base:standoffTagHasStart @createStandoff.standoffNode.startPosition ; - knora-base:standoffTagHasEnd @createStandoff.standoffNode.endPosition ; - rdf:type <@createStandoff.standoffNode.standoffTagClassIri> . + <@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: IntegerValueContentV2 => { - - <@newValueInfo.newValueIri> knora-base:valueHasInteger @intValue.valueHasInteger . - + case intValue: IntegerValueInfo => { + <@newValueInfo.valueIri> knora-base:valueHasInteger @intValue.valueHasInteger . } - case decimalValue: DecimalValueContentV2 => { - - <@newValueInfo.newValueIri> knora-base:valueHasDecimal "@decimalValue.valueHasDecimal"^^xsd:decimal . - + case decimalValue: DecimalValueInfo => { + <@newValueInfo.valueIri> knora-base:valueHasDecimal "@decimalValue.valueHasDecimal"^^xsd:decimal . } - case booleanValue: BooleanValueContentV2 => { - - <@newValueInfo.newValueIri> knora-base:valueHasBoolean @booleanValue.valueHasBoolean . - + case booleanValue: BooleanValueInfo => { + <@newValueInfo.valueIri> knora-base:valueHasBoolean @booleanValue.valueHasBoolean . } - case uriValue: UriValueContentV2 => { - - <@newValueInfo.newValueIri> knora-base:valueHasUri """@uriValue.valueHasUri"""^^xsd:anyURI . - + case uriValue: UriValueInfo => { + <@newValueInfo.valueIri> knora-base:valueHasUri """@uriValue.valueHasUri"""^^xsd:anyURI . } - case dateValue: DateValueContentV2 => { - - <@newValueInfo.newValueIri> knora-base:valueHasStartJDN @dateValue.valueHasStartJDN ; + 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: ColorValueContentV2 => { - - <@newValueInfo.newValueIri> knora-base:valueHasColor """@colorValue.valueHasColor""" . - + case colorValue: ColorValueInfo => { + <@newValueInfo.valueIri> knora-base:valueHasColor """@colorValue.valueHasColor""" . } - case geometryValue: GeomValueContentV2 => { - - <@newValueInfo.newValueIri> knora-base:valueHasGeometry """@geometryValue.valueHasGeometry""" . - + case geometryValue: GeomValueInfo => { + <@newValueInfo.valueIri> knora-base:valueHasGeometry """@geometryValue.valueHasGeometry""" . } - case fileValueContentV2: FileValueContentV2 => { - <@newValueInfo.newValueIri> knora-base:internalFilename """@fileValueContentV2.fileValue.internalFilename""" ; - knora-base:internalMimeType """@fileValueContentV2.fileValue.internalMimeType""" . - - @fileValueContentV2.fileValue.originalFilename match { - case Some(definedOriginalFilename) => { - <@newValueInfo.newValueIri> knora-base:originalFilename """@definedOriginalFilename""" . - } - - case None => {} + 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""" . } - - @fileValueContentV2.fileValue.originalMimeType match { - case Some(definedOriginalMimeType) => { - <@newValueInfo.newValueIri> knora-base:originalMimeType """@definedOriginalMimeType""" . - } - - case None => {} + @fileValue.originalMimeType.map { originalMimeType => + <@newValueInfo.valueIri> knora-base:originalMimeType """@originalMimeType""" . } + } - @fileValueContentV2 match { - case stillImageFileValue: StillImageFileValueContentV2 => { - <@newValueInfo.newValueIri> knora-base:dimX @stillImageFileValue.dimX ; - knora-base:dimY @stillImageFileValue.dimY . - } - - case stillImageFileValue: StillImageExternalFileValueContentV2 => { - <@newValueInfo.newValueIri> knora-base:externalUrl """@stillImageFileValue.externalUrl.value.toString""" . - } - - case documentFileValue: DocumentFileValueContentV2 => { - @documentFileValue.dimX match { - case Some(definedDimX) => { - <@newValueInfo.newValueIri> knora-base:dimX @definedDimX . - } - - case None => {} - } - - @documentFileValue.dimY match { - case Some(definedDimY) => { - <@newValueInfo.newValueIri> knora-base:dimY @definedDimY . - } - - case None => {} - } - - @documentFileValue.pageCount match { - case Some(definedPageCount) => { - <@newValueInfo.newValueIri> knora-base:pageCount @definedPageCount . - } - - case None => {} - } - } - - case _ => {} + 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 listValue: HierarchicalListValueContentV2 => { - - <@newValueInfo.newValueIri> knora-base:valueHasListNode <@listValue.valueHasListNode> . - + 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 intervalValue: IntervalValueContentV2 => { - - <@newValueInfo.newValueIri> knora-base:valueHasIntervalStart "@intervalValue.valueHasIntervalStart"^^xsd:decimal ; - knora-base:valueHasIntervalEnd "@intervalValue.valueHasIntervalEnd"^^xsd:decimal . - + 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 timeValue: TimeValueContentV2 => { - - <@newValueInfo.newValueIri> knora-base:valueHasTimeStamp "@timeValue.valueHasTimeStamp"^^xsd:dateTime . - + case listValue: HierarchicalListValueInfo => { + <@newValueInfo.valueIri> knora-base:valueHasListNode <@listValue.valueHasListNode> . } - case geonameValue: GeonameValueContentV2 => { - - <@newValueInfo.newValueIri> knora-base:valueHasGeonameCode """@geonameValue.valueHasGeonameCode""" . + 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 other => { - @{throw SparqlGenerationException(s"Value object $other is not supported in this SPARQL template"); ()} + case geonameValue: GeonameValueInfo => { + <@newValueInfo.valueIri> knora-base:valueHasGeonameCode """@geonameValue.valueHasGeonameCode""" . } } @* Insert the value's comment, if given. *@ - @newValueInfo.value.comment match { - case Some(commentStr) => { - <@newValueInfo.newValueIri> knora-base:valueHasComment """@commentStr""" . - } - - case None => {} + @newValueInfo.comment.map { commentStr => + <@newValueInfo.valueIri> knora-base:valueHasComment """@commentStr""" . } - <@newValueInfo.newValueIri> knora-base:attachedToUser <@newValueInfo.valueCreator> ; + <@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.newValueIri> . + <@newValueInfo.resourceIri> <@newValueInfo.propertyIri> <@newValueInfo.valueIri> . } 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 2839995b21..28833bf13f 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 @@ -12,11 +12,11 @@ import java.time.Instant import java.util.UUID import dsp.valueobjects.UuidUtil -import org.knora.webapi.ApiV2Complex +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.v2.responder.valuemessages.IntegerValueContentV2 +import org.knora.webapi.messages.twirl.TypeSpecificValueInfo import org.knora.webapi.slice.resourceinfo.domain.InternalIri import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Update @@ -109,13 +109,16 @@ object ResourcesRepoLiveSpec extends ZIOSpecDefault { NewValueInfo( resourceIri = resourceIri, propertyIri = "fooProperty", - value = IntegerValueContentV2(ApiV2Complex, 42), - newValueIri = valueIri, - newValueUUID = uuid, - valueCreator = valueCreator, + valueIri = valueIri, + valueTypeIri = OntologyConstants.KnoraBase.IntValue, + valueUUID = uuid, + value = TypeSpecificValueInfo.IntegerValueInfo(42), valuePermissions = valuePermissions, + valueCreator = valueCreator, creationDate = valueCreationDate, valueHasOrder = 1, + valueHasString = "42", + comment = None, ), ), linkUpdates = Seq.empty, @@ -144,16 +147,13 @@ object ResourcesRepoLiveSpec extends ZIOSpecDefault { | # Property: fooProperty | | - | rdf:type ; + | rdf:type ; | knora-base:isDeleted false ; | knora-base:valueHasString \"\"\"42\"\"\" ; | knora-base:valueHasUUID \"$uuidEncoded\" . | - | | - | | knora-base:valueHasInteger 42 . - | | | | @@ -164,8 +164,6 @@ object ResourcesRepoLiveSpec extends ZIOSpecDefault { | knora-base:valueHasOrder 1 ; | knora-base:valueCreationDate \"2024-01-01T10:00:00.673298Z\"^^xsd:dateTime . | - | - | | | . | From 44e592f3613b89bbf75ccc79070572df3e891631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Mon, 17 Jun 2024 09:44:26 +0200 Subject: [PATCH 2/5] chore: Add integration tests for erasing a project (DEV-3681) (#3283) --- build.sbt | 4 +- .../src/test/resources/application.conf | 24 ++- .../scala/org/knora/webapi/E2EZSpec.scala | 45 +++++ .../org/knora/webapi/ProjectEraseIT.scala | 183 ++++++++++++++++++ .../e2e/v2/OntologyFormatsE2ESpec.scala | 1 + .../webapi/e2ez/KnoraBaseJsonModels.scala | 5 + .../IntegrationTestListADMJsonProtocol.scala | 5 + .../domain/service/KnoraUserService.scala | 27 ++- 8 files changed, 276 insertions(+), 18 deletions(-) create mode 100644 integration/src/test/scala/org/knora/webapi/ProjectEraseIT.scala diff --git a/build.sbt b/build.sbt index afd00b09ae..7ef7c83059 100644 --- a/build.sbt +++ b/build.sbt @@ -82,11 +82,11 @@ lazy val root: Project = Project(id = "root", file(".")) addCommandAlias("fmt", "; all root/scalafmtSbt root/scalafmtAll; root/scalafixAll") addCommandAlias( "headerCreateAll", - "; all webapi/headerCreate webapi/Test/headerCreate integration/headerCreate", + "; all webapi/headerCreate webapi/Test/headerCreate integration/headerCreate integration/Test/headerCreate", ) addCommandAlias( "headerCheckAll", - "; all webapi/headerCheck webapi/Test/headerCheck integration/headerCheck", + "; all webapi/headerCheck webapi/Test/headerCheck integration/headerCheck integration/Test/headerCheck", ) addCommandAlias("check", "; all root/scalafmtSbtCheck root/scalafmtCheckAll; root/scalafixAll --check; headerCheckAll") addCommandAlias("it", "integration/test") diff --git a/integration/src/test/resources/application.conf b/integration/src/test/resources/application.conf index ebc222d338..d91b6568bb 100644 --- a/integration/src/test/resources/application.conf +++ b/integration/src/test/resources/application.conf @@ -1,16 +1,20 @@ include "test" app { - triplestore { - dbtype = "fuseki" + triplestore { + dbtype = "fuseki" - fuseki { - port = 3030 - repository-name = "knora-test-unit" - username = "admin" - password = "test" - } - - reload-on-start = false + fuseki { + port = 3030 + repository-name = "knora-test-unit" + username = "admin" + password = "test" } + + reload-on-start = false + } + + features { + allow-erase-projects = true + } } diff --git a/integration/src/test/scala/org/knora/webapi/E2EZSpec.scala b/integration/src/test/scala/org/knora/webapi/E2EZSpec.scala index 4226e400ae..89062b423d 100644 --- a/integration/src/test/scala/org/knora/webapi/E2EZSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/E2EZSpec.scala @@ -16,6 +16,8 @@ import org.knora.webapi.core.AppServer import org.knora.webapi.core.LayersTest import org.knora.webapi.core.TestStartupUtils import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject +import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode +import org.knora.webapi.slice.admin.domain.model.UserIri abstract class E2EZSpec extends ZIOSpecDefault with TestStartupUtils { @@ -94,6 +96,18 @@ abstract class E2EZSpec extends ZIOSpecDefault with TestStartupUtils { data <- response.body.asString.mapError(_.getMessage) } yield data + def sendDeleteRequest(url: String, token: Option[String]): ZIO[env, String, Response] = + (for { + client <- ZIO.service[Client] + urlStr = s"http://localhost:3333$url" + urlFull <- ZIO.fromEither(URL.decode(urlStr)) + _ <- ZIO.logDebug(s"POST ${urlFull.encode}") + bearer = token.map(Header.Authorization.Bearer.apply) + headers = Headers(List(Header.ContentType(MediaType.application.json)) ++ bearer.toList) + request = Request.delete(urlFull).addHeaders(headers) + response <- client.request(request) + } yield response).mapError(_.getMessage) + def getToken(email: String, password: String): ZIO[env, String, String] = for { response <- @@ -122,4 +136,35 @@ abstract class E2EZSpec extends ZIOSpecDefault with TestStartupUtils { } yield lmd.value } + object AdminApiRestClient { + private val shortcodePlaceholder = ":shortcode" + private def replace(shortcode: Shortcode, url: String) = url.replace(shortcodePlaceholder, shortcode.value) + + val projectsShortcodePath: String = s"/admin/projects/shortcode/$shortcodePlaceholder" + val projectsShortcodeErasePath: String = s"$projectsShortcodePath/erase" + + private val userIriPlaceholder = ":userIri" + private def replace(userIri: UserIri, url: String) = url.replace(userIriPlaceholder, userIri.value) + + val usersPath = "/admin/users" + val usersIriPath = s"$usersPath/iri/$userIriPlaceholder" + val usersIriProjectMemberships = s"$usersIriPath/project-memberships" + val usersIriProjectAdminMemberships = s"$usersIriPath/project-admin-memberships" + val usersIriGroupMemberships = s"$usersIriPath/group-memberships" + + def eraseProjectAsRoot(shortcode: Shortcode): ZIO[env, String, Response] = for { + jwt <- getRootToken.map(Some.apply) + response <- sendDeleteRequest(replace(shortcode, projectsShortcodeErasePath), jwt) + } yield response + + def getProjectAsRoot(shortcode: Shortcode): ZIO[env, String, Response] = for { + jwt <- getRootToken.map(Some.apply) + response <- sendGetRequest(replace(shortcode, projectsShortcodePath)) + } yield response + + def getUserAsRoot(userIri: UserIri): ZIO[env, IRI, Response] = for { + jwt <- getRootToken.map(Some.apply) + response <- sendGetRequest(replace(userIri, usersIriPath)) + } yield response + } } diff --git a/integration/src/test/scala/org/knora/webapi/ProjectEraseIT.scala b/integration/src/test/scala/org/knora/webapi/ProjectEraseIT.scala new file mode 100644 index 0000000000..7c4a34f557 --- /dev/null +++ b/integration/src/test/scala/org/knora/webapi/ProjectEraseIT.scala @@ -0,0 +1,183 @@ +/* + * 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 +import zio.ZIO +import zio.http.Status +import zio.test.Spec +import zio.test.TestAspect +import zio.test.assertTrue + +import dsp.valueobjects.LanguageCode +import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2 +import org.knora.webapi.slice.admin.api.GroupsRequests.GroupCreateRequest +import org.knora.webapi.slice.admin.api.UsersEndpoints.Requests.UserCreateRequest +import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequestsAndResponses.ProjectCreateRequest +import org.knora.webapi.slice.admin.domain.model.Email +import org.knora.webapi.slice.admin.domain.model.FamilyName +import org.knora.webapi.slice.admin.domain.model.GivenName +import org.knora.webapi.slice.admin.domain.model.GroupDescriptions +import org.knora.webapi.slice.admin.domain.model.GroupIri +import org.knora.webapi.slice.admin.domain.model.GroupName +import org.knora.webapi.slice.admin.domain.model.GroupSelfJoin +import org.knora.webapi.slice.admin.domain.model.GroupStatus +import org.knora.webapi.slice.admin.domain.model.KnoraProject +import org.knora.webapi.slice.admin.domain.model.KnoraProject.Description +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri +import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode +import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortname +import org.knora.webapi.slice.admin.domain.model.Password +import org.knora.webapi.slice.admin.domain.model.SystemAdmin +import org.knora.webapi.slice.admin.domain.model.UserIri +import org.knora.webapi.slice.admin.domain.model.UserStatus +import org.knora.webapi.slice.admin.domain.model.Username +import org.knora.webapi.slice.admin.domain.service.KnoraGroupService +import org.knora.webapi.slice.admin.domain.service.KnoraProjectService +import org.knora.webapi.slice.admin.domain.service.KnoraUserService +import org.knora.webapi.slice.admin.domain.service.ProjectService +import org.knora.webapi.slice.resourceinfo.domain.InternalIri +import org.knora.webapi.store.triplestore.api.TriplestoreService +import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Ask +import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Update + +object ProjectEraseIT extends E2EZSpec { + + private val users = ZIO.serviceWithZIO[KnoraUserService] + private val projects = ZIO.serviceWithZIO[KnoraProjectService] + private val groups = ZIO.serviceWithZIO[KnoraGroupService] + private val db = ZIO.serviceWithZIO[TriplestoreService] + + private def getUser(userIri: UserIri) = users(_.findById(userIri)).someOrFail(Exception(s"Must be present $userIri")) + private val shortcode = Shortcode.unsafeFrom("9999") + private val getProject = projects(_.findByShortcode(shortcode)).someOrFail(Exception(s"Must be present $shortcode")) + + private val createProject = projects( + _.createProject( + ProjectCreateRequest( + None, + Shortname.unsafeFrom("TestPrj"), + Shortcode.unsafeFrom("9999"), + None, + List(Description.unsafeFrom("description", None)), + List.empty, + None, + KnoraProject.Status.Active, + KnoraProject.SelfJoin.CanJoin, + ), + ), + ).orDie + + private val createUser = users( + _.createNewUser( + UserCreateRequest( + None, + Username.unsafeFrom("donald.duck"), + Email.unsafeFrom("donald.duck@example.com"), + GivenName.unsafeFrom("Donald"), + FamilyName.unsafeFrom("Duck"), + Password.unsafeFrom("test"), + UserStatus.from(true), + LanguageCode.en, + SystemAdmin.IsNotSystemAdmin, + ), + ), + ) + + private def createGroup(project: KnoraProject) = groups( + _.createGroup( + GroupCreateRequest( + None, + GroupName.unsafeFrom("group"), + GroupDescriptions.unsafeFrom(Seq(StringLiteralV2.unsafeFrom("group description", None))), + project.id, + GroupStatus.active, + GroupSelfJoin.enabled, + ), + project, + ), + ) + + private val createUserWithMemberships = for { + user <- createUser + project <- getProject + group <- createGroup(project) + user <- users(_.addUserToGroup(user, group)) + user <- users(_.addUserToProject(user, project)) + user <- users(_.addUserToProjectAsAdmin(user, project)) + } yield (user, group) + + override def e2eSpec: Spec[ProjectEraseIT.env, Any] = + suiteAll(s"The erasing project endpoint ${AdminApiRestClient.projectsShortcodeErasePath}") { + + suite("given project to delete is not present")( + test("when called as root then it responds with Not Found") { + for { + resp <- AdminApiRestClient.eraseProjectAsRoot(shortcode) + } yield assertTrue(resp.status == Status.NotFound) + }, + ) + + suite("given project to delete is present")( + test("when called as root then it should delete the project and respond with Ok") { + for { + before <- AdminApiRestClient.getProjectAsRoot(shortcode) + erased <- AdminApiRestClient.eraseProjectAsRoot(shortcode) + after <- AdminApiRestClient.getProjectAsRoot(shortcode) + } yield assertTrue( + before.status == Status.Ok, + erased.status == Status.Ok, + after.status == Status.NotFound, + ) + }, + test("when called as root then it should delete user memberships and groups and respond with Ok") { + for { + // given + userAndGroup <- createUserWithMemberships + (user, group) = userAndGroup + project <- getProject + + // when + erased <- AdminApiRestClient.eraseProjectAsRoot(shortcode) + + // then + user <- getUser(user.id) + groupWasDeleted <- groups(_.findById(group.id)).map(_.isEmpty) + } yield assertTrue( + erased.status == Status.Ok, + !user.isInProject.contains(project.id), + !user.isInProjectAdminGroup.contains(project.id), + !user.isInGroup.contains(group), + groupWasDeleted, + ) + }, + test("when called as root then it should delete the project graph") { + def doesGraphExist(graphName: InternalIri) = db(_.query(Ask(s"ASK { GRAPH <${graphName.value}> {} }"))) + for { + // given + project <- getProject + graphName = ProjectService.projectDataNamedGraphV2(project) + _ <- // insert something into the project graph, otherwise it does not exist + db(_.query(Update(s""" + |INSERT DATA { + | GRAPH <${graphName.value}> { + | "value". + | } + |}""".stripMargin))) + graphExisted <- doesGraphExist(graphName) + + // when + erased <- AdminApiRestClient.eraseProjectAsRoot(shortcode) + + // then + graphDeleted <- doesGraphExist(graphName).negate + } yield assertTrue( + erased.status == Status.Ok, + graphExisted, + graphDeleted, + ) + }, + ) @@ TestAspect.before(createProject) + } +} diff --git a/integration/src/test/scala/org/knora/webapi/e2e/v2/OntologyFormatsE2ESpec.scala b/integration/src/test/scala/org/knora/webapi/e2e/v2/OntologyFormatsE2ESpec.scala index 7845002796..a99447ea21 100644 --- a/integration/src/test/scala/org/knora/webapi/e2e/v2/OntologyFormatsE2ESpec.scala +++ b/integration/src/test/scala/org/knora/webapi/e2e/v2/OntologyFormatsE2ESpec.scala @@ -2,6 +2,7 @@ * 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.e2e.v2 import org.apache.pekko.http.scaladsl.model.* diff --git a/integration/src/test/scala/org/knora/webapi/e2ez/KnoraBaseJsonModels.scala b/integration/src/test/scala/org/knora/webapi/e2ez/KnoraBaseJsonModels.scala index e75eea9a7a..4d3188b62a 100644 --- a/integration/src/test/scala/org/knora/webapi/e2ez/KnoraBaseJsonModels.scala +++ b/integration/src/test/scala/org/knora/webapi/e2ez/KnoraBaseJsonModels.scala @@ -1,3 +1,8 @@ +/* + * 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.e2ez import zio.json.* diff --git a/integration/src/test/scala/org/knora/webapi/messages/admin/responder/listsmessages/IntegrationTestListADMJsonProtocol.scala b/integration/src/test/scala/org/knora/webapi/messages/admin/responder/listsmessages/IntegrationTestListADMJsonProtocol.scala index 668236504b..df67a07303 100644 --- a/integration/src/test/scala/org/knora/webapi/messages/admin/responder/listsmessages/IntegrationTestListADMJsonProtocol.scala +++ b/integration/src/test/scala/org/knora/webapi/messages/admin/responder/listsmessages/IntegrationTestListADMJsonProtocol.scala @@ -1,3 +1,8 @@ +/* + * 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.messages.admin.responder.listsmessages import org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraUserService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraUserService.scala index e88fd8ad37..2f4b7af7ba 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraUserService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraUserService.scala @@ -140,6 +140,12 @@ case class KnoraUserService( user <- updateUser(user, UserChangeRequest(groups = Some(user.isInGroup :+ group.groupIri))).orDie } yield user + def addUserToGroup(user: KnoraUser, group: KnoraGroup): UIO[KnoraUser] = + addUserToGroup(user, group.id) + + private def addUserToGroup(user: KnoraUser, groupIri: GroupIri): UIO[KnoraUser] = + updateUser(user, UserChangeRequest(groups = Some(user.isInGroup :+ groupIri))).orDie + def removeUserFromGroup(user: User, group: Group): IO[NotGroupMember, KnoraUser] = userRepo.findById(user.userIri).someOrFailException.orDie.flatMap(removeUserFromGroup(_, group)) @@ -156,9 +162,14 @@ case class KnoraUserService( def addUserToProject(user: KnoraUser, project: Project): IO[IsProjectMember, KnoraUser] = for { _ <- ZIO.fail(IsProjectMember(user.id, project.projectIri)).when(user.isInProject.contains(project.projectIri)) - user <- updateUser(user, UserChangeRequest(projects = Some(user.isInProject :+ project.projectIri))).orDie + user <- addUserToProject(user, project.projectIri) } yield user + def addUserToProject(user: KnoraUser, project: KnoraProject): UIO[KnoraUser] = addUserToProject(user, project.id) + + private def addUserToProject(user: KnoraUser, projectIri: ProjectIri): UIO[KnoraUser] = + updateUser(user, UserChangeRequest(projects = Some(user.isInProject :+ projectIri))).orDie + /** * Removes a user from a project. * If the user is a member of the project admin group, the user is also removed from the project admin group. @@ -195,14 +206,18 @@ case class KnoraUserService( ): IO[IsProjectAdminMember | NotProjectMember, KnoraUser] = { val projectIri = project.projectIri for { - _ <- - ZIO.fail(IsProjectAdminMember(user.id, projectIri)).when(user.isInProjectAdminGroup.contains(projectIri)) - _ <- ZIO.fail(NotProjectMember(user.id, projectIri)).unless(user.isInProject.contains(projectIri)) - theChange = UserChangeRequest(projectsAdmin = Some(user.isInProjectAdminGroup :+ projectIri)) - user <- updateUser(user, theChange).orDie + _ <- ZIO.fail(IsProjectAdminMember(user.id, projectIri)).when(user.isInProjectAdminGroup.contains(projectIri)) + _ <- ZIO.fail(NotProjectMember(user.id, projectIri)).unless(user.isInProject.contains(projectIri)) + _ <- addUserToProjectAsAdmin(user, projectIri) } yield user } + def addUserToProjectAsAdmin(user: KnoraUser, project: KnoraProject): UIO[KnoraUser] = + addUserToProjectAsAdmin(user, project.id) + + private def addUserToProjectAsAdmin(user: KnoraUser, projectIri: ProjectIri): UIO[KnoraUser] = + updateUser(user, UserChangeRequest(projectsAdmin = Some(user.isInProjectAdminGroup :+ projectIri))).orDie + /** * Removes a user from the project admin group of a project. * The user must already be an admin member of the project. From f15eb4e6a96752879682800de63d5e4d2b955be2 Mon Sep 17 00:00:00 2001 From: DaSCH Bot <50987250+daschbot@users.noreply.github.com> Date: Mon, 17 Jun 2024 10:15:29 +0200 Subject: [PATCH 3/5] chore: Major dependency updates (#3286) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Christian Kleinbölting --- project/Dependencies.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 2b8c224340..e27d9c1944 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -36,7 +36,7 @@ object Dependencies { val zioConfig = "dev.zio" %% "zio-config" % ZioConfigVersion val zioConfigMagnolia = "dev.zio" %% "zio-config-magnolia" % ZioConfigVersion val zioConfigTypesafe = "dev.zio" %% "zio-config-typesafe" % ZioConfigVersion - val zioJson = "dev.zio" %% "zio-json" % "0.6.2" + val zioJson = "dev.zio" %% "zio-json" % "0.7.0" val zioLogging = "dev.zio" %% "zio-logging" % ZioLoggingVersion val zioLoggingSlf4jBridge = "dev.zio" %% "zio-logging-slf4j2-bridge" % ZioLoggingVersion val zioNio = "dev.zio" %% "zio-nio" % ZioNioVersion @@ -47,7 +47,7 @@ object Dependencies { // refined val refined = Seq( "eu.timepit" %% "refined" % "0.11.1", - "dev.zio" %% "zio-json-interop-refined" % "0.6.2", + "dev.zio" %% "zio-json-interop-refined" % "0.7.0", ) // zio-test and friends From 844bfa0bdb4adbbc70ffe0337202d4d8395d6155 Mon Sep 17 00:00:00 2001 From: DaSCH Bot <50987250+daschbot@users.noreply.github.com> Date: Mon, 17 Jun 2024 10:39:26 +0200 Subject: [PATCH 4/5] chore: Minor dependency updates (#3285) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Christian Kleinbölting Co-authored-by: Christian Kleinbölting --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index e27d9c1944..9b352b7bc0 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -46,7 +46,7 @@ object Dependencies { // refined val refined = Seq( - "eu.timepit" %% "refined" % "0.11.1", + "eu.timepit" %% "refined" % "0.11.2", "dev.zio" %% "zio-json-interop-refined" % "0.7.0", ) From 1ed1506893b721f44c76f1fc316cb4e3767d9768 Mon Sep 17 00:00:00 2001 From: DaSCH Bot <50987250+daschbot@users.noreply.github.com> Date: Mon, 17 Jun 2024 11:30:34 +0200 Subject: [PATCH 5/5] chore: Patch dependency updates (#3284) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Christian Kleinbölting --- .git-blame-ignore-revs | 3 +++ .scalafmt.conf | 2 +- .../responders/v2/OntologyResponderV2Spec.scala | 2 +- project/Dependencies.scala | 11 +++++++---- webapi/src/main/scala/dsp/valueobjects/Iri.scala | 2 +- .../webapi/responders/v2/ResourcesResponderV2.scala | 10 +++------- .../responders/v2/ontology/CardinalityHandler.scala | 4 ++-- 7 files changed, 18 insertions(+), 16 deletions(-) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 537013a0a3..17c23f958d 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,2 +1,5 @@ # Scala Steward: Reformat with scalafmt 3.8.0 870d2491e283d5f6e329fa64ce0725f512dcadae + +# Scala Steward: Reformat with scalafmt 3.8.2 +318a790409a0dab067a060031cbc7c77ab2168e2 diff --git a/.scalafmt.conf b/.scalafmt.conf index fa9faaeb95..ce2f8b2f5d 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = "3.8.1" +version = "3.8.2" runner.dialect = scala3 maxColumn = 120 align.preset = most diff --git a/integration/src/test/scala/org/knora/webapi/responders/v2/OntologyResponderV2Spec.scala b/integration/src/test/scala/org/knora/webapi/responders/v2/OntologyResponderV2Spec.scala index 9f7ff090d7..d8b0fcead1 100644 --- a/integration/src/test/scala/org/knora/webapi/responders/v2/OntologyResponderV2Spec.scala +++ b/integration/src/test/scala/org/knora/webapi/responders/v2/OntologyResponderV2Spec.scala @@ -436,7 +436,7 @@ class OntologyResponderV2Spec extends CoreSpec with ImplicitSender { "", // rdf:type anything:Thing "", // rdf:type anything:BlueThing, a subclass of anything:Thing "", // a subclass of anything:Thing in another ontology - "",// a subproperty of anything:hasOtherThing in another ontology + "", // a subproperty of anything:hasOtherThing in another ontology ) expectedSubjects.forall(s => errorMsg.contains(s)) should ===(true) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 9b352b7bc0..8c21ebf6ac 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -21,7 +21,7 @@ object Dependencies { val PekkoActorVersion = "1.0.2" val PekkoHttpVersion = "1.0.1" val JenaVersion = "5.0.0" - val Rdf4jVersion = "4.3.11" + val Rdf4jVersion = "4.3.12" val ZioConfigVersion = "4.0.2" val ZioLoggingVersion = "2.3.0" @@ -29,7 +29,7 @@ object Dependencies { val ZioMetricsConnectorsVersion = "2.3.1" val ZioPreludeVersion = "1.0.0-RC27" val ZioSchemaVersion = "0.2.0" - val ZioVersion = "2.1.1" + val ZioVersion = "2.1.3" // ZIO val zio = "dev.zio" %% "zio" % ZioVersion @@ -80,7 +80,10 @@ object Dependencies { val jwtSprayJson = "com.github.jwt-scala" %% "jwt-zio-json" % "10.0.1" // jwtSprayJson -> 9.0.2 is the latest version that's compatible with spray-json; if it wasn't for spray, this would be Scala 3 compatible val springSecurityCore = - "org.springframework.security" % "spring-security-core" % "6.3.0" exclude ("commons-logging", "commons-logging") exclude ("org.springframework", "spring-aop") + "org.springframework.security" % "spring-security-core" % "6.3.0" exclude ( + "commons-logging", + "commons-logging", + ) exclude ("org.springframework", "spring-aop") val bouncyCastle = "org.bouncycastle" % "bcprov-jdk15to18" % "1.78.1" // caching @@ -112,7 +115,7 @@ object Dependencies { // found/added by the plugin but deleted anyway val commonsLang3 = "org.apache.commons" % "commons-lang3" % "3.14.0" - val tapirVersion = "1.10.8" + val tapirVersion = "1.10.9" val tapir = Seq( "com.softwaremill.sttp.tapir" %% "tapir-pekko-http-server" % tapirVersion, diff --git a/webapi/src/main/scala/dsp/valueobjects/Iri.scala b/webapi/src/main/scala/dsp/valueobjects/Iri.scala index e4f69152ba..2ddc2f1656 100644 --- a/webapi/src/main/scala/dsp/valueobjects/Iri.scala +++ b/webapi/src/main/scala/dsp/valueobjects/Iri.scala @@ -26,7 +26,7 @@ object Iri { val urlValidator = new UrlValidator( Array("http", "https"), // valid URL schemes - UrlValidator.ALLOW_LOCAL_URLS,// local URLs are URL-encoded IRIs as part of the whole URL + UrlValidator.ALLOW_LOCAL_URLS, // local URLs are URL-encoded IRIs as part of the whole URL ) object KnoraInternal { diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index 5b45056647..a91d15eb2e 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -1867,13 +1867,9 @@ final case class ResourcesResponderV2( // find the version of resource which has a value with previousValueIri versionDateAndPreviousVersion <- ZIO - .fromOption( - allResourceVersions.find { case (_, resource) => - resource.values.exists(item => - item._1 == propertyIri && item._2.exists(value => value.valueIri == previousValueIri), - ) - }, - ) + .fromOption(allResourceVersions.find { case (_, resource) => + resource.values.get(propertyIri).exists(_.exists(_.valueIri == previousValueIri)) + }) .orElseFail(NotFoundException(s"Could not find the previous value of ${currentVersionOfValue.valueIri}")) (previousVersionDate, previousVersionOfResource) = versionDateAndPreviousVersion diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/CardinalityHandler.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/CardinalityHandler.scala index 0cc92dfd47..335a99fdc6 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/CardinalityHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/CardinalityHandler.scala @@ -120,7 +120,7 @@ final case class CardinalityHandler( // turn the propertyIri into a ReadPropertyInfoV2 .map(propertyIri => cacheData.ontologies(propertyIri.getOntologyFromEntity).properties(propertyIri)) .filter(_.isLinkProp) // we are only interested in link properties - .map(_.entityInfoContent.propertyIri),// turn whatever is left back to a propertyIri + .map(_.entityInfoContent.propertyIri), // turn whatever is left back to a propertyIri ) .fold(e => throw e.head, v => v) @@ -262,7 +262,7 @@ final case class CardinalityHandler( // turn the propertyIri into a ReadPropertyInfoV2 .map(propertyIri => cacheData.ontologies(propertyIri.getOntologyFromEntity).properties(propertyIri)) .filter(_.isLinkProp) // we are only interested in link properties - .map(_.entityInfoContent.propertyIri),// turn whatever is left back to a propertyIri + .map(_.entityInfoContent.propertyIri), // turn whatever is left back to a propertyIri ) .fold(e => throw e.head, v => v)