/
ConstructResponseUtilV2.scala
1658 lines (1504 loc) · 77.2 KB
/
ConstructResponseUtilV2.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.messages.util
import java.time.Instant
import java.util.UUID
import akka.actor.ActorRef
import akka.http.scaladsl.util.FastFuture
import akka.pattern.ask
import akka.util.Timeout
import org.knora.webapi._
import org.knora.webapi.exceptions.{AssertionException, InconsistentRepositoryDataException, NotImplementedException}
import org.knora.webapi.feature.FeatureFactoryConfig
import org.knora.webapi.messages.IriConversions._
import org.knora.webapi.messages.admin.responder.projectsmessages.{
ProjectGetRequestADM,
ProjectGetResponseADM,
ProjectIdentifierADM
}
import org.knora.webapi.messages.admin.responder.usersmessages.UserADM
import org.knora.webapi.messages.store.triplestoremessages.SparqlExtendedConstructResponse.ConstructPredicateObjects
import org.knora.webapi.messages.store.triplestoremessages._
import org.knora.webapi.messages.util.PermissionUtilADM.EntityPermission
import org.knora.webapi.messages.util.standoff.StandoffTagUtilV2
import org.knora.webapi.messages.v2.responder.listsmessages.{NodeGetRequestV2, NodeGetResponseV2}
import org.knora.webapi.messages.v2.responder.ontologymessages.StandoffEntityInfoGetResponseV2
import org.knora.webapi.messages.v2.responder.resourcemessages.{ReadResourceV2, ReadResourcesSequenceV2}
import org.knora.webapi.messages.v2.responder.standoffmessages.{
GetRemainingStandoffFromTextValueRequestV2,
GetStandoffResponseV2,
MappingXMLtoStandoff,
StandoffTagV2
}
import org.knora.webapi.messages.v2.responder.valuemessages._
import org.knora.webapi.messages.{OntologyConstants, SmartIri, StringFormatter}
import org.knora.webapi.settings.KnoraSettingsImpl
import org.knora.webapi.util.ActorUtil
import scala.concurrent.{ExecutionContext, Future}
object ConstructResponseUtilV2 {
private val InferredPredicates = Set(
OntologyConstants.KnoraBase.HasValue,
OntologyConstants.KnoraBase.IsMainResource
)
/**
* A map of resource IRIs to resource RDF data.
*/
type RdfResources = Map[IRI, ResourceWithValueRdfData]
/**
* Makes an empty instance of [[RdfResources]].
*/
def emptyRdfResources: RdfResources = Map.empty
/**
* A map of property IRIs to value RDF data.
*/
type RdfPropertyValues = Map[SmartIri, Seq[ValueRdfData]]
/**
* Makes an empty instance of [[RdfPropertyValues]].
*/
def emptyRdfPropertyValues: RdfPropertyValues = Map.empty
/**
* A map of subject IRIs to [[ConstructPredicateObjects]] instances.
*/
type Statements = Map[IRI, ConstructPredicateObjects]
/**
* A flattened map of predicates to objects. This assumes that each predicate has
* * only one object.
*/
type FlatPredicateObjects = Map[SmartIri, LiteralV2]
/**
* A map of subject IRIs to flattened maps of predicates to objects.
*/
type FlatStatements = Map[IRI, Map[SmartIri, LiteralV2]]
/**
* Makes an empty instance of [[FlatStatements]].
*/
def emptyFlatStatements: FlatStatements = Map.empty
/**
* Represents assertions about an RDF subject.
*/
sealed trait RdfData {
/**
* The IRI of the subject.
*/
val subjectIri: IRI
/**
* Assertions about the subject.
*/
val assertions: FlatPredicateObjects
/**
* Returns the optional string object of the specified predicate. Throws an exception if the object is not a string.
*
* @param predicateIri the predicate.
* @return the string object of the predicate.
*/
def maybeStringObject(predicateIri: SmartIri): Option[String] = {
assertions.get(predicateIri).map { literal =>
literal
.asStringLiteral(
throw InconsistentRepositoryDataException(s"Unexpected object of $subjectIri $predicateIri: $literal"))
.value
}
}
/**
* Returns the required string object of the specified predicate. Throws an exception if the object is not found or
* is not a string.
*
* @param predicateIri the predicate.
* @return the string object of the predicate.
*/
def requireStringObject(predicateIri: SmartIri): String = {
maybeStringObject(predicateIri).getOrElse(
throw InconsistentRepositoryDataException(s"Subject $subjectIri does not have predicate $predicateIri"))
}
/**
* Returns the optional IRI object of the specified predicate. Throws an exception if the object is not an IRI.
*
* @param predicateIri the predicate.
* @return the IRI object of the predicate.
*/
def maybeIriObject(predicateIri: SmartIri): Option[IRI] = {
assertions.get(predicateIri).map { literal =>
literal
.asIriLiteral(
throw InconsistentRepositoryDataException(s"Unexpected object of $subjectIri $predicateIri: $literal"))
.value
}
}
/**
* Returns the required IRI object of the specified predicate. Throws an exception if the object is not found or
* is not an IRI.
*
* @param predicateIri the predicate.
* @return the IRI object of the predicate.
*/
def requireIriObject(predicateIri: SmartIri): IRI = {
maybeIriObject(predicateIri).getOrElse(
throw InconsistentRepositoryDataException(s"Subject $subjectIri does not have predicate $predicateIri"))
}
/**
* Returns the optional integer object of the specified predicate. Throws an exception if the object is not an integer.
*
* @param predicateIri the predicate.
* @return the integer object of the predicate.
*/
def maybeIntObject(predicateIri: SmartIri): Option[Int] = {
assertions.get(predicateIri).map { literal =>
literal
.asIntLiteral(
throw InconsistentRepositoryDataException(s"Unexpected object of $subjectIri $predicateIri: $literal"))
.value
}
}
/**
* Returns the required integer object of the specified predicate. Throws an exception if the object is not found or
* is not an integer.
*
* @param predicateIri the predicate.
* @return the integer object of the predicate.
*/
def requireIntObject(predicateIri: SmartIri): Int = {
maybeIntObject(predicateIri).getOrElse(
throw InconsistentRepositoryDataException(s"Subject $subjectIri does not have predicate $predicateIri"))
}
/**
* Returns the optional boolean object of the specified predicate. Throws an exception if the object is not a boolean.
*
* @param predicateIri the predicate.
* @return the boolean object of the predicate.
*/
def maybeBooleanObject(predicateIri: SmartIri): Option[Boolean] = {
assertions.get(predicateIri).map { literal =>
literal
.asBooleanLiteral(
throw InconsistentRepositoryDataException(s"Unexpected object of $subjectIri $predicateIri: $literal"))
.value
}
}
/**
* Returns the required boolean object of the specified predicate. Throws an exception if the object is not found or
* is not an boolean value.
*
* @param predicateIri the predicate.
* @return the boolean object of the predicate.
*/
def requireBooleanObject(predicateIri: SmartIri): Boolean = {
maybeBooleanObject(predicateIri).getOrElse(
throw InconsistentRepositoryDataException(s"Subject $subjectIri does not have predicate $predicateIri"))
}
/**
* Returns the optional decimal object of the specified predicate. Throws an exception if the object is not a decimal.
*
* @param predicateIri the predicate.
* @return the decimal object of the predicate.
*/
def maybeDecimalObject(predicateIri: SmartIri): Option[BigDecimal] = {
assertions.get(predicateIri).map { literal =>
literal
.asDecimalLiteral(
throw InconsistentRepositoryDataException(s"Unexpected object of $subjectIri $predicateIri: $literal"))
.value
}
}
/**
* Returns the required decimal object of the specified predicate. Throws an exception if the object is not found or
* is not an decimal value.
*
* @param predicateIri the predicate.
* @return the decimal object of the predicate.
*/
def requireDecimalObject(predicateIri: SmartIri): BigDecimal = {
maybeDecimalObject(predicateIri).getOrElse(
throw InconsistentRepositoryDataException(s"Subject $subjectIri does not have predicate $predicateIri"))
}
/**
* Returns the optional timestamp object of the specified predicate. Throws an exception if the object is not a timestamp.
*
* @param predicateIri the predicate.
* @return the timestamp object of the predicate.
*/
def maybeDateTimeObject(predicateIri: SmartIri): Option[Instant] = {
assertions.get(predicateIri).map { literal =>
literal
.asDateTimeLiteral(
throw InconsistentRepositoryDataException(s"Unexpected object of $subjectIri $predicateIri: $literal"))
.value
}
}
/**
* Returns the required timestamp object of the specified predicate. Throws an exception if the object is not found or
* is not an timestamp value.
*
* @param predicateIri the predicate.
* @return the timestamp object of the predicate.
*/
def requireDateTimeObject(predicateIri: SmartIri): Instant = {
maybeDateTimeObject(predicateIri).getOrElse(
throw InconsistentRepositoryDataException(s"Subject $subjectIri does not have predicate $predicateIri"))
}
}
/**
* Represents the RDF data about a value, possibly including standoff.
*
* @param subjectIri the value object's IRI.
* @param valueObjectClass the type (class) of the value object.
* @param nestedResource the nested resource in case of a link value (either the source or the target of a link value, depending on [[isIncomingLink]]).
* @param isIncomingLink indicates if it is an incoming or outgoing link in case of a link value.
* @param userPermission the permission that the requesting user has on the value.
* @param assertions the value objects assertions.
* @param standoff standoff assertions, if any.
*/
case class ValueRdfData(subjectIri: IRI,
valueObjectClass: SmartIri,
nestedResource: Option[ResourceWithValueRdfData] = None,
isIncomingLink: Boolean = false,
userPermission: EntityPermission,
assertions: FlatPredicateObjects,
standoff: FlatStatements)
extends RdfData
/**
* Represents a resource and its values.
*
* @param subjectIri the resource IRI.
* @param assertions assertions about the resource (direct statements).
* @param isMainResource indicates if this represents a top level resource or a referred resource (depending on the query).
* @param userPermission the permission that the requesting user has on the resource.
* @param valuePropertyAssertions assertions about value properties.
*/
case class ResourceWithValueRdfData(subjectIri: IRI,
assertions: FlatPredicateObjects,
isMainResource: Boolean,
userPermission: Option[EntityPermission],
valuePropertyAssertions: RdfPropertyValues)
extends RdfData
/**
* Represents a mapping including information about the standoff entities.
* May include a default XSL transformation.
*
* @param mapping the mapping from XML to standoff and vice versa.
* @param standoffEntities information about the standoff entities referred to in the mapping.
* @param XSLTransformation the default XSL transformation to convert the resulting XML (e.g., to HTML), if any.
*/
case class MappingAndXSLTransformation(mapping: MappingXMLtoStandoff,
standoffEntities: StandoffEntityInfoGetResponseV2,
XSLTransformation: Option[String])
/**
* Represents a tree structure of resources, values and dependent resources returned by a SPARQL CONSTRUCT query.
*
* @param resources a map of resource Iris to [[ResourceWithValueRdfData]]. The resource Iris represent main resources, dependent
* resources are contained in the link values as nested structures.
* @param hiddenResourceIris the IRIs of resources that were hidden because the user does not have permission
* to see them.
*/
case class MainResourcesAndValueRdfData(resources: RdfResources, hiddenResourceIris: Set[IRI] = Set.empty)
/**
* An intermediate data structure containing RDF assertions about an entity and the user's permission on the entity.
*
* @param assertions RDF assertions about the entity.
* @param maybeUserPermission the user's permission on the entity, if any.
*/
case class RdfWithUserPermission(assertions: ConstructPredicateObjects, maybeUserPermission: Option[EntityPermission])
/**
* A [[SparqlConstructResponse]] may contain both resources and value RDF data objects as well as standoff.
* This method turns a graph (i.e. triples) into a structure organized by the principle of resources and their values,
* i.e. a map of resource Iris to [[ResourceWithValueRdfData]].
* The resource Iris represent main resources, dependent resources are contained in the link values as nested structures.
*
* @param constructQueryResults the results of a SPARQL construct query representing resources and their values.
* @return an instance of [[MainResourcesAndValueRdfData]].
*/
def splitMainResourcesAndValueRdfData(
constructQueryResults: SparqlExtendedConstructResponse,
requestingUser: UserADM)(implicit stringFormatter: StringFormatter): MainResourcesAndValueRdfData = {
// Make sure all the subjects are IRIs, because blank nodes are not used in resources.
val resultsWithIriSubjects: Statements = constructQueryResults.statements.map {
case (iriSubject: IriSubjectV2, statements: ConstructPredicateObjects) => iriSubject.value -> statements
case (otherSubject: SubjectV2, _: ConstructPredicateObjects) =>
throw InconsistentRepositoryDataException(s"Unexpected subject: $otherSubject")
}
// split statements about resources and other statements (value objects and standoff)
// resources are identified by the triple "resourceIri a knora-base:Resource" which is an inferred information returned by the SPARQL Construct query.
val (resourceStatements: Statements, nonResourceStatements: Statements) = resultsWithIriSubjects.partition {
case (_: IRI, assertions: ConstructPredicateObjects) =>
// check if the subject is a Knora resource
assertions
.getOrElse(OntologyConstants.Rdf.Type.toSmartIri, Seq.empty)
.contains(IriLiteralV2(OntologyConstants.KnoraBase.Resource))
}
// create a single map of all resources with their representing values (rdf data)
val flatResourcesWithValues: RdfResources = resourceStatements.map {
case (resourceIri: IRI, assertions: ConstructPredicateObjects) =>
// remove inferred statements (non explicit) returned in the query result
// the query returns the following inferred information:
// - every resource is a knora-base:Resource
// - every value property is a subproperty of knora-base:hasValue
// - every resource that's a main resource (not a dependent resource) in the query result has knora-base:isMainResource true
val assertionsExplicit: ConstructPredicateObjects = assertions
.filterNot {
case (pred: SmartIri, _) => InferredPredicates(pred.toString)
}
.map {
case (pred: SmartIri, objs: Seq[LiteralV2]) =>
if (pred.toString == OntologyConstants.Rdf.Type) {
pred -> objs.filterNot {
case IriLiteralV2(OntologyConstants.KnoraBase.Resource) => true
case _ => false
}
} else {
pred -> objs
}
}
// check for the knora-base:isMainResource flag created by the SPARQL CONSTRUCT query
val isMainResource: Boolean = assertions.get(OntologyConstants.KnoraBase.IsMainResource.toSmartIri) match {
case Some(Seq(BooleanLiteralV2(value))) => value
case _ => false
}
// Make a set of all the value object IRIs, because we're going to associate them with their properties.
val valueObjectIris: Set[IRI] = assertions
.collect {
case (pred: SmartIri, objs: Seq[LiteralV2]) if pred.toString == OntologyConstants.KnoraBase.HasValue =>
objs.map {
case IriLiteralV2(iri) => iri
case other =>
throw InconsistentRepositoryDataException(
s"Unexpected object for $resourceIri knora-base:hasValue: $other")
}
}
.flatten
.toSet
// Make a map of property IRIs to sequences of value IRIs.
val valuePropertyToObjectIris: Map[SmartIri, Seq[IRI]] =
mapPropertyIrisToValueIris(assertionsExplicit, valueObjectIris)
// Make an RdfPropertyValues representing the values of the resource.
val valuePropertyToValueObject: RdfPropertyValues = makeRdfPropertyValuesForResource(
valuePropertyToObjectIris = valuePropertyToObjectIris,
resourceIri = resourceIri,
requestingUser = requestingUser,
assertionsExplicit = assertionsExplicit,
nonResourceStatements = nonResourceStatements
)
// Flatten the resource assertions.
val resourceAssertions: FlatPredicateObjects = assertionsExplicit.map {
case (pred: SmartIri, objs: Seq[LiteralV2]) => pred -> objs.head
}
val userPermission: Option[EntityPermission] =
PermissionUtilADM.getUserPermissionFromConstructAssertionsADM(resourceIri, assertions, requestingUser)
// Make a ResourceWithValueRdfData for each resource IRI.
resourceIri -> ResourceWithValueRdfData(
subjectIri = resourceIri,
assertions = resourceAssertions,
isMainResource = isMainResource,
userPermission = userPermission,
valuePropertyAssertions = valuePropertyToValueObject
)
}
// Identify the resources that the user has permission to see.
val (visibleResources: RdfResources, hiddenResources: RdfResources) = flatResourcesWithValues.partition {
case (_: IRI, resource: ResourceWithValueRdfData) => resource.userPermission.nonEmpty
}
val mainResourceIrisVisible: Set[IRI] = visibleResources.collect {
case (resourceIri: IRI, resource: ResourceWithValueRdfData) if resource.isMainResource => resourceIri
}.toSet
val mainResourceIrisNotVisible: Set[IRI] = hiddenResources.collect {
case (resourceIri: IRI, resource: ResourceWithValueRdfData) if resource.isMainResource => resourceIri
}.toSet
val dependentResourceIrisVisible: Set[IRI] = visibleResources.collect {
case (resourceIri: IRI, resource: ResourceWithValueRdfData) if !resource.isMainResource => resourceIri
}.toSet
val dependentResourceIrisNotVisible: Set[IRI] = hiddenResources.collect {
case (resourceIri: IRI, resource: ResourceWithValueRdfData) if !resource.isMainResource => resourceIri
}.toSet
// get incoming links for each resource: a map of resource IRIs to resources that link to it
val incomingLinksForResource: Map[IRI, RdfResources] = getIncomingLink(visibleResources, flatResourcesWithValues)
val mainResourcesNested: Map[IRI, ResourceWithValueRdfData] = mainResourceIrisVisible.map { resourceIri =>
val transformedResource = nestResources(
resourceIri = resourceIri,
flatResourcesWithValues = flatResourcesWithValues,
visibleResources = visibleResources,
dependentResourceIrisVisible = dependentResourceIrisVisible,
dependentResourceIrisNotVisible = dependentResourceIrisNotVisible,
incomingLinksForResource = incomingLinksForResource
)
resourceIri -> transformedResource
}.toMap
MainResourcesAndValueRdfData(
resources = mainResourcesNested,
hiddenResourceIris = mainResourceIrisNotVisible ++ dependentResourceIrisNotVisible
)
}
/**
* Converts a [[ConstructPredicateObjects]] to a map of property IRIs to sequences of value IRIs.
*
* @param assertionsExplicit all non-inferred statements.
* @param valueObjectIris a set of all value object IRIs.
* @return a map of property IRIs to sequences of value IRIs.
*/
private def mapPropertyIrisToValueIris(assertionsExplicit: ConstructPredicateObjects,
valueObjectIris: Set[IRI]): Map[SmartIri, Seq[IRI]] = {
assertionsExplicit
.map {
case (pred: SmartIri, objs: Seq[LiteralV2]) =>
// Get only the assertions in which the object is a value object IRI.
val valueObjIris: Seq[IriLiteralV2] = objs.collect {
case iriObj: IriLiteralV2 if valueObjectIris(iriObj.value) => iriObj
}
// create an entry using pred as a key and valueObjIris as the value
pred -> valueObjIris
}
.filter {
case (_: SmartIri, objs: Seq[IriLiteralV2]) => objs.nonEmpty
}
.groupBy {
case (pred: SmartIri, _: Seq[IriLiteralV2]) =>
// Turn the sequence of assertions into a Map of predicate IRIs to assertions.
pred
}
.map {
case (pred: SmartIri, valueAssertions: Map[SmartIri, Seq[IriLiteralV2]]) =>
// Replace the assertions with their objects, i.e. the value object IRIs.
pred -> valueAssertions.values.flatten.map(_.value).toSeq
}
}
/**
* Given the assertions that describe a resource and its values, makes an [[RdfPropertyValues]] representing the values.
*
* @param valuePropertyToObjectIris a map of property IRIs to value IRIs.
* @param resourceIri the IRI of the resource.
* @param requestingUser the user making the request.
* @param assertionsExplicit all non-inferred statements.
* @param nonResourceStatements statements that are not about the containing resource.
* @return an [[RdfPropertyValues]] describing the values of the resource.
*/
private def makeRdfPropertyValuesForResource(
valuePropertyToObjectIris: Map[SmartIri, Seq[IRI]],
resourceIri: IRI,
requestingUser: UserADM,
assertionsExplicit: ConstructPredicateObjects,
nonResourceStatements: Statements)(implicit stringFormatter: StringFormatter): RdfPropertyValues = {
valuePropertyToObjectIris
.map {
case (property: SmartIri, valObjIris: Seq[IRI]) =>
// Make an RdfWithUserPermission for each value of the property.
val rdfWithUserPermissionsForValues: Seq[(IRI, RdfWithUserPermission)] = valObjIris.map { valObjIri: IRI =>
val valueObjAssertions: ConstructPredicateObjects = nonResourceStatements(valObjIri)
// get the resource's project
// value objects belong to the parent resource's project
val resourceProjectLiteral: LiteralV2 = assertionsExplicit
.getOrElse(
OntologyConstants.KnoraBase.AttachedToProject.toSmartIri,
throw InconsistentRepositoryDataException(s"Resource $resourceIri has no knora-base:attachedToProject")
)
.head
// add the resource's project to the value's assertions, and get the user's permission on the value
val maybeUserPermission = PermissionUtilADM.getUserPermissionFromConstructAssertionsADM(
entityIri = valObjIri,
assertions = valueObjAssertions + (OntologyConstants.KnoraBase.AttachedToProject.toSmartIri -> Seq(
resourceProjectLiteral)),
requestingUser = requestingUser
)
valObjIri -> RdfWithUserPermission(valueObjAssertions, maybeUserPermission)
}
// Filter out objects that the user doesn't have permission to see.
val visibleRdfWithUserPermissionsForValues: Seq[(IRI, RdfWithUserPermission)] =
rdfWithUserPermissionsForValues.filter {
// check if the user has sufficient permissions to see the value object
case (_: IRI, rdfWithUserPermission: RdfWithUserPermission) =>
rdfWithUserPermission.maybeUserPermission.nonEmpty
}
// Make a ValueRdfData for each value object.
val valueRdfDataForProperty: Seq[ValueRdfData] = visibleRdfWithUserPermissionsForValues.flatMap {
case (valObjIri: IRI, valueRdfWithUserPermission: RdfWithUserPermission) =>
// get all the standoff node IRIs possibly belonging to this value object
val standoffNodeIris: Set[IRI] = valueRdfWithUserPermission.assertions
.collect {
case (pred: SmartIri, objs: Seq[LiteralV2])
if pred.toString == OntologyConstants.KnoraBase.ValueHasStandoff =>
objs.map(_.toString)
}
.flatten
.toSet
// given the standoff node IRIs, get the standoff assertions
val standoffAssertions: FlatStatements = nonResourceStatements.collect {
case (subjIri: IRI, assertions: ConstructPredicateObjects) if standoffNodeIris(subjIri) =>
subjIri -> assertions.flatMap {
case (pred: SmartIri, objs: Seq[LiteralV2]) =>
objs.map { obj =>
pred -> obj
}
}
}
// Flatten the value's statements.
val valueStatements: FlatPredicateObjects = valueRdfWithUserPermission.assertions.flatMap {
case (pred: SmartIri, objs: Seq[LiteralV2]) =>
objs.map { obj =>
pred -> obj
}
}
// Get the rdf:type of the value.
val rdfTypeLiteral: LiteralV2 = valueStatements.getOrElse(
OntologyConstants.Rdf.Type.toSmartIri,
throw InconsistentRepositoryDataException(s"Value $valObjIri has no rdf:type"))
val valueObjectClass: SmartIri = rdfTypeLiteral
.asIriLiteral(
throw InconsistentRepositoryDataException(
s"Unexpected object of $valObjIri rdf:type: $rdfTypeLiteral")
)
.value
.toSmartIri
// check if it is a link value
if (valueObjectClass.toString == OntologyConstants.KnoraBase.LinkValue) {
// create a link value object
Some(
ValueRdfData(
subjectIri = valObjIri,
valueObjectClass = valueObjectClass,
userPermission = valueRdfWithUserPermission.maybeUserPermission.get,
assertions = valueStatements,
standoff = emptyFlatStatements // link value does not contain standoff
)
)
} else {
// create a non-link value object
Some(
ValueRdfData(
subjectIri = valObjIri,
valueObjectClass = valueObjectClass,
userPermission = valueRdfWithUserPermission.maybeUserPermission.get,
assertions = valueStatements,
standoff = standoffAssertions
)
)
}
}
// Associate each property IRI with its Seq[ValueRdfData].
property -> valueRdfDataForProperty
}
.filterNot {
// filter out those properties that do not have value objects (they may have been filtered out because the user does not have sufficient permissions to see them)
case (_, valObjs: Seq[ValueRdfData]) =>
valObjs.isEmpty
}
}
/**
* This method returns all the incoming link for each resource as a map of resource IRI to resources that link to it.
*
* @param visibleResources the resources that the user has permission to see
* @param flatResourcesWithValues the set of resources with their representing values, before permission filtering
* @return the incoming links as a map of resource IRIs
*/
private def getIncomingLink(visibleResources: RdfResources, flatResourcesWithValues: RdfResources)(
implicit stringFormatter: StringFormatter): Map[IRI, RdfResources] = {
visibleResources.map {
case (resourceIri: IRI, values: ResourceWithValueRdfData) =>
// get all incoming links for resourceIri
val incomingLinksForRes: RdfResources = flatResourcesWithValues.foldLeft(emptyRdfResources) {
case (acc: RdfResources, (otherResourceIri: IRI, otherResource: ResourceWithValueRdfData)) =>
// get all incoming links having assertions about value properties pointing to this resource
val incomingLinkPropertyAssertions: RdfPropertyValues =
otherResource.valuePropertyAssertions.foldLeft(emptyRdfPropertyValues) {
case (acc: RdfPropertyValues, (prop: SmartIri, otherResourceValues: Seq[ValueRdfData])) =>
// collect all link values that point to resourceIri
val incomingLinkValues: Seq[ValueRdfData] = otherResourceValues.foldLeft(Seq.empty[ValueRdfData]) {
(acc, value: ValueRdfData) =>
// check if it is a link value and points to this resource
if (value.valueObjectClass.toString == OntologyConstants.KnoraBase.LinkValue && value
.requireIriObject(OntologyConstants.Rdf.Object.toSmartIri) == resourceIri) {
acc :+ value
} else {
acc
}
}
// check if the link value already exists
if (incomingLinkValues.nonEmpty) {
// add link value to the existing values
acc + (prop -> incomingLinkValues)
} else {
// it does not already exists therefore add the new oone
acc
}
}
// check if the property assertion already exists
if (incomingLinkPropertyAssertions.nonEmpty) {
// add resource values to the existing values
acc + (otherResourceIri -> values.copy(
valuePropertyAssertions = incomingLinkPropertyAssertions
))
} else {
// it does not already exist therefore add the new one
acc
}
}
// create an entry using the resource's Iri as a key and its incoming links as the value
resourceIri -> incomingLinksForRes
}
}
/**
* Given a resource IRI, finds any link values in the resource, and recursively embeds the target resource in each link value.
*
* @param resourceIri the IRI of the resource to start with.
* @param flatResourcesWithValues the complete set of resources with their values, before permission filtering.
* @param visibleResources the resources that the user has permission to see.
* @param dependentResourceIrisVisible the IRIs of dependent resources that the user has permission to see.
* @param dependentResourceIrisNotVisible the IRIs of dependent resources that the user does not have permission to see.
* @param incomingLinksForResource a map of resource IRIs to resources that link to each resource.
* @param alreadyTraversed a set (initially empty) of the IRIs of resources that this function has already
* traversed, to prevent an infinite loop if a cycle is encountered.
* @return the same resource, with any nested resources attached to it.
*/
private def nestResources(resourceIri: IRI,
flatResourcesWithValues: RdfResources,
visibleResources: RdfResources,
dependentResourceIrisVisible: Set[IRI],
dependentResourceIrisNotVisible: Set[IRI],
incomingLinksForResource: Map[IRI, RdfResources],
alreadyTraversed: Set[IRI] = Set.empty[IRI])(
implicit stringFormatter: StringFormatter): ResourceWithValueRdfData = {
val resource = visibleResources(resourceIri)
val transformedValuePropertyAssertions: RdfPropertyValues = resource.valuePropertyAssertions
.map {
case (propIri: SmartIri, values: Seq[ValueRdfData]) =>
val transformedValues: Seq[ValueRdfData] = transformValuesByNestingResources(
resourceIri = resourceIri,
values = values,
flatResourcesWithValues = flatResourcesWithValues,
visibleResources = visibleResources,
dependentResourceIrisVisible = dependentResourceIrisVisible,
dependentResourceIrisNotVisible = dependentResourceIrisNotVisible,
incomingLinksForResource = incomingLinksForResource,
alreadyTraversed = alreadyTraversed + resourceIri
)
propIri -> transformedValues
}
.filter {
case (_: SmartIri, values: Seq[ValueRdfData]) =>
// If we filtered out all the values for the property, filter out the property, too.
values.nonEmpty
}
// incomingLinksForResource contains incoming link values for each resource
// flatResourcesWithValues contains the complete information
// filter out those resources that already have been processed
// and the main resources (they are already present on the top level of the response)
//
// the main resources point to dependent resources and would be treated as incoming links of dependent resources
// this would create circular dependencies
// resources that point to this resource
val referringResources: RdfResources = incomingLinksForResource(resourceIri).filterNot {
case (incomingResIri: IRI, _: ResourceWithValueRdfData) =>
alreadyTraversed(incomingResIri) || flatResourcesWithValues(incomingResIri).isMainResource
}
// link value assertions that point to this resource
val incomingLinkAssertions: RdfPropertyValues = referringResources.values.foldLeft(emptyRdfPropertyValues) {
case (acc: RdfPropertyValues, assertions: ResourceWithValueRdfData) =>
val values: RdfPropertyValues = assertions.valuePropertyAssertions.flatMap {
case (propIri: SmartIri, values: Seq[ValueRdfData]) =>
// check if the property Iri already exists (there could be several instances of the same property)
if (acc.contains(propIri)) {
// add values to property Iri (keeping the already existing values)
acc + (propIri -> (acc(propIri) ++ values).sortBy(_.subjectIri))
} else {
// prop Iri does not exists yet, add it
acc + (propIri -> values.sortBy(_.subjectIri))
}
}
values
}
if (incomingLinkAssertions.nonEmpty) {
// create a virtual property representing an incoming link
val incomingProps
: (SmartIri, Seq[ValueRdfData]) = OntologyConstants.KnoraBase.HasIncomingLinkValue.toSmartIri -> incomingLinkAssertions.values.toSeq.flatten
.map { linkValue: ValueRdfData =>
// get the source of the link value (it points to the resource that is currently processed)
val sourceIri: IRI = linkValue.requireIriObject(OntologyConstants.Rdf.Subject.toSmartIri)
val source = Some(
nestResources(
resourceIri = sourceIri,
flatResourcesWithValues = flatResourcesWithValues,
visibleResources = visibleResources,
dependentResourceIrisVisible = dependentResourceIrisVisible,
dependentResourceIrisNotVisible = dependentResourceIrisNotVisible,
incomingLinksForResource = incomingLinksForResource,
alreadyTraversed = alreadyTraversed + resourceIri
)
)
linkValue.copy(
nestedResource = source,
isIncomingLink = true
)
}
resource.copy(
valuePropertyAssertions = transformedValuePropertyAssertions + incomingProps
)
} else {
resource.copy(
valuePropertyAssertions = transformedValuePropertyAssertions
)
}
}
/**
* Transforms a resource's values by nesting dependent resources in link values.
*
* @param resourceIri the IRI of the resource.
* @param values the values of the resource.
* @param flatResourcesWithValues the complete set of resources with their values, before permission filtering.
* @param visibleResources the resources that the user has permission to see.
* @param dependentResourceIrisVisible the IRIs of dependent resources that the user has permission to see.
* @param dependentResourceIrisNotVisible the IRIs of dependent resources that the user does not have permission to see.
* @param incomingLinksForResource a map of resource IRIs to resources that link to each resource.
* @param alreadyTraversed a set (initially empty) of the IRIs of resources that this function has already
* traversed, to prevent an infinite loop if a cycle is encountered.
* @return the transformed values.
*/
private def transformValuesByNestingResources(
resourceIri: IRI,
values: Seq[ValueRdfData],
flatResourcesWithValues: RdfResources,
visibleResources: RdfResources,
dependentResourceIrisVisible: Set[IRI],
dependentResourceIrisNotVisible: Set[IRI],
incomingLinksForResource: Map[IRI, RdfResources],
alreadyTraversed: Set[IRI])(implicit stringFormatter: StringFormatter): Seq[ValueRdfData] = {
values.foldLeft(Vector.empty[ValueRdfData]) {
case (acc: Vector[ValueRdfData], value: ValueRdfData) =>
if (value.valueObjectClass.toString == OntologyConstants.KnoraBase.LinkValue) {
val dependentResourceIri: IRI = value.requireIriObject(OntologyConstants.Rdf.Object.toSmartIri)
if (alreadyTraversed(dependentResourceIri)) {
acc :+ value
} else {
// Do we have the dependent resource?
if (dependentResourceIrisVisible.contains(dependentResourceIri)) {
// Yes. Nest it in the link value.
val dependentResource: ResourceWithValueRdfData = nestResources(
resourceIri = dependentResourceIri,
flatResourcesWithValues = flatResourcesWithValues,
visibleResources = visibleResources,
dependentResourceIrisVisible = dependentResourceIrisVisible,
dependentResourceIrisNotVisible = dependentResourceIrisNotVisible,
incomingLinksForResource = incomingLinksForResource,
alreadyTraversed = alreadyTraversed
)
acc :+ value.copy(
nestedResource = Some(dependentResource)
)
} else if (dependentResourceIrisNotVisible.contains(dependentResourceIri)) {
// No, because the user doesn't have permission to see it. Skip the link value.
acc
} else {
// We don't have the dependent resource because it is marked as deleted. Just
// return the link value without a nested resource.
acc :+ value
}
}
} else {
acc :+ value
}
}
}
/**
* Collect all mapping Iris referred to in the given value assertions.
*
* @param valuePropertyAssertions the given assertions (property -> value object).
* @return a set of mapping Iris.
*/
def getMappingIrisFromValuePropertyAssertions(valuePropertyAssertions: RdfPropertyValues)(
implicit stringFormatter: StringFormatter): Set[IRI] = {
valuePropertyAssertions.foldLeft(Set.empty[IRI]) {
case (acc: Set[IRI], (_: SmartIri, valObjs: Seq[ValueRdfData])) =>
val mappings: Seq[String] = valObjs
.filter { valObj: ValueRdfData =>
valObj.valueObjectClass == OntologyConstants.KnoraBase.TextValue.toSmartIri && valObj.assertions.contains(
OntologyConstants.KnoraBase.ValueHasMapping.toSmartIri)
}
.map { textValObj: ValueRdfData =>
textValObj.requireIriObject(OntologyConstants.KnoraBase.ValueHasMapping.toSmartIri)
}
// get mappings from linked resources
val mappingsFromReferredResources: Set[IRI] = valObjs
.filter { valObj: ValueRdfData =>
valObj.nestedResource.nonEmpty
}
.flatMap { valObj: ValueRdfData =>
val referredRes: ResourceWithValueRdfData = valObj.nestedResource.get
// recurse on the nested resource's values
getMappingIrisFromValuePropertyAssertions(referredRes.valuePropertyAssertions)
}
.toSet
acc ++ mappings ++ mappingsFromReferredResources
}
}
/**
* Given a [[ValueRdfData]], constructs a [[TextValueContentV2]]. This method is used to process a text value
* as returned in an API response, as well as to process a page of standoff markup that is being queried
* separately from its text value.
*
* @param valueObject the given [[ValueRdfData]].
* @param valueObjectValueHasString the value's `knora-base:valueHasString`.
* @param valueCommentOption the value's comment, if any.
* @param mappings the mappings needed for standoff conversions and XSL transformations.
* @param queryStandoff if `true`, make separate queries to get the standoff for the text value.
* @param responderManager the Knora responder manager.
* @param featureFactoryConfig the feature factory configuration.
* @param requestingUser the user making the request.
* @return a [[TextValueContentV2]].
*/
private def makeTextValueContentV2(resourceIri: IRI,
valueObject: ValueRdfData,
valueObjectValueHasString: Option[String],
valueCommentOption: Option[String],
mappings: Map[IRI, MappingAndXSLTransformation],
queryStandoff: Boolean,
responderManager: ActorRef,
featureFactoryConfig: FeatureFactoryConfig,
requestingUser: UserADM)(
implicit stringFormatter: StringFormatter,
timeout: Timeout,
executionContext: ExecutionContext): Future[TextValueContentV2] = {
// Any knora-base:TextValue may have a language
val valueLanguageOption: Option[String] =
valueObject.maybeStringObject(OntologyConstants.KnoraBase.ValueHasLanguage.toSmartIri)
if (valueObject.standoff.nonEmpty) {
// The query included a page of standoff markup. This is either because we've queried the text value
// and got the first page of its standoff along with it, or because we're querying a subsequent page
// of standoff for a text value.
val mappingIri: Option[IRI] = valueObject.maybeIriObject(OntologyConstants.KnoraBase.ValueHasMapping.toSmartIri)
val mappingAndXsltTransformation: Option[MappingAndXSLTransformation] =
mappingIri.flatMap(definedMappingIri => mappings.get(definedMappingIri))
for {
standoff: Vector[StandoffTagV2] <- StandoffTagUtilV2.createStandoffTagsV2FromConstructResults(
standoffAssertions = valueObject.standoff,
responderManager = responderManager,
requestingUser = requestingUser
)
valueHasMaxStandoffStartIndex: Int = valueObject.requireIntObject(
OntologyConstants.KnoraBase.ValueHasMaxStandoffStartIndex.toSmartIri)
lastStartIndexQueried = standoff.last.startIndex
// Should we get more the rest of the standoff for the same text value?
standoffToReturn <- if (queryStandoff && lastStartIndexQueried < valueHasMaxStandoffStartIndex) {
// We're supposed to get all the standoff for the text value. Ask the standoff responder for the rest of it.
// Each page of markup will be also be processed by this method. The resulting pages will be
// concatenated together and returned in a GetStandoffResponseV2.
for {
standoffResponse <- (responderManager ? GetRemainingStandoffFromTextValueRequestV2(
resourceIri = resourceIri,
valueIri = valueObject.subjectIri,
featureFactoryConfig = featureFactoryConfig,
requestingUser = requestingUser