/
ResourcesResponderV2.scala
2249 lines (1941 loc) · 104 KB
/
ResourcesResponderV2.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Copyright © 2015-2021 the contributors (see Contributors.md).
*
* This file is part of Knora.
*
* Knora is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Knora is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with Knora. If not, see <http://www.gnu.org/licenses/>.
*/
package org.knora.webapi.responders.v2
import java.time.Instant
import java.util.UUID
import akka.http.scaladsl.util.FastFuture
import akka.pattern._
import akka.stream.Materializer
import org.knora.webapi._
import org.knora.webapi.exceptions._
import org.knora.webapi.feature.FeatureFactoryConfig
import org.knora.webapi.messages.IriConversions._
import org.knora.webapi.messages.admin.responder.permissionsmessages.{
DefaultObjectAccessPermissionsStringForResourceClassGetADM,
DefaultObjectAccessPermissionsStringResponseADM,
ResourceCreateOperation
}
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM
import org.knora.webapi.messages.admin.responder.usersmessages.UserADM
import org.knora.webapi.messages.store.sipimessages.{SipiGetTextFileRequest, SipiGetTextFileResponse}
import org.knora.webapi.messages.store.triplestoremessages._
import org.knora.webapi.messages.twirl.SparqlTemplateResourceToCreate
import org.knora.webapi.messages.util.ConstructResponseUtilV2.MappingAndXSLTransformation
import org.knora.webapi.messages.util.PermissionUtilADM.{
AGreaterThanB,
DeletePermission,
ModifyPermission,
PermissionComparisonResult
}
import org.knora.webapi.messages.util._
import org.knora.webapi.messages.util.rdf.{SparqlSelectResult, VariableResultsRow}
import org.knora.webapi.messages.util.search.ConstructQuery
import org.knora.webapi.messages.util.search.gravsearch.GravsearchParser
import org.knora.webapi.messages.util.standoff.StandoffTagUtilV2
import org.knora.webapi.messages.v2.responder.ontologymessages._
import org.knora.webapi.messages.v2.responder.resourcemessages._
import org.knora.webapi.messages.v2.responder.searchmessages.GravsearchRequestV2
import org.knora.webapi.messages.v2.responder.standoffmessages.{
GetMappingRequestV2,
GetMappingResponseV2,
GetXSLTransformationRequestV2,
GetXSLTransformationResponseV2
}
import org.knora.webapi.messages.v2.responder.valuemessages._
import org.knora.webapi.messages.v2.responder.{SuccessResponseV2, UpdateResultInProject}
import org.knora.webapi.messages.{OntologyConstants, SmartIri}
import org.knora.webapi.responders.IriLocker
import org.knora.webapi.responders.Responder.handleUnexpectedMessage
import org.knora.webapi.util._
import scala.concurrent.Future
import scala.util.{Failure, Success}
class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithStandoffV2(responderData) {
/* actor materializer needed for http requests */
implicit val materializer: Materializer = Materializer.matFromSystem(system)
/**
* Represents a resource that is ready to be created and whose contents can be verified afterwards.
*
* @param sparqlTemplateResourceToCreate a [[SparqlTemplateResourceToCreate]] describing SPARQL for creating
* the resource.
* @param values the resource's values for verification.
* @param hasStandoffLink `true` if the property `knora-base:hasStandoffLinkToValue` was automatically added.
*/
private case class ResourceReadyToCreate(sparqlTemplateResourceToCreate: SparqlTemplateResourceToCreate,
values: Map[SmartIri, Seq[UnverifiedValueV2]],
hasStandoffLink: Boolean)
/**
* Receives a message of type [[ResourcesResponderRequestV2]], and returns an appropriate response message.
*/
def receive(msg: ResourcesResponderRequestV2) = msg match {
case ResourcesGetRequestV2(resIris,
propertyIri,
valueUuid,
versionDate,
targetSchema,
schemaOptions,
featureFactoryConfig,
requestingUser) =>
getResourcesV2(resIris,
propertyIri,
valueUuid,
versionDate,
targetSchema,
schemaOptions,
featureFactoryConfig,
requestingUser)
case ResourcesPreviewGetRequestV2(resIris, targetSchema, featureFactoryConfig, requestingUser) =>
getResourcePreviewV2(resIris, targetSchema, featureFactoryConfig, requestingUser)
case ResourceTEIGetRequestV2(resIri,
textProperty,
mappingIri,
gravsearchTemplateIri,
headerXSLTIri,
featureFactoryConfig,
requestingUser) =>
getResourceAsTeiV2(resIri,
textProperty,
mappingIri,
gravsearchTemplateIri,
headerXSLTIri,
featureFactoryConfig,
requestingUser)
case createResourceRequestV2: CreateResourceRequestV2 => createResourceV2(createResourceRequestV2)
case updateResourceMetadataRequestV2: UpdateResourceMetadataRequestV2 =>
updateResourceMetadataV2(updateResourceMetadataRequestV2)
case deleteOrEraseResourceRequestV2: DeleteOrEraseResourceRequestV2 =>
deleteOrEraseResourceV2(deleteOrEraseResourceRequestV2)
case graphDataGetRequest: GraphDataGetRequestV2 => getGraphDataResponseV2(graphDataGetRequest)
case resourceHistoryRequest: ResourceVersionHistoryGetRequestV2 => getResourceHistoryV2(resourceHistoryRequest)
case other => handleUnexpectedMessage(other, log, this.getClass.getName)
}
/**
* Creates a new resource.
*
* @param createResourceRequestV2 the request to create the resource.
* @return a [[ReadResourcesSequenceV2]] containing a preview of the resource.
*/
private def createResourceV2(createResourceRequestV2: CreateResourceRequestV2): Future[ReadResourcesSequenceV2] = {
def makeTaskFuture(resourceIri: IRI): Future[ReadResourcesSequenceV2] = {
for {
//check if resourceIri already exists holding a lock on the IRI
result <- stringFormatter.checkIriExists(resourceIri, storeManager)
_ = if (result) {
throw DuplicateValueException(s"Resource IRI: '${resourceIri}' already exists.")
}
// Convert the resource to the internal ontology schema.
internalCreateResource: CreateResourceV2 <- Future(
createResourceRequestV2.createResource.toOntologySchema(InternalSchema))
// Check link targets and list nodes that should exist.
_ <- checkStandoffLinkTargets(
values = internalCreateResource.flatValues,
featureFactoryConfig = createResourceRequestV2.featureFactoryConfig,
requestingUser = createResourceRequestV2.requestingUser
)
_ <- checkListNodes(internalCreateResource.flatValues, createResourceRequestV2.requestingUser)
// Get the class IRIs of all the link targets in the request.
linkTargetClasses: Map[IRI, SmartIri] <- getLinkTargetClasses(
resourceIri: IRI,
internalCreateResources = Seq(internalCreateResource),
featureFactoryConfig = createResourceRequestV2.featureFactoryConfig,
requestingUser = createResourceRequestV2.requestingUser
)
// Get the definitions of the resource class and its properties, as well as of the classes of all
// resources that are link targets.
resourceClassEntityInfoResponse: EntityInfoGetResponseV2 <- (responderManager ? EntityInfoGetRequestV2(
classIris = linkTargetClasses.values.toSet + internalCreateResource.resourceClassIri,
requestingUser = createResourceRequestV2.requestingUser
)).mapTo[EntityInfoGetResponseV2]
resourceClassInfo: ReadClassInfoV2 = resourceClassEntityInfoResponse.classInfoMap(
internalCreateResource.resourceClassIri)
propertyEntityInfoResponse: EntityInfoGetResponseV2 <- (responderManager ? EntityInfoGetRequestV2(
propertyIris = resourceClassInfo.knoraResourceProperties,
requestingUser = createResourceRequestV2.requestingUser
)).mapTo[EntityInfoGetResponseV2]
allEntityInfo = EntityInfoGetResponseV2(
classInfoMap = resourceClassEntityInfoResponse.classInfoMap,
propertyInfoMap = propertyEntityInfoResponse.propertyInfoMap
)
// Get the default permissions of the resource class.
defaultResourcePermissionsMap <- getResourceClassDefaultPermissions(
projectIri = createResourceRequestV2.createResource.projectADM.id,
resourceClassIris = Set(internalCreateResource.resourceClassIri),
requestingUser = createResourceRequestV2.requestingUser
)
defaultResourcePermissions: String = defaultResourcePermissionsMap(internalCreateResource.resourceClassIri)
// Get the default permissions of each property used.
defaultPropertyPermissionsMap: Map[SmartIri, Map[SmartIri, String]] <- getDefaultPropertyPermissions(
projectIri = createResourceRequestV2.createResource.projectADM.id,
resourceClassProperties = Map(internalCreateResource.resourceClassIri -> internalCreateResource.values.keySet),
requestingUser = createResourceRequestV2.requestingUser
)
defaultPropertyPermissions: Map[SmartIri, String] = defaultPropertyPermissionsMap(
internalCreateResource.resourceClassIri)
// Make a versionDate for the resource and its values.
creationDate: Instant = internalCreateResource.creationDate.getOrElse(Instant.now)
// Do the remaining pre-update checks and make a ResourceReadyToCreate describing the SPARQL
// for creating the resource.
resourceReadyToCreate: ResourceReadyToCreate <- generateResourceReadyToCreate(
resourceIri = resourceIri,
internalCreateResource = internalCreateResource,
linkTargetClasses = linkTargetClasses,
entityInfo = allEntityInfo,
clientResourceIDs = Map.empty[IRI, String],
defaultResourcePermissions = defaultResourcePermissions,
defaultPropertyPermissions = defaultPropertyPermissions,
creationDate = creationDate,
featureFactoryConfig = createResourceRequestV2.featureFactoryConfig,
requestingUser = createResourceRequestV2.requestingUser
)
// Get the IRI of the named graph in which the resource will be created.
dataNamedGraph: IRI = stringFormatter.projectDataNamedGraphV2(createResourceRequestV2.createResource.projectADM)
// Generate SPARQL for creating the resource.
sparqlUpdate = org.knora.webapi.messages.twirl.queries.sparql.v2.txt
.createNewResources(
dataNamedGraph = dataNamedGraph,
triplestore = settings.triplestoreType,
resourcesToCreate = Seq(resourceReadyToCreate.sparqlTemplateResourceToCreate),
projectIri = createResourceRequestV2.createResource.projectADM.id,
creatorIri = createResourceRequestV2.requestingUser.id
)
.toString()
// Do the update.
_ <- (storeManager ? SparqlUpdateRequest(sparqlUpdate)).mapTo[SparqlUpdateResponse]
// Verify that the resource was created correctly.
previewOfCreatedResource: ReadResourcesSequenceV2 <- verifyResource(
resourceReadyToCreate = resourceReadyToCreate,
projectIri = createResourceRequestV2.createResource.projectADM.id,
featureFactoryConfig = createResourceRequestV2.featureFactoryConfig,
requestingUser = createResourceRequestV2.requestingUser
)
} yield previewOfCreatedResource
}
val triplestoreUpdateFuture: Future[ReadResourcesSequenceV2] = for {
// Don't allow anonymous users to create resources.
_ <- Future {
if (createResourceRequestV2.requestingUser.isAnonymousUser) {
throw ForbiddenException("Anonymous users aren't allowed to create resources")
} else {
createResourceRequestV2.requestingUser.id
}
}
// Ensure that the project isn't the system project or the shared ontologies project.
projectIri = createResourceRequestV2.createResource.projectADM.id
_ = if (projectIri == OntologyConstants.KnoraAdmin.SystemProject || projectIri == OntologyConstants.KnoraAdmin.DefaultSharedOntologiesProject) {
throw BadRequestException(s"Resources cannot be created in project <$projectIri>")
}
// Ensure that the resource class isn't from a non-shared ontology in another project.
resourceClassOntologyIri: SmartIri = createResourceRequestV2.createResource.resourceClassIri.getOntologyFromEntity
readOntologyMetadataV2: ReadOntologyMetadataV2 <- (responderManager ? OntologyMetadataGetByIriRequestV2(
Set(resourceClassOntologyIri),
createResourceRequestV2.requestingUser)).mapTo[ReadOntologyMetadataV2]
ontologyMetadata: OntologyMetadataV2 = readOntologyMetadataV2.ontologies.headOption
.getOrElse(throw BadRequestException(s"Ontology $resourceClassOntologyIri not found"))
ontologyProjectIri: IRI = ontologyMetadata.projectIri
.getOrElse(throw InconsistentRepositoryDataException(s"Ontology $resourceClassOntologyIri has no project"))
.toString
_ = if (projectIri != ontologyProjectIri && !(ontologyMetadata.ontologyIri.isKnoraBuiltInDefinitionIri || ontologyMetadata.ontologyIri.isKnoraSharedDefinitionIri)) {
throw BadRequestException(
s"Cannot create a resource in project <$projectIri> with resource class <${createResourceRequestV2.createResource.resourceClassIri}>, which is defined in a non-shared ontology in another project")
}
// Check user's PermissionProfile (part of UserADM) to see if the user has the permission to
// create a new resource in the given project.
internalResourceClassIri: SmartIri = createResourceRequestV2.createResource.resourceClassIri
.toOntologySchema(InternalSchema)
_ = if (!createResourceRequestV2.requestingUser.permissions.hasPermissionFor(
ResourceCreateOperation(internalResourceClassIri.toString),
projectIri,
None)) {
throw ForbiddenException(
s"User ${createResourceRequestV2.requestingUser.username} does not have permission to create a resource of class <${createResourceRequestV2.createResource.resourceClassIri}> in project <$projectIri>")
}
resourceIri: IRI <- checkOrCreateEntityIri(
createResourceRequestV2.createResource.resourceIri,
stringFormatter.makeRandomResourceIri(createResourceRequestV2.createResource.projectADM.shortcode))
// Do the remaining pre-update checks and the update while holding an update lock on the resource to be created.
taskResult <- IriLocker.runWithIriLock(
createResourceRequestV2.apiRequestID,
resourceIri,
() => makeTaskFuture(resourceIri)
)
} yield taskResult
// If the request includes file values, tell Sipi to move the files to permanent storage if the update
// succeeded, or to delete the temporary files if the update failed.
doSipiPostUpdateForResources(
updateFuture = triplestoreUpdateFuture,
createResources = Seq(createResourceRequestV2.createResource),
requestingUser = createResourceRequestV2.requestingUser
)
}
/**
* Updates a resources metadata.
*
* @param updateResourceMetadataRequestV2 the update request.
* @return a [[SuccessResponseV2]].
*/
private def updateResourceMetadataV2(
updateResourceMetadataRequestV2: UpdateResourceMetadataRequestV2): Future[SuccessResponseV2] = {
def makeTaskFuture: Future[SuccessResponseV2] = {
for {
// Get the metadata of the resource to be updated.
resourcesSeq: ReadResourcesSequenceV2 <- getResourcePreviewV2(
resourceIris = Seq(updateResourceMetadataRequestV2.resourceIri),
targetSchema = ApiV2Complex,
featureFactoryConfig = updateResourceMetadataRequestV2.featureFactoryConfig,
requestingUser = updateResourceMetadataRequestV2.requestingUser
)
resource: ReadResourceV2 = resourcesSeq.toResource(updateResourceMetadataRequestV2.resourceIri)
internalResourceClassIri = updateResourceMetadataRequestV2.resourceClassIri.toOntologySchema(InternalSchema)
// Make sure that the resource's class is what the client thinks it is.
_ = if (resource.resourceClassIri != internalResourceClassIri) {
throw BadRequestException(
s"Resource <${resource.resourceIri}> is not a member of class <${updateResourceMetadataRequestV2.resourceClassIri}>")
}
// Make sure that the resource hasn't been updated since the client got its last modification date.
_ = if (resource.lastModificationDate != updateResourceMetadataRequestV2.maybeLastModificationDate) {
throw EditConflictException(s"Resource <${resource.resourceIri}> has been modified since you last read it")
}
// Check that the user has permission to modify the resource.
_ = ResourceUtilV2.checkResourcePermission(
resourceInfo = resource,
permissionNeeded = ModifyPermission,
requestingUser = updateResourceMetadataRequestV2.requestingUser
)
// Get the IRI of the named graph in which the resource is stored.
dataNamedGraph: IRI = stringFormatter.projectDataNamedGraphV2(resource.projectADM)
newModificationDate: Instant = updateResourceMetadataRequestV2.maybeNewModificationDate match {
case Some(submittedNewModificationDate) =>
if (resource.lastModificationDate.exists(_.isAfter(submittedNewModificationDate))) {
throw BadRequestException(
s"Submitted knora-api:newModificationDate is before the resource's current knora-api:lastModificationDate")
} else {
submittedNewModificationDate
}
case None => Instant.now
}
// Generate SPARQL for updating the resource.
sparqlUpdate = org.knora.webapi.messages.twirl.queries.sparql.v2.txt
.changeResourceMetadata(
triplestore = settings.triplestoreType,
dataNamedGraph = dataNamedGraph,
resourceIri = updateResourceMetadataRequestV2.resourceIri,
resourceClassIri = internalResourceClassIri,
maybeLastModificationDate = updateResourceMetadataRequestV2.maybeLastModificationDate,
newModificationDate = newModificationDate,
maybeLabel = updateResourceMetadataRequestV2.maybeLabel,
maybePermissions = updateResourceMetadataRequestV2.maybePermissions
)
.toString()
// Do the update.
_ <- (storeManager ? SparqlUpdateRequest(sparqlUpdate)).mapTo[SparqlUpdateResponse]
// Verify that the resource was updated correctly.
updatedResourcesSeq: ReadResourcesSequenceV2 <- getResourcePreviewV2(
resourceIris = Seq(updateResourceMetadataRequestV2.resourceIri),
targetSchema = ApiV2Complex,
featureFactoryConfig = updateResourceMetadataRequestV2.featureFactoryConfig,
requestingUser = updateResourceMetadataRequestV2.requestingUser
)
_ = if (updatedResourcesSeq.resources.size != 1) {
throw AssertionException(s"Expected one resource, got ${resourcesSeq.resources.size}")
}
updatedResource: ReadResourceV2 = updatedResourcesSeq.resources.head
_ = if (!updatedResource.lastModificationDate.contains(newModificationDate)) {
throw UpdateNotPerformedException(
s"Updated resource has last modification date ${updatedResource.lastModificationDate}, expected $newModificationDate")
}
_ = updateResourceMetadataRequestV2.maybeLabel match {
case Some(newLabel) =>
if (!updatedResource.label.contains(stringFormatter.fromSparqlEncodedString(newLabel))) {
throw UpdateNotPerformedException()
}
case None => ()
}
_ = updateResourceMetadataRequestV2.maybePermissions match {
case Some(newPermissions) =>
if (PermissionUtilADM.parsePermissions(updatedResource.permissions) != PermissionUtilADM.parsePermissions(
newPermissions)) {
throw UpdateNotPerformedException()
}
case None => ()
}
// If the resource's label was changed, update the full-text search index.
_ <- updateResourceMetadataRequestV2.maybeLabel match {
case Some(_) =>
for {
_ <- (storeManager ? SearchIndexUpdateRequest(Some(updateResourceMetadataRequestV2.resourceIri)))
.mapTo[SparqlUpdateResponse]
} yield ()
case None => FastFuture.successful(())
}
} yield SuccessResponseV2("Resource metadata updated")
}
for {
// Do the remaining pre-update checks and the update while holding an update lock on the resource.
taskResult <- IriLocker.runWithIriLock(
updateResourceMetadataRequestV2.apiRequestID,
updateResourceMetadataRequestV2.resourceIri,
() => makeTaskFuture
)
} yield taskResult
}
/**
* Either marks a resource as deleted or erases it from the triplestore, depending on the value of `erase`
* in the request message.
*
* @param deleteOrEraseResourceV2 the request message.
*/
private def deleteOrEraseResourceV2(
deleteOrEraseResourceV2: DeleteOrEraseResourceRequestV2): Future[SuccessResponseV2] = {
if (deleteOrEraseResourceV2.erase) {
eraseResourceV2(deleteOrEraseResourceV2)
} else {
markResourceAsDeletedV2(deleteOrEraseResourceV2)
}
}
/**
* Marks a resource as deleted.
*
* @param deleteResourceV2 the request message.
*/
private def markResourceAsDeletedV2(deleteResourceV2: DeleteOrEraseResourceRequestV2): Future[SuccessResponseV2] = {
def makeTaskFuture: Future[SuccessResponseV2] = {
for {
// Get the metadata of the resource to be updated.
resourcesSeq: ReadResourcesSequenceV2 <- getResourcePreviewV2(
resourceIris = Seq(deleteResourceV2.resourceIri),
targetSchema = ApiV2Complex,
featureFactoryConfig = deleteResourceV2.featureFactoryConfig,
requestingUser = deleteResourceV2.requestingUser
)
resource: ReadResourceV2 = resourcesSeq.toResource(deleteResourceV2.resourceIri)
internalResourceClassIri = deleteResourceV2.resourceClassIri.toOntologySchema(InternalSchema)
// Make sure that the resource's class is what the client thinks it is.
_ = if (resource.resourceClassIri != internalResourceClassIri) {
throw BadRequestException(
s"Resource <${resource.resourceIri}> is not a member of class <${deleteResourceV2.resourceClassIri}>")
}
// Make sure that the resource hasn't been updated since the client got its last modification date.
_ = if (resource.lastModificationDate != deleteResourceV2.maybeLastModificationDate) {
throw EditConflictException(s"Resource <${resource.resourceIri}> has been modified since you last read it")
}
// If a custom delete date was provided, make sure it's later than the resource's most recent timestamp.
_ = if (deleteResourceV2.maybeDeleteDate.exists(
!_.isAfter(resource.lastModificationDate.getOrElse(resource.creationDate)))) {
throw BadRequestException(
s"A custom delete date must be later than the date when the resource was created or last modified")
}
// Check that the user has permission to mark the resource as deleted.
_ = ResourceUtilV2.checkResourcePermission(
resourceInfo = resource,
permissionNeeded = DeletePermission,
requestingUser = deleteResourceV2.requestingUser
)
// Get the IRI of the named graph in which the resource is stored.
dataNamedGraph: IRI = stringFormatter.projectDataNamedGraphV2(resource.projectADM)
// Generate SPARQL for marking the resource as deleted.
sparqlUpdate = org.knora.webapi.messages.twirl.queries.sparql.v2.txt
.deleteResource(
triplestore = settings.triplestoreType,
dataNamedGraph = dataNamedGraph,
resourceIri = deleteResourceV2.resourceIri,
maybeDeleteComment = deleteResourceV2.maybeDeleteComment,
currentTime = deleteResourceV2.maybeDeleteDate.getOrElse(Instant.now),
requestingUser = deleteResourceV2.requestingUser.id
)
.toString()
// Do the update.
_ <- (storeManager ? SparqlUpdateRequest(sparqlUpdate)).mapTo[SparqlUpdateResponse]
// Verify that the resource was deleted correctly.
sparqlQuery = org.knora.webapi.messages.twirl.queries.sparql.v2.txt
.checkResourceDeletion(
triplestore = settings.triplestoreType,
resourceIri = deleteResourceV2.resourceIri
)
.toString()
sparqlSelectResponse <- (storeManager ? SparqlSelectRequest(sparqlQuery)).mapTo[SparqlSelectResult]
rows = sparqlSelectResponse.results.bindings
_ = if (rows.isEmpty || !stringFormatter.optionStringToBoolean(
rows.head.rowMap.get("isDeleted"),
throw InconsistentRepositoryDataException(
s"Invalid boolean for isDeleted: ${rows.head.rowMap.get("isDeleted")}"))) {
throw UpdateNotPerformedException(
s"Resource <${deleteResourceV2.resourceIri}> was not marked as deleted. Please report this as a possible bug.")
}
} yield SuccessResponseV2("Resource marked as deleted")
}
if (deleteResourceV2.erase) {
throw AssertionException(s"Request message has erase == true")
}
for {
// Do the remaining pre-update checks and the update while holding an update lock on the resource.
taskResult <- IriLocker.runWithIriLock(
deleteResourceV2.apiRequestID,
deleteResourceV2.resourceIri,
() => makeTaskFuture
)
} yield taskResult
}
/**
* Erases a resource from the triplestore.
*
* @param eraseResourceV2 the request message.
*/
private def eraseResourceV2(eraseResourceV2: DeleteOrEraseResourceRequestV2): Future[SuccessResponseV2] = {
def makeTaskFuture: Future[SuccessResponseV2] = {
for {
// Get the metadata of the resource to be updated.
resourcesSeq: ReadResourcesSequenceV2 <- getResourcePreviewV2(
resourceIris = Seq(eraseResourceV2.resourceIri),
targetSchema = ApiV2Complex,
featureFactoryConfig = eraseResourceV2.featureFactoryConfig,
requestingUser = eraseResourceV2.requestingUser
)
resource: ReadResourceV2 = resourcesSeq.toResource(eraseResourceV2.resourceIri)
// Ensure that the requesting user is a system admin, or an admin of this project.
_ = if (!(eraseResourceV2.requestingUser.permissions.isProjectAdmin(resource.projectADM.id) ||
eraseResourceV2.requestingUser.permissions.isSystemAdmin)) {
throw ForbiddenException(s"Only a system admin or project admin can erase a resource")
}
internalResourceClassIri = eraseResourceV2.resourceClassIri.toOntologySchema(InternalSchema)
// Make sure that the resource's class is what the client thinks it is.
_ = if (resource.resourceClassIri != internalResourceClassIri) {
throw BadRequestException(
s"Resource <${resource.resourceIri}> is not a member of class <${eraseResourceV2.resourceClassIri}>")
}
// Make sure that the resource hasn't been updated since the client got its last modification date.
_ = if (resource.lastModificationDate != eraseResourceV2.maybeLastModificationDate) {
throw EditConflictException(s"Resource <${resource.resourceIri}> has been modified since you last read it")
}
// Check that the resource is not referred to by any other resources. We ignore rdf:subject (so we
// can erase the resource's own links) and rdf:object (in case there is a deleted link value that
// refers to it). Any such deleted link values will be erased along with the resource. If there
// is a non-deleted link to the resource, the direct link (rather than the link value) will case
// isEntityUsed() to throw an exception.
resourceSmartIri = eraseResourceV2.resourceIri.toSmartIri
_ <- isEntityUsed(
entityIri = resourceSmartIri,
errorFun = throw BadRequestException(
s"Resource ${eraseResourceV2.resourceIri} cannot be erased, because it is referred to by another resource"),
ignoreRdfSubjectAndObject = true
)
// Get the IRI of the named graph from which the resource will be erased.
dataNamedGraph: IRI = stringFormatter.projectDataNamedGraphV2(resource.projectADM)
// Generate SPARQL for erasing the resource.
sparqlUpdate = org.knora.webapi.messages.twirl.queries.sparql.v2.txt
.eraseResource(
triplestore = settings.triplestoreType,
dataNamedGraph = dataNamedGraph,
resourceIri = eraseResourceV2.resourceIri
)
.toString()
// _ = println(sparqlUpdate)
// Do the update.
_ <- (storeManager ? SparqlUpdateRequest(sparqlUpdate)).mapTo[SparqlUpdateResponse]
// Verify that the resource was erased correctly.
resourceStillExists: Boolean <- stringFormatter.checkIriExists(resourceSmartIri.toString, storeManager)
_ = if (resourceStillExists) {
throw UpdateNotPerformedException(
s"Resource <${eraseResourceV2.resourceIri}> was not erased. Please report this as a possible bug.")
}
} yield SuccessResponseV2("Resource erased")
}
if (!eraseResourceV2.erase) {
throw AssertionException(s"Request message has erase == false")
}
for {
// Do the pre-update checks and the update while holding an update lock on the resource.
taskResult <- IriLocker.runWithIriLock(
eraseResourceV2.apiRequestID,
eraseResourceV2.resourceIri,
() => makeTaskFuture
)
} yield taskResult
}
/**
* Generates a [[SparqlTemplateResourceToCreate]] describing SPARQL for creating a resource and its values.
* This method does pre-update checks that have to be done for each new resource individually, even when
* multiple resources are being created in a single request.
*
* @param internalCreateResource the resource to be created.
* @param linkTargetClasses a map of resources that are link targets to the IRIs of those resources' classes.
* @param entityInfo an [[EntityInfoGetResponseV2]] containing definitions of the class of the resource to
* be created, as well as the classes that all the link targets
* belong to.
* @param clientResourceIDs a map of IRIs of resources to be created to client IDs for the same resources, if any.
* @param defaultResourcePermissions the default permissions to be given to the resource, if it does not have custom permissions.
* @param defaultPropertyPermissions the default permissions to be given to the resource's values, if they do not
* have custom permissions. This is a map of property IRIs to permission strings.
* @param creationDate the versionDate to be attached to the resource and its values.
* @param featureFactoryConfig the feature factory configuration.
* @param requestingUser the user making the request.
* @return a [[ResourceReadyToCreate]].
*/
private def generateResourceReadyToCreate(resourceIri: IRI,
internalCreateResource: CreateResourceV2,
linkTargetClasses: Map[IRI, SmartIri],
entityInfo: EntityInfoGetResponseV2,
clientResourceIDs: Map[IRI, String],
defaultResourcePermissions: String,
defaultPropertyPermissions: Map[SmartIri, String],
creationDate: Instant,
featureFactoryConfig: FeatureFactoryConfig,
requestingUser: UserADM): Future[ResourceReadyToCreate] = {
val resourceIDForErrorMsg: String =
clientResourceIDs.get(resourceIri).map(resourceID => s"In resource '$resourceID': ").getOrElse("")
for {
// Check that the resource class has a suitable cardinality for each submitted value.
resourceClassInfo <- Future(entityInfo.classInfoMap(internalCreateResource.resourceClassIri))
knoraPropertyCardinalities: Map[SmartIri, Cardinality.KnoraCardinalityInfo] = resourceClassInfo.allCardinalities
.filterKeys(resourceClassInfo.knoraResourceProperties)
_ = internalCreateResource.values.foreach {
case (propertyIri: SmartIri, valuesForProperty: Seq[CreateValueInNewResourceV2]) =>
val internalPropertyIri = propertyIri.toOntologySchema(InternalSchema)
val cardinalityInfo = knoraPropertyCardinalities.getOrElse(
internalPropertyIri,
throw OntologyConstraintException(
s"${resourceIDForErrorMsg}Resource class <${internalCreateResource.resourceClassIri.toOntologySchema(
ApiV2Complex)}> has no cardinality for property <$propertyIri>")
)
if ((cardinalityInfo.cardinality == Cardinality.MayHaveOne || cardinalityInfo.cardinality == Cardinality.MustHaveOne) && valuesForProperty.size > 1) {
throw OntologyConstraintException(
s"${resourceIDForErrorMsg}Resource class <${internalCreateResource.resourceClassIri.toOntologySchema(
ApiV2Complex)}> does not allow more than one value for property <$propertyIri>")
}
}
// Check that no required values are missing.
requiredProps: Set[SmartIri] = knoraPropertyCardinalities.filter {
case (_, cardinalityInfo) =>
cardinalityInfo.cardinality == Cardinality.MustHaveOne || cardinalityInfo.cardinality == Cardinality.MustHaveSome
}.keySet -- resourceClassInfo.linkProperties
internalPropertyIris: Set[SmartIri] = internalCreateResource.values.keySet
_ = if (!requiredProps.subsetOf(internalPropertyIris)) {
val missingProps =
(requiredProps -- internalPropertyIris).map(iri => s"<${iri.toOntologySchema(ApiV2Complex)}>").mkString(", ")
throw OntologyConstraintException(
s"${resourceIDForErrorMsg}Values were not submitted for the following property or properties, which are required by resource class <${internalCreateResource.resourceClassIri
.toOntologySchema(ApiV2Complex)}>: $missingProps")
}
// Check that each submitted value is consistent with the knora-base:objectClassConstraint of the property that is supposed to
// point to it.
_ = checkObjectClassConstraints(
values = internalCreateResource.values,
linkTargetClasses = linkTargetClasses,
entityInfo = entityInfo,
clientResourceIDs = clientResourceIDs,
resourceIDForErrorMsg = resourceIDForErrorMsg
)
// Check that the submitted values do not contain duplicates.
_ = checkForDuplicateValues(
values = internalCreateResource.values,
clientResourceIDs = clientResourceIDs,
resourceIDForErrorMsg = resourceIDForErrorMsg
)
// Validate and reformat any custom permissions in the request, and set all permissions to defaults if custom
// permissions are not provided.
resourcePermissions: String <- internalCreateResource.permissions match {
case Some(permissionStr) =>
for {
validatedCustomPermissions: String <- PermissionUtilADM.validatePermissions(
permissionLiteral = permissionStr,
featureFactoryConfig = featureFactoryConfig,
responderManager = responderManager
)
// Is the requesting user a system admin, or an admin of this project?
_ = if (!(requestingUser.permissions.isProjectAdmin(internalCreateResource.projectADM.id) || requestingUser.permissions.isSystemAdmin)) {
// No. Make sure they don't give themselves higher permissions than they would get from the default permissions.
val permissionComparisonResult: PermissionComparisonResult = PermissionUtilADM.comparePermissionsADM(
entityCreator = requestingUser.id,
entityProject = internalCreateResource.projectADM.id,
permissionLiteralA = validatedCustomPermissions,
permissionLiteralB = defaultResourcePermissions,
requestingUser = requestingUser
)
if (permissionComparisonResult == AGreaterThanB) {
throw ForbiddenException(
s"${resourceIDForErrorMsg}The specified permissions would give the resource's creator a higher permission on the resource than the default permissions")
}
}
} yield validatedCustomPermissions
case None => FastFuture.successful(defaultResourcePermissions)
}
valuesWithValidatedPermissions: Map[SmartIri, Seq[GenerateSparqlForValueInNewResourceV2]] <- validateAndFormatValuePermissions(
project = internalCreateResource.projectADM,
values = internalCreateResource.values,
defaultPropertyPermissions = defaultPropertyPermissions,
resourceIDForErrorMsg = resourceIDForErrorMsg,
featureFactoryConfig = featureFactoryConfig,
requestingUser = requestingUser
)
// Ask the values responder for SPARQL for generating the values.
sparqlForValuesResponse: GenerateSparqlToCreateMultipleValuesResponseV2 <- (responderManager ?
GenerateSparqlToCreateMultipleValuesRequestV2(
resourceIri = resourceIri,
values = valuesWithValidatedPermissions,
creationDate = creationDate,
requestingUser = requestingUser
)).mapTo[GenerateSparqlToCreateMultipleValuesResponseV2]
} yield
ResourceReadyToCreate(
sparqlTemplateResourceToCreate = SparqlTemplateResourceToCreate(
resourceIri = resourceIri,
permissions = resourcePermissions,
sparqlForValues = sparqlForValuesResponse.insertSparql,
resourceClassIri = internalCreateResource.resourceClassIri.toString,
resourceLabel = internalCreateResource.label,
resourceCreationDate = creationDate
),
values = sparqlForValuesResponse.unverifiedValues,
hasStandoffLink = sparqlForValuesResponse.hasStandoffLink
)
}
/**
* Given a sequence of resources to be created, gets the class IRIs of all the resources that are the targets of
* link values in the new resources, whether these already exist in the triplestore or are among the resources
* to be created.
*
* @param internalCreateResources the resources to be created.
* @param featureFactoryConfig the feature factory configuration.
* @param requestingUser the user making the request.
* @return a map of resource IRIs to class IRIs.
*/
private def getLinkTargetClasses(resourceIri: IRI,
internalCreateResources: Seq[CreateResourceV2],
featureFactoryConfig: FeatureFactoryConfig,
requestingUser: UserADM): Future[Map[IRI, SmartIri]] = {
// Get the IRIs of the new and existing resources that are targets of links.
val (existingTargetIris: Set[IRI], newTargets: Set[IRI]) =
internalCreateResources.flatMap(_.flatValues).foldLeft((Set.empty[IRI], Set.empty[IRI])) {
case ((accExisting: Set[IRI], accNew: Set[IRI]), valueToCreate: CreateValueInNewResourceV2) =>
valueToCreate.valueContent match {
case linkValueContentV2: LinkValueContentV2 =>
if (linkValueContentV2.referredResourceExists) {
(accExisting + linkValueContentV2.referredResourceIri, accNew)
} else {
(accExisting, accNew + linkValueContentV2.referredResourceIri)
}
case _ => (accExisting, accNew)
}
}
// Make a map of the IRIs of new target resources to their class IRIs.
val classesOfNewTargets: Map[IRI, SmartIri] = internalCreateResources
.map { resourceToCreate =>
resourceIri -> resourceToCreate.resourceClassIri
}
.toMap
.filterKeys(newTargets)
for {
// Get information about the existing resources that are targets of links.
existingTargets: ReadResourcesSequenceV2 <- getResourcePreviewV2(
resourceIris = existingTargetIris.toSeq,
targetSchema = ApiV2Complex,
featureFactoryConfig = featureFactoryConfig,
requestingUser = requestingUser
)
// Make a map of the IRIs of existing target resources to their class IRIs.
classesOfExistingTargets: Map[IRI, SmartIri] = existingTargets.resources
.map(resource => resource.resourceIri -> resource.resourceClassIri)
.toMap
} yield classesOfNewTargets ++ classesOfExistingTargets
}
/**
* Checks that values to be created in a new resource do not contain duplicates.
*
* @param values a map of property IRIs to values to be created (in the internal schema).
* @param clientResourceIDs a map of IRIs of resources to be created to client IDs for the same resources, if any.
* @param resourceIDForErrorMsg something that can be prepended to an error message to specify the client's ID for the
* resource to be created, if any.
*/
private def checkForDuplicateValues(values: Map[SmartIri, Seq[CreateValueInNewResourceV2]],
clientResourceIDs: Map[IRI, String] = Map.empty[IRI, String],
resourceIDForErrorMsg: IRI): Unit = {
values.foreach {
case (propertyIri: SmartIri, valuesToCreate: Seq[CreateValueInNewResourceV2]) =>
// Given the values for a property, compute all possible combinations of two of those values.
val valueCombinations: Iterator[Seq[CreateValueInNewResourceV2]] = valuesToCreate.combinations(2)
for (valueCombination: Seq[CreateValueInNewResourceV2] <- valueCombinations) {
// valueCombination must have two elements.
val firstValue: ValueContentV2 = valueCombination.head.valueContent
val secondValue: ValueContentV2 = valueCombination(1).valueContent
if (firstValue.wouldDuplicateOtherValue(secondValue)) {
throw DuplicateValueException(
s"${resourceIDForErrorMsg}Duplicate values for property <${propertyIri.toOntologySchema(ApiV2Complex)}>")
}
}
}
}
/**
* Checks that values to be created in a new resource are compatible with the object class constraints
* of the resource's properties.
*
* @param values a map of property IRIs to values to be created (in the internal schema).
* @param linkTargetClasses a map of resources that are link targets to the IRIs of those resource's classes.
* @param entityInfo an [[EntityInfoGetResponseV2]] containing definitions of the classes that all the link targets
* belong to.
* @param clientResourceIDs a map of IRIs of resources to be created to client IDs for the same resources, if any.
* @param resourceIDForErrorMsg something that can be prepended to an error message to specify the client's ID for the
* resource to be created, if any.
*/
private def checkObjectClassConstraints(values: Map[SmartIri, Seq[CreateValueInNewResourceV2]],
linkTargetClasses: Map[IRI, SmartIri],
entityInfo: EntityInfoGetResponseV2,
clientResourceIDs: Map[IRI, String] = Map.empty[IRI, String],
resourceIDForErrorMsg: IRI): Unit = {
values.foreach {
case (propertyIri: SmartIri, valuesToCreate: Seq[CreateValueInNewResourceV2]) =>
val propertyInfo: ReadPropertyInfoV2 = entityInfo.propertyInfoMap(propertyIri)
// Don't accept link properties.
if (propertyInfo.isLinkProp) {
throw BadRequestException(
s"${resourceIDForErrorMsg}Invalid property <${propertyIri.toOntologySchema(ApiV2Complex)}>. Use a link value property to submit a link.")
}
// Get the property's object class constraint. If this is a link value property, we want the object
// class constraint of the corresponding link property instead.
val propertyInfoForObjectClassConstraint = if (propertyInfo.isLinkValueProp) {
entityInfo.propertyInfoMap(propertyIri.fromLinkValuePropToLinkProp)
} else {
propertyInfo
}
val propertyIriForObjectClassConstraint = propertyInfoForObjectClassConstraint.entityInfoContent.propertyIri
val objectClassConstraint: SmartIri = propertyInfoForObjectClassConstraint.entityInfoContent.requireIriObject(
OntologyConstants.KnoraBase.ObjectClassConstraint.toSmartIri,
throw InconsistentRepositoryDataException(
s"Property <$propertyIriForObjectClassConstraint> has no knora-api:objectType")
)
// Check each value.
for (valueToCreate: CreateValueInNewResourceV2 <- valuesToCreate) {
valueToCreate.valueContent match {
case linkValueContentV2: LinkValueContentV2 =>
// It's a link value.
if (!propertyInfo.isLinkValueProp) {
throw OntologyConstraintException(s"${resourceIDForErrorMsg}Property <${propertyIri.toOntologySchema(
ApiV2Complex)}> requires a value of type <${objectClassConstraint.toOntologySchema(ApiV2Complex)}>")
}
// Does the resource that's the target of the link belongs to a subclass of the
// link property's object class constraint?
val linkTargetClass = linkTargetClasses(linkValueContentV2.referredResourceIri)
val linkTargetClassInfo = entityInfo.classInfoMap(linkTargetClass)
if (!linkTargetClassInfo.allBaseClasses.contains(objectClassConstraint)) {
// No. If the target resource already exists, use its IRI in the error message.
// Otherwise, use the client's ID for the resource.
val resourceID = if (linkValueContentV2.referredResourceExists) {
s"<${linkValueContentV2.referredResourceIri}>"
} else {
s"'${clientResourceIDs(linkValueContentV2.referredResourceIri)}'"
}
throw OntologyConstraintException(
s"${resourceIDForErrorMsg}Resource $resourceID cannot be the object of property <${propertyIriForObjectClassConstraint
.toOntologySchema(ApiV2Complex)}>, because it does not belong to class <${objectClassConstraint
.toOntologySchema(ApiV2Complex)}>")
}
case otherValueContentV2: ValueContentV2 =>
// It's not a link value. Check that its type is equal to the property's object
// class constraint.
if (otherValueContentV2.valueType != objectClassConstraint) {
throw OntologyConstraintException(s"${resourceIDForErrorMsg}Property <${propertyIri.toOntologySchema(
ApiV2Complex)}> requires a value of type <${objectClassConstraint.toOntologySchema(ApiV2Complex)}>")
}
}
}
}