diff --git a/docs/src/paradox/00-release-notes/v2.x.x.md b/docs/src/paradox/00-release-notes/v2.x.x.md
index 459a125d11..9a06751a04 100644
--- a/docs/src/paradox/00-release-notes/v2.x.x.md
+++ b/docs/src/paradox/00-release-notes/v2.x.x.md
@@ -31,14 +31,16 @@ description.
- trouble with xml-checker and/or consistency-checker during bulk import (@github[#978](#978))
- ontology API error with link values (@github[#988](#988))
-## v2.0.1 (not release yet)
+## v2.0.1 (not released yet)
### Bugfixes:
- sipi container config / sipi not able to talk to knora (@github[#988](#994))
-## v2.1.0 (not release yet)
+## v2.1.0 (not released yet)
### New features:
-### Bugfixes:
\ No newline at end of file
+- Implement graph query in API v2 (@github[#1009](#1009))
+
+### Bugfixes:
diff --git a/docs/src/paradox/03-apis/api-v2/reading-and-searching-resources.md b/docs/src/paradox/03-apis/api-v2/reading-and-searching-resources.md
index 337d04395f..6b2b579484 100644
--- a/docs/src/paradox/03-apis/api-v2/reading-and-searching-resources.md
+++ b/docs/src/paradox/03-apis/api-v2/reading-and-searching-resources.md
@@ -100,6 +100,80 @@ the path segment `resourcespreview`:
HTTP GET to http://host/v2/resourcespreview/resourceIRI(/anotherResourceIri)*
```
+## Get a Graph of Resources
+
+Knora can return a graph of connections between resources, e.g. for generating
+a network diagram.
+
+```
+HTTP GET to http://host/v2/graph/resourceIRI[depth=Integer]
+[direction=outbound|inbound|both][excludeProperty=propertyIri]
+```
+
+The first parameter must be preceded by a question mark `?`, any
+following parameter by an ampersand `&`.
+
+- `depth` must be at least 1. The maximum depth is an Knora configuration setting.
+ The default is 4.
+- `direction` specifies the direction of the links to be queried, i.e. links to
+ and/or from the given resource. The default is `outbound`.
+- `excludeProperty` is an optional link property to be excluded from the
+ results.
+
+To accommodate large graphs, the graph response format is very concise, and is therefore
+simpler than the usual resources response format. Each resource represented only by its IRI,
+class, and label. Direct links are shown instead of link values. For example:
+
+```jsonld
+{
+ "@graph" : [ {
+ "@id" : "http://rdfh.ch/0001/0C-0L1kORryKzJAJxxRyRQ",
+ "@type" : "anything:Thing",
+ "rdfs:label" : "Sierra"
+ }, {
+ "@id" : "http://rdfh.ch/0001/A67ka6UQRHWf313tbhQBjw",
+ "@type" : "anything:Thing",
+ "rdfs:label" : "Victor"
+ }, {
+ "@id" : "http://rdfh.ch/0001/Lz7WEqJETJqqsUZQYexBQg",
+ "@type" : "anything:Thing",
+ "rdfs:label" : "Foxtrot"
+ }, {
+ "@id" : "http://rdfh.ch/0001/WLSHxQUgTOmG1T0lBU2r5w",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/A67ka6UQRHWf313tbhQBjw"
+ },
+ "rdfs:label" : "Tango"
+ }, {
+ "@id" : "http://rdfh.ch/0001/start",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : [ {
+ "@id" : "http://rdfh.ch/0001/0C-0L1kORryKzJAJxxRyRQ"
+ }, {
+ "@id" : "http://rdfh.ch/0001/WLSHxQUgTOmG1T0lBU2r5w"
+ }, {
+ "@id" : "http://rdfh.ch/0001/tPfZeNMvRVujCQqbIbvO0A"
+ } ],
+ "rdfs:label" : "Romeo"
+ }, {
+ "@id" : "http://rdfh.ch/0001/tPfZeNMvRVujCQqbIbvO0A",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/Lz7WEqJETJqqsUZQYexBQg"
+ },
+ "rdfs:label" : "Echo"
+ } ],
+ "@context" : {
+ "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+ "knora-api" : "http://api.knora.org/ontology/knora-api/v2#",
+ "rdfs" : "http://www.w3.org/2000/01/rdf-schema#",
+ "xsd" : "http://www.w3.org/2001/XMLSchema#",
+ "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#"
+ }
+}
+```
+
## Search for Resources
### Search for a Resource by its `rdfs:label`
@@ -122,8 +196,10 @@ specific. The first term should at least contain four characters. To
make this kind of "search as you type" possible, a wildcard character is
automatically added to the last search term.
- HTTP GET to http://host/v2/searchbylabel/searchValue[limitToResourceClass=resourceClassIRI]
- [limitToProject=projectIRI][offset=Integer]
+```
+HTTP GET to http://host/v2/searchbylabel/searchValue[limitToResourceClass=resourceClassIRI]
+[limitToProject=projectIRI][offset=Integer]
+```
The first parameter must be preceded by a question mark `?`, any
following parameter by an ampersand `&`.
diff --git a/webapi/src/main/resources/application.conf b/webapi/src/main/resources/application.conf
index b224f25abf..926b768b26 100644
--- a/webapi/src/main/resources/application.conf
+++ b/webapi/src/main/resources/application.conf
@@ -332,6 +332,11 @@ app {
},
fulltext-search {
search-value-min-length = 3
+ },
+ graph-route {
+ default-graph-depth = 4
+ max-graph-depth = 10
+ max-graph-breadth = 50
}
}
diff --git a/webapi/src/main/scala/org/knora/webapi/Settings.scala b/webapi/src/main/scala/org/knora/webapi/Settings.scala
index 9f47dcba30..14a649f209 100644
--- a/webapi/src/main/scala/org/knora/webapi/Settings.scala
+++ b/webapi/src/main/scala/org/knora/webapi/Settings.scala
@@ -127,10 +127,13 @@ class SettingsImpl(config: Config) extends Extension {
val defaultIconSizeDimX: Int = config.getInt("app.gui.default-icon-size.dimX")
val defaultIconSizeDimY: Int = config.getInt("app.gui.default-icon-size.dimY")
-
val v2ResultsPerPage: Int = config.getInt("app.v2.resources-sequence.results-per-page")
val searchValueMinLength: Int = config.getInt("app.v2.fulltext-search.search-value-min-length")
+ val defaultGraphDepth: Int = config.getInt("app.v2.graph-route.default-graph-depth")
+ val maxGraphDepth: Int = config.getInt("app.v2.graph-route.max-graph-depth")
+ val maxGraphBreadth: Int = config.getInt("app.v2.graph-route.max-graph-breadth")
+
val triplestoreType: String = config.getString("app.triplestore.dbtype")
val triplestoreHost: String = config.getString("app.triplestore.host")
diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/KnoraApiV2Simple.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/KnoraApiV2Simple.scala
index 3b7ddd7f3a..84ef01b1a7 100644
--- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/KnoraApiV2Simple.scala
+++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/KnoraApiV2Simple.scala
@@ -571,43 +571,6 @@ object KnoraApiV2Simple {
)
}
- /**
- * Makes a [[ReadClassInfoV2]] representing an owl:Class.
- *
- * @param classIri the IRI of the class.
- * @param subClassOf the set of direct superclasses of this class.
- * @param predicates the predicates of the class.
- * @param directCardinalities the direct cardinalities of the class.
- * @param inheritedCardinalities the inherited cardinalities of the class.
- * @return a [[ReadClassInfoV2]].
- */
- private def makeClass(classIri: IRI,
- subClassOf: Set[IRI] = Set.empty[IRI],
- predicates: Seq[PredicateInfoV2] = Seq.empty[PredicateInfoV2],
- directCardinalities: Map[IRI, Cardinality.Value] = Map.empty[IRI, Cardinality.Value],
- inheritedCardinalities: Map[SmartIri, KnoraCardinalityInfo] = Map.empty[SmartIri, KnoraCardinalityInfo]): ReadClassInfoV2 = {
-
- val rdfType = OntologyConstants.Rdf.Type.toSmartIri -> PredicateInfoV2(
- predicateIri = OntologyConstants.Rdf.Type.toSmartIri,
- objects = Seq(SmartIriLiteralV2(OntologyConstants.Owl.Class.toSmartIri))
- )
-
- ReadClassInfoV2(
- entityInfoContent = ClassInfoContentV2(
- classIri = classIri.toSmartIri,
- predicates = predicates.map {
- pred => pred.predicateIri -> pred
- }.toMap + rdfType,
- directCardinalities = directCardinalities.map {
- case (propertyIri, cardinality) => propertyIri.toSmartIri -> KnoraCardinalityInfo(cardinality)
- },
- subClassOf = subClassOf.map(_.toSmartIri),
- ontologySchema = ApiV2Simple
- ),
- inheritedCardinalities = inheritedCardinalities
- )
- }
-
/**
* Makes a [[ReadClassInfoV2]] representing an rdfs:Datatype.
*
diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/KnoraApiV2WithValueObjects.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/KnoraApiV2WithValueObjects.scala
index 285397aad9..7165fc7c7e 100644
--- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/KnoraApiV2WithValueObjects.scala
+++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/KnoraApiV2WithValueObjects.scala
@@ -1577,49 +1577,4 @@ object KnoraApiV2WithValueObjects {
isLinkValueProp = isLinkValueProp
)
}
-
- /**
- * Makes a [[ReadClassInfoV2]].
- *
- * @param classIri the IRI of the class.
- * @param subClassOf the set of direct superclasses of this class.
- * @param predicates the predicates of the class.
- * @param isResourceClass true if this is a subclass of `knora-api:Resource`.
- * @param canBeInstantiated true if this is a Knora resource class that can be instantiated via the Knora API.
- * @param directCardinalities the direct cardinalities of the class.
- * @param inheritedCardinalities the inherited cardinalities of the class.
- * @return a [[ReadClassInfoV2]].
- */
- private def makeClass(classIri: IRI,
- subClassOf: Set[IRI] = Set.empty[IRI],
- predicates: Seq[PredicateInfoV2] = Seq.empty[PredicateInfoV2],
- isResourceClass: Boolean = false,
- canBeInstantiated: Boolean = false,
- isValueClass: Boolean = false,
- directCardinalities: Map[IRI, Cardinality.Value] = Map.empty[IRI, Cardinality.Value],
- inheritedCardinalities: Map[SmartIri, KnoraCardinalityInfo] = Map.empty[SmartIri, KnoraCardinalityInfo]): ReadClassInfoV2 = {
- val rdfType = OntologyConstants.Rdf.Type.toSmartIri -> PredicateInfoV2(
- predicateIri = OntologyConstants.Rdf.Type.toSmartIri,
- objects = Seq(SmartIriLiteralV2(OntologyConstants.Owl.Class.toSmartIri))
- )
-
- ReadClassInfoV2(
- entityInfoContent = ClassInfoContentV2(
- classIri = classIri.toSmartIri,
- predicates = predicates.map {
- pred => pred.predicateIri -> pred
- }.toMap + rdfType,
- directCardinalities = directCardinalities.map {
- case (propertyIri, cardinality) => propertyIri.toSmartIri -> KnoraCardinalityInfo(cardinality)
- },
- subClassOf = subClassOf.map(iri => iri.toSmartIri),
- ontologySchema = ApiV2WithValueObjects
- ),
- inheritedCardinalities = inheritedCardinalities,
- canBeInstantiated = canBeInstantiated,
- isResourceClass = isResourceClass,
- isValueClass = isValueClass
- )
- }
-
}
diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala
index a6927d64e6..1da117cc86 100644
--- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala
+++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala
@@ -536,7 +536,6 @@ case class ReadResourcesSequenceV2(numberOfResources: Int, resources: Seq[ReadRe
toOntologySchema(targetSchema).generateJsonLD(targetSchema, settings)
}
-
/**
* Checks that a [[ReadResourcesSequenceV2]] contains exactly one resource, and returns that resource. If the resource
* is not present, or if it's `ForbiddenResource`, throws an exception.
@@ -562,3 +561,143 @@ case class ReadResourcesSequenceV2(numberOfResources: Int, resources: Seq[ReadRe
resourceInfo
}
}
+
+/**
+ * Requests a graph of resources that are reachable via links to or from a given resource. A successful response
+ * will be a [[GraphDataGetResponseV2]].
+ *
+ * @param resourceIri the IRI of the initial resource.
+ * @param depth the maximum depth of the graph, counting from the initial resource.
+ * @param inbound `true` to query inbound links.
+ * @param outbound `true` to query outbound links.
+ * @param excludeProperty the IRI of a link property to exclude from the results.
+ * @param requestingUser the user making the request.
+ */
+case class GraphDataGetRequestV2(resourceIri: IRI,
+ depth: Int,
+ inbound: Boolean,
+ outbound: Boolean,
+ excludeProperty: Option[SmartIri],
+ requestingUser: UserADM) extends ResourcesResponderRequestV2 {
+ if (!(inbound || outbound)) {
+ throw BadRequestException("No link direction selected")
+ }
+}
+
+/**
+ * Represents a node (i.e. a resource) in a resource graph.
+ *
+ * @param resourceIri the IRI of the resource.
+ * @param resourceLabel the label of the resource.
+ * @param resourceClassIri the IRI of the resource's OWL class.
+ */
+case class GraphNodeV2(resourceIri: IRI, resourceClassIri: SmartIri, resourceLabel: String) extends KnoraReadV2[GraphNodeV2] {
+ override def toOntologySchema(targetSchema: ApiV2Schema): GraphNodeV2 = {
+ copy(resourceClassIri = resourceClassIri.toOntologySchema(targetSchema))
+ }
+}
+
+/**
+ * Represents an edge (i.e. a link) in a resource graph.
+ *
+ * @param source the resource that is the source of the link.
+ * @param propertyIri the link property that links the source to the target.
+ * @param target the resource that is the target of the link.
+ */
+case class GraphEdgeV2(source: IRI, propertyIri: SmartIri, target: IRI) extends KnoraReadV2[GraphEdgeV2] {
+ override def toOntologySchema(targetSchema: ApiV2Schema): GraphEdgeV2 = {
+ copy(propertyIri = propertyIri.toOntologySchema(targetSchema))
+ }
+}
+
+/**
+ * Represents a graph of resources.
+ *
+ * @param nodes the nodes in the graph.
+ * @param edges the edges in the graph.
+ */
+case class GraphDataGetResponseV2(nodes: Seq[GraphNodeV2], edges: Seq[GraphEdgeV2], ontologySchema: OntologySchema) extends KnoraResponseV2 with KnoraReadV2[GraphDataGetResponseV2] {
+ private def generateJsonLD(targetSchema: ApiV2Schema, settings: SettingsImpl): JsonLDDocument = {
+ implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance
+
+ val sortedNodesInTargetSchema: Seq[GraphNodeV2] = nodes.map(_.toOntologySchema(targetSchema)).sortBy(_.resourceIri)
+ val edgesInTargetSchema: Seq[GraphEdgeV2] = edges.map(_.toOntologySchema(targetSchema))
+
+ // Make JSON-LD prefixes for the project-specific ontologies used in the response.
+
+ val resourceOntologiesUsed: Set[SmartIri] = sortedNodesInTargetSchema.map(_.resourceClassIri.getOntologyFromEntity).toSet.filter(!_.isKnoraBuiltInDefinitionIri)
+ val propertyOntologiesUsed: Set[SmartIri] = edgesInTargetSchema.map(_.propertyIri.getOntologyFromEntity).toSet.filter(!_.isKnoraBuiltInDefinitionIri)
+ val projectSpecificOntologiesUsed = resourceOntologiesUsed ++ propertyOntologiesUsed
+
+ // Make the knora-api prefix for the target schema.
+
+ val knoraApiPrefixExpansion = targetSchema match {
+ case ApiV2Simple => OntologyConstants.KnoraApiV2Simple.KnoraApiV2PrefixExpansion
+ case ApiV2WithValueObjects => OntologyConstants.KnoraApiV2WithValueObjects.KnoraApiV2PrefixExpansion
+ }
+
+ // Make the JSON-LD context.
+
+ val context = JsonLDUtil.makeContext(
+ fixedPrefixes = Map(
+ "rdf" -> OntologyConstants.Rdf.RdfPrefixExpansion,
+ "rdfs" -> OntologyConstants.Rdfs.RdfsPrefixExpansion,
+ "xsd" -> OntologyConstants.Xsd.XsdPrefixExpansion,
+ OntologyConstants.KnoraApi.KnoraApiOntologyLabel -> knoraApiPrefixExpansion
+ ),
+ knoraOntologiesNeedingPrefixes = projectSpecificOntologiesUsed
+ )
+
+ // Group the edges by source IRI and add them to the nodes.
+
+ val groupedEdges: Map[IRI, Seq[GraphEdgeV2]] = edgesInTargetSchema.groupBy(_.source)
+
+ val nodesWithEdges: Seq[JsonLDObject] = sortedNodesInTargetSchema.map {
+ node: GraphNodeV2 =>
+ // Convert the node to JSON-LD.
+ val jsonLDNodeMap = Map(
+ JsonLDConstants.ID -> JsonLDString(node.resourceIri),
+ JsonLDConstants.TYPE -> JsonLDString(node.resourceClassIri.toString),
+ OntologyConstants.Rdfs.Label -> JsonLDString(node.resourceLabel)
+ )
+
+ // Is this node the source of any edges?
+ groupedEdges.get(node.resourceIri) match {
+ case Some(nodeEdges: Seq[GraphEdgeV2]) =>
+ // Yes. Convert them to JSON-LD and add them to the node.
+
+ val nodeEdgesGroupedAndSortedByProperty: Vector[(SmartIri, Seq[GraphEdgeV2])] = nodeEdges.groupBy(_.propertyIri).toVector.sortBy(_._1)
+
+ val jsonLDNodeEdges: Map[IRI, JsonLDArray] = nodeEdgesGroupedAndSortedByProperty.map {
+ case (propertyIri: SmartIri, propertyEdges: Seq[GraphEdgeV2]) =>
+ val sortedPropertyEdges = propertyEdges.sortBy(_.target)
+ propertyIri.toString -> JsonLDArray(sortedPropertyEdges.map(propertyEdge => JsonLDUtil.iriToJsonLDObject(propertyEdge.target)))
+ }.toMap
+
+ JsonLDObject(jsonLDNodeMap ++ jsonLDNodeEdges)
+
+ case None =>
+ // This node isn't the source of any edges.
+ JsonLDObject(jsonLDNodeMap)
+ }
+ }
+
+ // Make the JSON-LD document.
+
+ val body = JsonLDObject(Map(JsonLDConstants.GRAPH -> JsonLDArray(nodesWithEdges)))
+
+ JsonLDDocument(body = body, context = context)
+ }
+
+ override def toJsonLDDocument(targetSchema: ApiV2Schema, settings: SettingsImpl): JsonLDDocument = {
+ toOntologySchema(targetSchema).generateJsonLD(targetSchema, settings)
+ }
+
+ override def toOntologySchema(targetSchema: ApiV2Schema): GraphDataGetResponseV2 = {
+ GraphDataGetResponseV2(
+ nodes = nodes.map(_.toOntologySchema(targetSchema)),
+ edges = edges.map(_.toOntologySchema(targetSchema)),
+ ontologySchema = targetSchema
+ )
+ }
+}
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 74bc70534c..0bb9901009 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
@@ -29,7 +29,7 @@ import akka.stream.ActorMaterializer
import org.knora.webapi._
import org.knora.webapi.messages.admin.responder.permissionsmessages.{DefaultObjectAccessPermissionsStringForResourceClassGetADM, DefaultObjectAccessPermissionsStringResponseADM, ResourceCreateOperation}
import org.knora.webapi.messages.admin.responder.usersmessages.UserADM
-import org.knora.webapi.messages.store.triplestoremessages.{SparqlConstructRequest, SparqlConstructResponse, SparqlUpdateRequest, SparqlUpdateResponse}
+import org.knora.webapi.messages.store.triplestoremessages._
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
@@ -67,6 +67,7 @@ class ResourcesResponderV2 extends ResponderWithStandoffV2 {
case ResourcesPreviewGetRequestV2(resIris, requestingUser) => future2Message(sender(), getResourcePreview(resIris, requestingUser), log)
case ResourceTEIGetRequestV2(resIri, textProperty, mappingIri, gravsearchTemplateIri, headerXSLTIri, requestingUser) => future2Message(sender(), getResourceAsTEI(resIri, textProperty, mappingIri, gravsearchTemplateIri, headerXSLTIri, requestingUser), log)
case createResourceRequestV2: CreateResourceRequestV2 => future2Message(sender(), createResourceV2(createResourceRequestV2), log)
+ case graphDataGetRequest: GraphDataGetRequestV2 => future2Message(sender(), getGraphDataResponseV2(graphDataGetRequest), log)
case other => handleUnexpectedMessage(sender(), other, log, this.getClass.getName)
}
@@ -1067,5 +1068,307 @@ class ResourcesResponderV2 extends ResponderWithStandoffV2 {
getResourcePreview(targetResourceIris.toSeq, requestingUser).map(_ => ())
}
}
+
+ /**
+ * Gets a graph of resources that are reachable via links to or from a given resource.
+ *
+ * @param graphDataGetRequest a [[GraphDataGetRequestV2]] specifying the characteristics of the graph.
+ * @return a [[GraphDataGetResponseV2]] representing the requested graph.
+ */
+ private def getGraphDataResponseV2(graphDataGetRequest: GraphDataGetRequestV2): Future[GraphDataGetResponseV2] = {
+ val excludePropertyInternal = graphDataGetRequest.excludeProperty.map(_.toOntologySchema(InternalSchema))
+
+ /**
+ * The internal representation of a node returned by a SPARQL query generated by the `getGraphData` template.
+ *
+ * @param nodeIri the IRI of the node.
+ * @param nodeClass the IRI of the node's class.
+ * @param nodeLabel the node's label.
+ * @param nodeCreator the node's creator.
+ * @param nodeProject the node's project.
+ * @param nodePermissions the node's permissions.
+ */
+ case class QueryResultNode(nodeIri: IRI,
+ nodeClass: SmartIri,
+ nodeLabel: String,
+ nodeCreator: IRI,
+ nodeProject: IRI,
+ nodePermissions: String)
+
+ /**
+ * The internal representation of an edge returned by a SPARQL query generated by the `getGraphData` template.
+ *
+ * @param linkValueIri the IRI of the link value.
+ * @param sourceNodeIri the IRI of the source node.
+ * @param targetNodeIri the IRI of the target node.
+ * @param linkProp the IRI of the link property.
+ * @param linkValueCreator the link value's creator.
+ * @param sourceNodeProject the project of the source node.
+ * @param linkValuePermissions the link value's permissions.
+ */
+ case class QueryResultEdge(linkValueIri: IRI,
+ sourceNodeIri: IRI,
+ targetNodeIri: IRI,
+ linkProp: SmartIri,
+ linkValueCreator: IRI,
+ sourceNodeProject: IRI,
+ linkValuePermissions: String)
+
+ /**
+ * Represents results returned by a SPARQL query generated by the `getGraphData` template.
+ *
+ * @param nodes the nodes that were returned by the query.
+ * @param edges the edges that were returned by the query.
+ */
+ case class GraphQueryResults(nodes: Set[QueryResultNode] = Set.empty[QueryResultNode], edges: Set[QueryResultEdge] = Set.empty[QueryResultEdge])
+
+ /**
+ * Recursively queries outbound or inbound links from/to a resource.
+ *
+ * @param startNode the node to use as the starting point of the query. The user is assumed to have permission
+ * to see this node.
+ * @param outbound `true` to get outbound links, `false` to get inbound links.
+ * @param depth the maximum depth of the query.
+ * @param traversedEdges edges that have already been traversed.
+ * @return a [[GraphQueryResults]].
+ */
+ def traverseGraph(startNode: QueryResultNode, outbound: Boolean, depth: Int, traversedEdges: Set[QueryResultEdge] = Set.empty[QueryResultEdge]): Future[GraphQueryResults] = {
+ if (depth < 1) Future.failed(AssertionException("Depth must be at least 1"))
+
+ for {
+ // Get the direct links from/to the start node.
+ sparql <- Future(queries.sparql.v2.txt.getGraphData(
+ triplestore = settings.triplestoreType,
+ startNodeIri = startNode.nodeIri,
+ startNodeOnly = false,
+ maybeExcludeLinkProperty = excludePropertyInternal,
+ outbound = outbound, // true to query outbound edges, false to query inbound edges
+ limit = settings.maxGraphBreadth
+ ).toString())
+
+ // _ = println(sparql)
+
+ response: SparqlSelectResponse <- (storeManager ? SparqlSelectRequest(sparql)).mapTo[SparqlSelectResponse]
+ rows: Seq[VariableResultsRow] = response.results.bindings
+
+ // Did we get any results?
+ recursiveResults: GraphQueryResults <- if (rows.isEmpty) {
+ // No. Return nothing.
+ Future(GraphQueryResults())
+ } else {
+ // Yes. Get the nodes from the query results.
+ val otherNodes: Seq[QueryResultNode] = rows.map {
+ row: VariableResultsRow =>
+ val rowMap: Map[String, String] = row.rowMap
+
+ QueryResultNode(
+ nodeIri = rowMap("node"),
+ nodeClass = rowMap("nodeClass").toSmartIri,
+ nodeLabel = rowMap("nodeLabel"),
+ nodeCreator = rowMap("nodeCreator"),
+ nodeProject = rowMap("nodeProject"),
+ nodePermissions = rowMap("nodePermissions")
+ )
+ }.filter {
+ node: QueryResultNode =>
+ // Filter out the nodes that the user doesn't have permission to see.
+ PermissionUtilADM.getUserPermissionADM(
+ entityIri = node.nodeIri,
+ entityCreator = node.nodeCreator,
+ entityProject = node.nodeProject,
+ entityPermissionLiteral = node.nodePermissions,
+ requestingUser = graphDataGetRequest.requestingUser
+ ).nonEmpty
+ }
+
+ // Collect the IRIs of the nodes that the user has permission to see, including the start node.
+ val visibleNodeIris: Set[IRI] = otherNodes.map(_.nodeIri).toSet + startNode.nodeIri
+
+ // Get the edges from the query results.
+ val edges: Set[QueryResultEdge] = rows.map {
+ row: VariableResultsRow =>
+ val rowMap: Map[String, String] = row.rowMap
+ val nodeIri: IRI = rowMap("node")
+
+ // The SPARQL query takes a start node and returns the other node in the edge.
+ //
+ // If we're querying outbound edges, the start node is the source node, and the other
+ // node is the target node.
+ //
+ // If we're querying inbound edges, the start node is the target node, and the other
+ // node is the source node.
+
+ QueryResultEdge(
+ linkValueIri = rowMap("linkValue"),
+ sourceNodeIri = if (outbound) startNode.nodeIri else nodeIri,
+ targetNodeIri = if (outbound) nodeIri else startNode.nodeIri,
+ linkProp = rowMap("linkProp").toSmartIri,
+ linkValueCreator = rowMap("linkValueCreator"),
+ sourceNodeProject = if (outbound) startNode.nodeProject else rowMap("nodeProject"),
+ linkValuePermissions = rowMap("linkValuePermissions")
+ )
+ }.filter {
+ edge: QueryResultEdge =>
+ // Filter out the edges that the user doesn't have permission to see. To see an edge,
+ // the user must have some permission on the link value and on the source and target
+ // nodes.
+ val hasPermission: Boolean = visibleNodeIris.contains(edge.sourceNodeIri) && visibleNodeIris.contains(edge.targetNodeIri) &&
+ PermissionUtilADM.getUserPermissionADM(
+ entityIri = edge.linkValueIri,
+ entityCreator = edge.linkValueCreator,
+ entityProject = edge.sourceNodeProject,
+ entityPermissionLiteral = edge.linkValuePermissions,
+ requestingUser = graphDataGetRequest.requestingUser
+ ).nonEmpty
+
+ // Filter out edges we've already traversed.
+ val isRedundant: Boolean = traversedEdges.contains(edge)
+ // if (isRedundant) println(s"filtering out edge from ${edge.sourceNodeIri} to ${edge.targetNodeIri}")
+
+ hasPermission && !isRedundant
+ }.toSet
+
+ // Include only nodes that are reachable via edges that we're going to traverse (i.e. the user
+ // has permission to see those edges, and we haven't already traversed them).
+ val visibleNodeIrisFromEdges: Set[IRI] = edges.map(_.sourceNodeIri) ++ edges.map(_.targetNodeIri)
+ val filteredOtherNodes: Seq[QueryResultNode] = otherNodes.filter(node => visibleNodeIrisFromEdges.contains(node.nodeIri))
+
+ // Make a GraphQueryResults containing the resulting nodes and edges, including the start
+ // node.
+ val results = GraphQueryResults(nodes = filteredOtherNodes.toSet + startNode, edges = edges)
+
+ // Have we reached the maximum depth?
+ if (depth == 1) {
+ // Yes. Just return the results we have.
+ Future(results)
+ } else {
+ // No. Recursively get results for each of the nodes we found.
+
+ val traversedEdgesForRecursion: Set[QueryResultEdge] = traversedEdges ++ edges
+
+ val lowerResultFutures: Seq[Future[GraphQueryResults]] = filteredOtherNodes.map {
+ node =>
+ traverseGraph(
+ startNode = node,
+ outbound = outbound,
+ depth = depth - 1,
+ traversedEdges = traversedEdgesForRecursion
+ )
+ }
+
+ val lowerResultsFuture: Future[Seq[GraphQueryResults]] = Future.sequence(lowerResultFutures)
+
+ // Return those results plus the ones we found.
+
+ lowerResultsFuture.map {
+ lowerResultsSeq: Seq[GraphQueryResults] =>
+ lowerResultsSeq.foldLeft(results) {
+ case (acc: GraphQueryResults, lowerResults: GraphQueryResults) =>
+ GraphQueryResults(
+ nodes = acc.nodes ++ lowerResults.nodes,
+ edges = acc.edges ++ lowerResults.edges
+ )
+ }
+ }
+ }
+ }
+ } yield recursiveResults
+ }
+
+ for {
+ // Get the start node.
+ sparql <- Future(queries.sparql.v2.txt.getGraphData(
+ triplestore = settings.triplestoreType,
+ startNodeIri = graphDataGetRequest.resourceIri,
+ maybeExcludeLinkProperty = excludePropertyInternal,
+ startNodeOnly = true,
+ outbound = true,
+ limit = settings.maxGraphBreadth
+ ).toString())
+
+ // _ = println(sparql)
+
+ response: SparqlSelectResponse <- (storeManager ? SparqlSelectRequest(sparql)).mapTo[SparqlSelectResponse]
+ rows: Seq[VariableResultsRow] = response.results.bindings
+
+ _ = if (rows.isEmpty) {
+ throw NotFoundException(s"Resource <${graphDataGetRequest.resourceIri}> not found (it may have been deleted)")
+ }
+
+ firstRowMap: Map[String, String] = rows.head.rowMap
+
+ startNode: QueryResultNode = QueryResultNode(
+ nodeIri = firstRowMap("node"),
+ nodeClass = firstRowMap("nodeClass").toSmartIri,
+ nodeLabel = firstRowMap("nodeLabel"),
+ nodeCreator = firstRowMap("nodeCreator"),
+ nodeProject = firstRowMap("nodeProject"),
+ nodePermissions = firstRowMap("nodePermissions")
+ )
+
+ // Make sure the user has permission to see the start node.
+ _ = if (PermissionUtilADM.getUserPermissionADM(
+ entityIri = startNode.nodeIri,
+ entityCreator = startNode.nodeCreator,
+ entityProject = startNode.nodeProject,
+ entityPermissionLiteral = startNode.nodePermissions,
+ requestingUser = graphDataGetRequest.requestingUser
+ ).isEmpty) {
+ throw ForbiddenException(s"User ${graphDataGetRequest.requestingUser.email} does not have permission to view resource <${graphDataGetRequest.resourceIri}>")
+ }
+
+ // Recursively get the graph containing outbound links.
+ outboundQueryResults: GraphQueryResults <- if (graphDataGetRequest.outbound) {
+ traverseGraph(
+ startNode = startNode,
+ outbound = true,
+ depth = graphDataGetRequest.depth
+ )
+ } else {
+ FastFuture.successful(GraphQueryResults())
+ }
+
+ // Recursively get the graph containing inbound links.
+ inboundQueryResults: GraphQueryResults <- if (graphDataGetRequest.inbound) {
+ traverseGraph(
+ startNode = startNode,
+ outbound = false,
+ depth = graphDataGetRequest.depth
+ )
+ } else {
+ FastFuture.successful(GraphQueryResults())
+ }
+
+ // Combine the outbound and inbound graphs into a single graph.
+ nodes: Set[QueryResultNode] = outboundQueryResults.nodes ++ inboundQueryResults.nodes + startNode
+ edges: Set[QueryResultEdge] = outboundQueryResults.edges ++ inboundQueryResults.edges
+
+ // Convert each node to a GraphNodeV2 for the API response message.
+ resultNodes: Vector[GraphNodeV2] = nodes.map {
+ node: QueryResultNode =>
+ GraphNodeV2(
+ resourceIri = node.nodeIri,
+ resourceClassIri = node.nodeClass,
+ resourceLabel = node.nodeLabel,
+ )
+ }.toVector
+
+ // Convert each edge to a GraphEdgeV2 for the API response message.
+ resultEdges: Vector[GraphEdgeV2] = edges.map {
+ edge: QueryResultEdge =>
+ GraphEdgeV2(
+ source = edge.sourceNodeIri,
+ propertyIri = edge.linkProp,
+ target = edge.targetNodeIri,
+ )
+ }.toVector
+
+ } yield GraphDataGetResponseV2(
+ nodes = resultNodes,
+ edges = resultEdges,
+ ontologySchema = InternalSchema
+ )
+ }
+
}
diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala
index cca28185e4..58131afbd3 100644
--- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala
+++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala
@@ -27,7 +27,7 @@ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import akka.util.Timeout
import org.knora.webapi._
-import org.knora.webapi.messages.v2.responder.resourcemessages.{CreateResourceRequestV2, ResourceTEIGetRequestV2, ResourcesGetRequestV2, ResourcesPreviewGetRequestV2}
+import org.knora.webapi.messages.v2.responder.resourcemessages._
import org.knora.webapi.responders.RESPONDER_MANAGER_ACTOR_PATH
import org.knora.webapi.routing.{Authenticator, RouteUtilV2}
import org.knora.webapi.store.STORE_MANAGER_ACTOR_PATH
@@ -45,6 +45,12 @@ object ResourcesRouteV2 extends Authenticator {
private val Mapping_Iri = "mappingIri"
private val GravsearchTemplate_Iri = "gravsearchTemplateIri"
private val TEIHeader_XSLT_IRI = "teiHeaderXSLTIri"
+ private val Depth = "depth"
+ private val ExcludeProperty = "excludeProperty"
+ private val Direction = "direction"
+ private val Inbound = "inbound"
+ private val Outbound = "outbound"
+ private val Both = "both"
/**
* Gets the Iri of the property that represents the text of the resource.
@@ -58,10 +64,10 @@ object ResourcesRouteV2 extends Authenticator {
textProperty match {
case Some(textPropIriStr: String) =>
- val externalResourceClassIri = textPropIriStr.toSmartIriWithErr(throw BadRequestException(s"Invalid property IRI: $textPropIriStr"))
+ val externalResourceClassIri = textPropIriStr.toSmartIriWithErr(throw BadRequestException(s"Invalid property IRI: <$textPropIriStr>"))
if (!externalResourceClassIri.isKnoraApiV2EntityIri) {
- throw BadRequestException(s"$textPropIriStr is not a valid knora-api property IRI")
+ throw BadRequestException(s"<$textPropIriStr> is not a valid knora-api property IRI")
}
externalResourceClassIri.toOntologySchema(InternalSchema)
@@ -82,7 +88,7 @@ object ResourcesRouteV2 extends Authenticator {
mappingIriStr match {
case Some(mapping: String) =>
- Some(stringFormatter.validateAndEscapeIri(mapping, throw BadRequestException(s"Invalid mapping IRI: '$mapping'")))
+ Some(stringFormatter.validateAndEscapeIri(mapping, throw BadRequestException(s"Invalid mapping IRI: <$mapping>")))
case None => None
}
@@ -100,7 +106,7 @@ object ResourcesRouteV2 extends Authenticator {
gravsearchTemplateIriStr match {
case Some(gravsearch: String) =>
- Some(stringFormatter.validateAndEscapeIri(gravsearch, throw BadRequestException(s"Invalid template IRI: '$gravsearch'")))
+ Some(stringFormatter.validateAndEscapeIri(gravsearch, throw BadRequestException(s"Invalid template IRI: <$gravsearch>")))
case None => None
}
@@ -118,7 +124,7 @@ object ResourcesRouteV2 extends Authenticator {
headerXSLTIriStr match {
case Some(xslt: String) =>
- Some(stringFormatter.validateAndEscapeIri(xslt, throw BadRequestException(s"Invalid XSLT IRI: '$xslt'")))
+ Some(stringFormatter.validateAndEscapeIri(xslt, throw BadRequestException(s"Invalid XSLT IRI: <$xslt>")))
case None => None
}
@@ -170,7 +176,7 @@ object ResourcesRouteV2 extends Authenticator {
val resourceIris: Seq[IRI] = resIris.map {
resIri: String =>
- stringFormatter.validateAndEscapeIri(resIri, throw BadRequestException(s"Invalid resource IRI: '$resIri'"))
+ stringFormatter.validateAndEscapeIri(resIri, throw BadRequestException(s"Invalid resource IRI: <$resIri>"))
}
val requestMessage: Future[ResourcesGetRequestV2] = for {
@@ -194,7 +200,7 @@ object ResourcesRouteV2 extends Authenticator {
val resourceIris: Seq[IRI] = resIris.map {
resIri: String =>
- stringFormatter.validateAndEscapeIri(resIri, throw BadRequestException(s"Invalid resource IRI: '$resIri'"))
+ stringFormatter.validateAndEscapeIri(resIri, throw BadRequestException(s"Invalid resource IRI: <$resIri>"))
}
val requestMessage: Future[ResourcesPreviewGetRequestV2] = for {
@@ -216,7 +222,7 @@ object ResourcesRouteV2 extends Authenticator {
get {
requestContext => {
- val resourceIri = stringFormatter.validateAndEscapeIri(resIri, throw BadRequestException(s"Invalid resource IRI: '$resIri'"))
+ val resourceIri: IRI = stringFormatter.validateAndEscapeIri(resIri, throw BadRequestException(s"Invalid resource IRI: <$resIri>"))
val params: Map[String, String] = requestContext.request.uri.query().toMap
@@ -251,9 +257,52 @@ object ResourcesRouteV2 extends Authenticator {
}
}
- }
+ } ~ path("v2" / "graph" / Segment) { resIriStr: String =>
+ get {
+ requestContext => {
+ val resourceIri: IRI = stringFormatter.validateAndEscapeIri(resIriStr, throw BadRequestException(s"Invalid resource IRI: <$resIriStr>"))
+ val params: Map[String, String] = requestContext.request.uri.query().toMap
+ val depth: Int = params.get(Depth).map(_.toInt).getOrElse(settings.defaultGraphDepth)
- }
+ if (depth < 1) {
+ throw BadRequestException(s"$Depth must be at least 1")
+ }
+
+ if (depth > settings.maxGraphDepth) {
+ throw BadRequestException(s"$Depth cannot be greater than ${settings.maxGraphDepth}")
+ }
+
+ val direction: String = params.getOrElse(Direction, Outbound)
+ val excludeProperty: Option[SmartIri] = params.get(ExcludeProperty).map(propIriStr => propIriStr.toSmartIriWithErr(throw BadRequestException(s"Invalid property IRI: <$propIriStr>")))
+ val (inbound: Boolean, outbound: Boolean) = direction match {
+ case Inbound => (true, false)
+ case Outbound => (false, true)
+ case Both => (true, true)
+ case other => throw BadRequestException(s"Invalid direction: $other")
+ }
+ val requestMessage: Future[GraphDataGetRequestV2] = for {
+ requestingUser <- getUserADM(requestContext)
+ } yield GraphDataGetRequestV2(
+ resourceIri = resourceIri,
+ depth = depth,
+ inbound = inbound,
+ outbound = outbound,
+ excludeProperty = excludeProperty,
+ requestingUser = requestingUser
+ )
+
+ RouteUtilV2.runRdfRouteWithFuture(
+ requestMessage,
+ requestContext,
+ settings,
+ responderManager,
+ log,
+ RouteUtilV2.getOntologySchema(requestContext)
+ )
+ }
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/webapi/src/main/twirl/queries/sparql/v2/getGraphData.scala.txt b/webapi/src/main/twirl/queries/sparql/v2/getGraphData.scala.txt
new file mode 100644
index 0000000000..0646670cda
--- /dev/null
+++ b/webapi/src/main/twirl/queries/sparql/v2/getGraphData.scala.txt
@@ -0,0 +1,61 @@
+@*
+ * Copyright © 2015-2018 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 .
+ *@
+
+@import org.knora.webapi._
+@import org.knora.webapi.util.SmartIri
+
+@**
+ * Gets the outbound or inbound links from/to a resource. This query is used recursively to get a graph of
+ * resources reachable from a given resource.
+ *
+ * If the triplestore type is GraphDB, this template delegates to getGraphDataGraphDB.sparql.txt, which is optimised
+ * for GraphDB. Otherwise, it delegates to getGraphDataStandard.sparql.txt.
+ *
+ * @param triplestore the name of the triplestore being used.
+ * @param startNodeIri the IRI of the resource to use as the starting point of the query.
+ * @param startNodeOnly if true, returns information only about the start node.
+ * @param maybeExcludeLinkProperty if provided, a link property that should be excluded from the results.
+ * @param outbound true to get outbound links, false to get inbound links.
+ * @param limit the maximum number of edges to return.
+ *@
+@(triplestore: String,
+ startNodeIri: IRI,
+ startNodeOnly: Boolean,
+ maybeExcludeLinkProperty: Option[SmartIri],
+ outbound: Boolean,
+ limit: Int)
+
+@if(triplestore.startsWith("graphdb")) {
+ @{
+ queries.sparql.v2.txt.getGraphDataGraphDB(startNodeIri = startNodeIri,
+ startNodeOnly = startNodeOnly,
+ maybeExcludeLinkProperty = maybeExcludeLinkProperty,
+ outbound = outbound,
+ limit = limit)
+ }
+} else {
+ @{
+ queries.sparql.v2.txt.getGraphDataStandard(triplestore = triplestore,
+ startNodeIri = startNodeIri,
+ startNodeOnly = startNodeOnly,
+ maybeExcludeLinkProperty = maybeExcludeLinkProperty,
+ outbound = outbound,
+ limit = limit)
+ }
+}
diff --git a/webapi/src/main/twirl/queries/sparql/v2/getGraphDataGraphDB.scala.txt b/webapi/src/main/twirl/queries/sparql/v2/getGraphDataGraphDB.scala.txt
new file mode 100644
index 0000000000..84ed7555f4
--- /dev/null
+++ b/webapi/src/main/twirl/queries/sparql/v2/getGraphDataGraphDB.scala.txt
@@ -0,0 +1,133 @@
+@*
+ * Copyright © 2015-2018 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 .
+ *@
+
+@import org.knora.webapi._
+@import org.knora.webapi.util.SmartIri
+
+@**
+ * Gets the outbound or inbound links from/to a resource, using GraphDB. This query is used recursively to get a graph
+ * of resources reachable from a given resource.
+ *
+ * This template is used only by getGraphData.scala.txt.
+ *
+ * Since the triplestore type is GraphDB, we assume that inference is enabled, and we use it to optimise the generated
+ * SPARQL. Specifically, we use inference to find subproperties of knora-base:hasLinkTo and knora-base:isPartOf.
+ * This requires us to use GraphDB's GRAPH whenever we need to get explicit
+ * (non-inferred) statements.
+ *
+ * @param startNodeIri the IRI of the resource to use as the starting point of the query.
+ * @param startNodeOnly if true, returns information only about the start node.
+ * @param maybeExcludeLinkProperty if provided, a link property that should be excluded from the results.
+ * @param outbound true to get outbound links, false to get inbound links.
+ * @param limit the maximum number of edges to return.
+ *@
+@(startNodeIri: IRI,
+ startNodeOnly: Boolean,
+ maybeExcludeLinkProperty: Option[SmartIri],
+ outbound: Boolean,
+ limit: Int)
+
+PREFIX rdf:
+PREFIX rdfs:
+PREFIX knora-base:
+
+SELECT ?node ?nodeClass ?nodeLabel ?nodeCreator ?nodeProject ?nodePermissions
+ ?linkValue ?linkProp ?linkValueCreator ?linkValuePermissions
+WHERE {
+ @if(startNodeOnly) {
+ BIND(IRI("@startNodeIri") AS ?node) .
+
+ GRAPH {
+ ?node a ?nodeClass .
+ }
+
+ ?node rdfs:label ?nodeLabel ;
+ knora-base:attachedToUser ?nodeCreator ;
+ knora-base:attachedToProject ?nodeProject ;
+ knora-base:isDeleted false ;
+ knora-base:hasPermissions ?nodePermissions .
+ } else {
+ BIND(IRI("@startNodeIri") AS ?startNode) .
+
+ @if(outbound) {
+ ?startNode knora-base:hasLinkTo ?node .
+
+ @maybeExcludeLinkProperty match {
+ case Some(excludeLinkProperty) => {
+
+ FILTER NOT EXISTS {
+ ?startNode <@excludeLinkProperty> ?node .
+ }
+
+ }
+
+ case None => {}
+ }
+
+ GRAPH {
+ ?startNode ?linkProp ?node .
+ }
+
+ ?node knora-base:isDeleted false .
+
+ ?linkValue a knora-base:LinkValue ;
+ rdf:subject ?startNode ;
+ rdf:predicate ?linkProp ;
+ rdf:object ?node .
+ } else {
+ ?node knora-base:hasLinkTo ?startNode .
+
+ @maybeExcludeLinkProperty match {
+ case Some(excludeLinkProperty) => {
+
+ FILTER NOT EXISTS {
+ ?node <@excludeLinkProperty> ?startNode .
+ }
+
+ }
+
+ case None => {}
+ }
+
+ GRAPH {
+ ?node ?linkProp ?startNode .
+ }
+
+ ?node knora-base:isDeleted false .
+
+ ?linkValue a knora-base:LinkValue ;
+ rdf:subject ?node ;
+ rdf:predicate ?linkProp ;
+ rdf:object ?startNode .
+ }
+
+ GRAPH {
+ ?node a ?nodeClass .
+ }
+
+ ?node rdfs:label ?nodeLabel ;
+ knora-base:attachedToUser ?nodeCreator ;
+ knora-base:attachedToProject ?nodeProject ;
+ knora-base:hasPermissions ?nodePermissions .
+
+ ?linkValue knora-base:attachedToUser ?linkValueCreator ;
+ knora-base:hasPermissions ?linkValuePermissions .
+ }
+}
+LIMIT @limit
\ No newline at end of file
diff --git a/webapi/src/main/twirl/queries/sparql/v2/getGraphDataStandard.scala.txt b/webapi/src/main/twirl/queries/sparql/v2/getGraphDataStandard.scala.txt
new file mode 100644
index 0000000000..ab91316c32
--- /dev/null
+++ b/webapi/src/main/twirl/queries/sparql/v2/getGraphDataStandard.scala.txt
@@ -0,0 +1,120 @@
+@*
+ * Copyright © 2015-2018 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 .
+ *@
+
+@import org.knora.webapi._
+@import org.knora.webapi.util.SmartIri
+
+@**
+ * Gets the outbound or inbound links from/to a resource, using standard SPARQL, without inference. This query is used
+ * recursively to get a graph of resources reachable from a given resource.
+ *
+ * This template is used only by getGraphData.scala.txt.
+ *
+ * @param triplestore the name of the triplestore being used.
+ * @param startNodeIri the IRI of the resource to use as the starting point of the query.
+ * @param startNodeOnly if true, returns information only about the start node.
+ * @param maybeExcludeLinkProperty if provided, a link property that should be excluded from the results.
+ * @param outbound true to get outbound links, false to get inbound links.
+ * @param limit the maximum number of edges to return.
+ *@
+@(triplestore: String,
+ startNodeIri: IRI,
+ startNodeOnly: Boolean,
+ maybeExcludeLinkProperty: Option[SmartIri],
+ outbound: Boolean,
+ limit: Int)
+
+PREFIX rdf:
+PREFIX rdfs:
+PREFIX knora-base:
+
+SELECT ?node ?nodeClass ?nodeLabel ?nodeCreator ?nodeProject ?nodePermissions
+ ?linkValue ?linkProp ?linkValueCreator ?linkValuePermissions
+WHERE {
+ @if(startNodeOnly) {
+ BIND(IRI("@startNodeIri") AS ?node) .
+
+ ?node a ?nodeClass ;
+ rdfs:label ?nodeLabel ;
+ knora-base:attachedToUser ?nodeCreator ;
+ knora-base:attachedToProject ?nodeProject ;
+ knora-base:isDeleted false ;
+ knora-base:hasPermissions ?nodePermissions .
+ } else {
+ BIND(IRI("@startNodeIri") AS ?startNode) .
+
+ ?linkProp rdfs:subPropertyOf* knora-base:hasLinkTo .
+
+ @if(outbound) {
+ ?startNode ?linkProp ?node .
+
+ @maybeExcludeLinkProperty match {
+ case Some(excludeLinkProperty) => {
+
+ FILTER NOT EXISTS {
+ ?excludedProp rdfs:subPropertyOf* <@excludeLinkProperty> .
+ ?startNode ?excludedProp ?node .
+ }
+
+ }
+
+ case None => {}
+ }
+
+ ?node knora-base:isDeleted false .
+
+ ?linkValue a knora-base:LinkValue ;
+ rdf:subject ?startNode ;
+ rdf:predicate ?linkProp ;
+ rdf:object ?node .
+ } else {
+ ?node ?linkProp ?startNode .
+
+ @maybeExcludeLinkProperty match {
+ case Some(excludeLinkProperty) => {
+
+ FILTER NOT EXISTS {
+ ?excludedProp rdfs:subPropertyOf* <@excludeLinkProperty> .
+ ?node ?excludedProp ?startNode .
+ }
+
+ }
+
+ case None => {}
+ }
+
+ ?node knora-base:isDeleted false .
+
+ ?linkValue a knora-base:LinkValue ;
+ rdf:subject ?node ;
+ rdf:predicate ?linkProp ;
+ rdf:object ?startNode .
+ }
+
+ ?node a ?nodeClass ;
+ rdfs:label ?nodeLabel ;
+ knora-base:attachedToUser ?nodeCreator ;
+ knora-base:attachedToProject ?nodeProject ;
+ knora-base:hasPermissions ?nodePermissions .
+
+ ?linkValue knora-base:attachedToUser ?linkValueCreator ;
+ knora-base:hasPermissions ?linkValuePermissions .
+ }
+}
+LIMIT @limit
\ No newline at end of file
diff --git a/webapi/src/test/resources/test-data/resourcesR2RV2/ThingGraphBoth.jsonld b/webapi/src/test/resources/test-data/resourcesR2RV2/ThingGraphBoth.jsonld
new file mode 100644
index 0000000000..05907f675f
--- /dev/null
+++ b/webapi/src/test/resources/test-data/resourcesR2RV2/ThingGraphBoth.jsonld
@@ -0,0 +1,129 @@
+{
+ "@graph" : [ {
+ "@id" : "http://rdfh.ch/0001/0C-0L1kORryKzJAJxxRyRQ",
+ "@type" : "anything:Thing",
+ "anything:isPartOfOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/LOV-6aLYQFW15jwdyS51Yw"
+ },
+ "rdfs:label" : "Sierra"
+ }, {
+ "@id" : "http://rdfh.ch/0001/9eSuQ_J7T0aOqoImTkwPxA",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/YgL4JhwfSQq-1davjrp8Ow"
+ },
+ "rdfs:label" : "excluded Bravo"
+ }, {
+ "@id" : "http://rdfh.ch/0001/A67ka6UQRHWf313tbhQBjw",
+ "@type" : "anything:Thing",
+ "rdfs:label" : "Victor"
+ }, {
+ "@id" : "http://rdfh.ch/0001/L5xU7Qe5QUu6Wz3cDaCxbA",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/iqW_PBiHRdyTFzik8tuSog"
+ },
+ "rdfs:label" : "Papa"
+ }, {
+ "@id" : "http://rdfh.ch/0001/LOV-6aLYQFW15jwdyS51Yw",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/MiBwAFcxQZGHNL-WfgFAPQ"
+ },
+ "rdfs:label" : "Uniform"
+ }, {
+ "@id" : "http://rdfh.ch/0001/Lz7WEqJETJqqsUZQYexBQg",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : [ {
+ "@id" : "http://rdfh.ch/0001/sHCLAGg-R5qJ6oPZPV-zOQ"
+ }, {
+ "@id" : "http://rdfh.ch/0001/start"
+ } ],
+ "rdfs:label" : "Foxtrot"
+ }, {
+ "@id" : "http://rdfh.ch/0001/MiBwAFcxQZGHNL-WfgFAPQ",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/nResNuvARcWYUdWyo0GWGw"
+ },
+ "rdfs:label" : "Whiskey"
+ }, {
+ "@id" : "http://rdfh.ch/0001/WLSHxQUgTOmG1T0lBU2r5w",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/A67ka6UQRHWf313tbhQBjw"
+ },
+ "anything:isPartOfOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/nVh9YJ1ZStSmCHb9hIdwWw"
+ },
+ "rdfs:label" : "Tango"
+ }, {
+ "@id" : "http://rdfh.ch/0001/YgL4JhwfSQq-1davjrp8Ow",
+ "@type" : "anything:Thing",
+ "rdfs:label" : "November"
+ }, {
+ "@id" : "http://rdfh.ch/0001/ZYNUiHtsTROWblui9xHEuw",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/cL5AwEioRLOm6Vrqwl1RmQ"
+ },
+ "rdfs:label" : "excluded Alpha"
+ }, {
+ "@id" : "http://rdfh.ch/0001/cL5AwEioRLOm6Vrqwl1RmQ",
+ "@type" : "anything:Thing",
+ "rdfs:label" : "Oscar"
+ }, {
+ "@id" : "http://rdfh.ch/0001/iqW_PBiHRdyTFzik8tuSog",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/start"
+ },
+ "rdfs:label" : "Quebec"
+ }, {
+ "@id" : "http://rdfh.ch/0001/nResNuvARcWYUdWyo0GWGw",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/tPfZeNMvRVujCQqbIbvO0A"
+ },
+ "rdfs:label" : "X-ray"
+ }, {
+ "@id" : "http://rdfh.ch/0001/nVh9YJ1ZStSmCHb9hIdwWw",
+ "@type" : "anything:Thing",
+ "rdfs:label" : "excluded Charlie"
+ }, {
+ "@id" : "http://rdfh.ch/0001/sHCLAGg-R5qJ6oPZPV-zOQ",
+ "@type" : "anything:Thing",
+ "rdfs:label" : "Golf"
+ }, {
+ "@id" : "http://rdfh.ch/0001/start",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : [ {
+ "@id" : "http://rdfh.ch/0001/0C-0L1kORryKzJAJxxRyRQ"
+ }, {
+ "@id" : "http://rdfh.ch/0001/WLSHxQUgTOmG1T0lBU2r5w"
+ }, {
+ "@id" : "http://rdfh.ch/0001/tPfZeNMvRVujCQqbIbvO0A"
+ } ],
+ "anything:isPartOfOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/9eSuQ_J7T0aOqoImTkwPxA"
+ },
+ "rdfs:label" : "Romeo"
+ }, {
+ "@id" : "http://rdfh.ch/0001/tPfZeNMvRVujCQqbIbvO0A",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/Lz7WEqJETJqqsUZQYexBQg"
+ },
+ "anything:isPartOfOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/ZYNUiHtsTROWblui9xHEuw"
+ },
+ "rdfs:label" : "Echo"
+ } ],
+ "@context" : {
+ "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+ "knora-api" : "http://api.knora.org/ontology/knora-api/v2#",
+ "rdfs" : "http://www.w3.org/2000/01/rdf-schema#",
+ "xsd" : "http://www.w3.org/2001/XMLSchema#",
+ "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#"
+ }
+}
\ No newline at end of file
diff --git a/webapi/src/test/resources/test-data/resourcesR2RV2/ThingGraphBothWithDepth.jsonld b/webapi/src/test/resources/test-data/resourcesR2RV2/ThingGraphBothWithDepth.jsonld
new file mode 100644
index 0000000000..4f0e2f174a
--- /dev/null
+++ b/webapi/src/test/resources/test-data/resourcesR2RV2/ThingGraphBothWithDepth.jsonld
@@ -0,0 +1,99 @@
+{
+ "@graph" : [ {
+ "@id" : "http://rdfh.ch/0001/0C-0L1kORryKzJAJxxRyRQ",
+ "@type" : "anything:Thing",
+ "anything:isPartOfOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/LOV-6aLYQFW15jwdyS51Yw"
+ },
+ "rdfs:label" : "Sierra"
+ }, {
+ "@id" : "http://rdfh.ch/0001/9eSuQ_J7T0aOqoImTkwPxA",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/YgL4JhwfSQq-1davjrp8Ow"
+ },
+ "rdfs:label" : "excluded Bravo"
+ }, {
+ "@id" : "http://rdfh.ch/0001/A67ka6UQRHWf313tbhQBjw",
+ "@type" : "anything:Thing",
+ "rdfs:label" : "Victor"
+ }, {
+ "@id" : "http://rdfh.ch/0001/L5xU7Qe5QUu6Wz3cDaCxbA",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/iqW_PBiHRdyTFzik8tuSog"
+ },
+ "rdfs:label" : "Papa"
+ }, {
+ "@id" : "http://rdfh.ch/0001/LOV-6aLYQFW15jwdyS51Yw",
+ "@type" : "anything:Thing",
+ "rdfs:label" : "Uniform"
+ }, {
+ "@id" : "http://rdfh.ch/0001/Lz7WEqJETJqqsUZQYexBQg",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/start"
+ },
+ "rdfs:label" : "Foxtrot"
+ }, {
+ "@id" : "http://rdfh.ch/0001/WLSHxQUgTOmG1T0lBU2r5w",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/A67ka6UQRHWf313tbhQBjw"
+ },
+ "anything:isPartOfOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/nVh9YJ1ZStSmCHb9hIdwWw"
+ },
+ "rdfs:label" : "Tango"
+ }, {
+ "@id" : "http://rdfh.ch/0001/YgL4JhwfSQq-1davjrp8Ow",
+ "@type" : "anything:Thing",
+ "rdfs:label" : "November"
+ }, {
+ "@id" : "http://rdfh.ch/0001/ZYNUiHtsTROWblui9xHEuw",
+ "@type" : "anything:Thing",
+ "rdfs:label" : "excluded Alpha"
+ }, {
+ "@id" : "http://rdfh.ch/0001/iqW_PBiHRdyTFzik8tuSog",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/start"
+ },
+ "rdfs:label" : "Quebec"
+ }, {
+ "@id" : "http://rdfh.ch/0001/nVh9YJ1ZStSmCHb9hIdwWw",
+ "@type" : "anything:Thing",
+ "rdfs:label" : "excluded Charlie"
+ }, {
+ "@id" : "http://rdfh.ch/0001/start",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : [ {
+ "@id" : "http://rdfh.ch/0001/0C-0L1kORryKzJAJxxRyRQ"
+ }, {
+ "@id" : "http://rdfh.ch/0001/WLSHxQUgTOmG1T0lBU2r5w"
+ }, {
+ "@id" : "http://rdfh.ch/0001/tPfZeNMvRVujCQqbIbvO0A"
+ } ],
+ "anything:isPartOfOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/9eSuQ_J7T0aOqoImTkwPxA"
+ },
+ "rdfs:label" : "Romeo"
+ }, {
+ "@id" : "http://rdfh.ch/0001/tPfZeNMvRVujCQqbIbvO0A",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/Lz7WEqJETJqqsUZQYexBQg"
+ },
+ "anything:isPartOfOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/ZYNUiHtsTROWblui9xHEuw"
+ },
+ "rdfs:label" : "Echo"
+ } ],
+ "@context" : {
+ "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+ "knora-api" : "http://api.knora.org/ontology/knora-api/v2#",
+ "rdfs" : "http://www.w3.org/2000/01/rdf-schema#",
+ "xsd" : "http://www.w3.org/2001/XMLSchema#",
+ "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#"
+ }
+}
\ No newline at end of file
diff --git a/webapi/src/test/resources/test-data/resourcesR2RV2/ThingGraphBothWithExcludedProp.jsonld b/webapi/src/test/resources/test-data/resourcesR2RV2/ThingGraphBothWithExcludedProp.jsonld
new file mode 100644
index 0000000000..cf7f8ae4eb
--- /dev/null
+++ b/webapi/src/test/resources/test-data/resourcesR2RV2/ThingGraphBothWithExcludedProp.jsonld
@@ -0,0 +1,84 @@
+{
+ "@graph" : [ {
+ "@id" : "http://rdfh.ch/0001/0C-0L1kORryKzJAJxxRyRQ",
+ "@type" : "anything:Thing",
+ "rdfs:label" : "Sierra"
+ }, {
+ "@id" : "http://rdfh.ch/0001/A67ka6UQRHWf313tbhQBjw",
+ "@type" : "anything:Thing",
+ "rdfs:label" : "Victor"
+ }, {
+ "@id" : "http://rdfh.ch/0001/L5xU7Qe5QUu6Wz3cDaCxbA",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/iqW_PBiHRdyTFzik8tuSog"
+ },
+ "rdfs:label" : "Papa"
+ }, {
+ "@id" : "http://rdfh.ch/0001/Lz7WEqJETJqqsUZQYexBQg",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : [ {
+ "@id" : "http://rdfh.ch/0001/sHCLAGg-R5qJ6oPZPV-zOQ"
+ }, {
+ "@id" : "http://rdfh.ch/0001/start"
+ } ],
+ "rdfs:label" : "Foxtrot"
+ }, {
+ "@id" : "http://rdfh.ch/0001/MiBwAFcxQZGHNL-WfgFAPQ",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/nResNuvARcWYUdWyo0GWGw"
+ },
+ "rdfs:label" : "Whiskey"
+ }, {
+ "@id" : "http://rdfh.ch/0001/WLSHxQUgTOmG1T0lBU2r5w",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/A67ka6UQRHWf313tbhQBjw"
+ },
+ "rdfs:label" : "Tango"
+ }, {
+ "@id" : "http://rdfh.ch/0001/iqW_PBiHRdyTFzik8tuSog",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/start"
+ },
+ "rdfs:label" : "Quebec"
+ }, {
+ "@id" : "http://rdfh.ch/0001/nResNuvARcWYUdWyo0GWGw",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/tPfZeNMvRVujCQqbIbvO0A"
+ },
+ "rdfs:label" : "X-ray"
+ }, {
+ "@id" : "http://rdfh.ch/0001/sHCLAGg-R5qJ6oPZPV-zOQ",
+ "@type" : "anything:Thing",
+ "rdfs:label" : "Golf"
+ }, {
+ "@id" : "http://rdfh.ch/0001/start",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : [ {
+ "@id" : "http://rdfh.ch/0001/0C-0L1kORryKzJAJxxRyRQ"
+ }, {
+ "@id" : "http://rdfh.ch/0001/WLSHxQUgTOmG1T0lBU2r5w"
+ }, {
+ "@id" : "http://rdfh.ch/0001/tPfZeNMvRVujCQqbIbvO0A"
+ } ],
+ "rdfs:label" : "Romeo"
+ }, {
+ "@id" : "http://rdfh.ch/0001/tPfZeNMvRVujCQqbIbvO0A",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/Lz7WEqJETJqqsUZQYexBQg"
+ },
+ "rdfs:label" : "Echo"
+ } ],
+ "@context" : {
+ "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+ "knora-api" : "http://api.knora.org/ontology/knora-api/v2#",
+ "rdfs" : "http://www.w3.org/2000/01/rdf-schema#",
+ "xsd" : "http://www.w3.org/2001/XMLSchema#",
+ "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#"
+ }
+}
\ No newline at end of file
diff --git a/webapi/src/test/resources/test-data/resourcesR2RV2/ThingGraphInbound.jsonld b/webapi/src/test/resources/test-data/resourcesR2RV2/ThingGraphInbound.jsonld
new file mode 100644
index 0000000000..ceea1a3ed2
--- /dev/null
+++ b/webapi/src/test/resources/test-data/resourcesR2RV2/ThingGraphInbound.jsonld
@@ -0,0 +1,59 @@
+{
+ "@graph" : [ {
+ "@id" : "http://rdfh.ch/0001/L5xU7Qe5QUu6Wz3cDaCxbA",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/iqW_PBiHRdyTFzik8tuSog"
+ },
+ "rdfs:label" : "Papa"
+ }, {
+ "@id" : "http://rdfh.ch/0001/Lz7WEqJETJqqsUZQYexBQg",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/start"
+ },
+ "rdfs:label" : "Foxtrot"
+ }, {
+ "@id" : "http://rdfh.ch/0001/MiBwAFcxQZGHNL-WfgFAPQ",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/nResNuvARcWYUdWyo0GWGw"
+ },
+ "rdfs:label" : "Whiskey"
+ }, {
+ "@id" : "http://rdfh.ch/0001/iqW_PBiHRdyTFzik8tuSog",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/start"
+ },
+ "rdfs:label" : "Quebec"
+ }, {
+ "@id" : "http://rdfh.ch/0001/nResNuvARcWYUdWyo0GWGw",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/tPfZeNMvRVujCQqbIbvO0A"
+ },
+ "rdfs:label" : "X-ray"
+ }, {
+ "@id" : "http://rdfh.ch/0001/start",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/tPfZeNMvRVujCQqbIbvO0A"
+ },
+ "rdfs:label" : "Romeo"
+ }, {
+ "@id" : "http://rdfh.ch/0001/tPfZeNMvRVujCQqbIbvO0A",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/Lz7WEqJETJqqsUZQYexBQg"
+ },
+ "rdfs:label" : "Echo"
+ } ],
+ "@context" : {
+ "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+ "knora-api" : "http://api.knora.org/ontology/knora-api/v2#",
+ "rdfs" : "http://www.w3.org/2000/01/rdf-schema#",
+ "xsd" : "http://www.w3.org/2001/XMLSchema#",
+ "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#"
+ }
+}
\ No newline at end of file
diff --git a/webapi/src/test/resources/test-data/resourcesR2RV2/ThingGraphOutbound.jsonld b/webapi/src/test/resources/test-data/resourcesR2RV2/ThingGraphOutbound.jsonld
new file mode 100644
index 0000000000..9a7fc3ed7b
--- /dev/null
+++ b/webapi/src/test/resources/test-data/resourcesR2RV2/ThingGraphOutbound.jsonld
@@ -0,0 +1,112 @@
+{
+ "@graph" : [ {
+ "@id" : "http://rdfh.ch/0001/0C-0L1kORryKzJAJxxRyRQ",
+ "@type" : "anything:Thing",
+ "anything:isPartOfOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/LOV-6aLYQFW15jwdyS51Yw"
+ },
+ "rdfs:label" : "Sierra"
+ }, {
+ "@id" : "http://rdfh.ch/0001/9eSuQ_J7T0aOqoImTkwPxA",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/YgL4JhwfSQq-1davjrp8Ow"
+ },
+ "rdfs:label" : "excluded Bravo"
+ }, {
+ "@id" : "http://rdfh.ch/0001/A67ka6UQRHWf313tbhQBjw",
+ "@type" : "anything:Thing",
+ "rdfs:label" : "Victor"
+ }, {
+ "@id" : "http://rdfh.ch/0001/LOV-6aLYQFW15jwdyS51Yw",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/MiBwAFcxQZGHNL-WfgFAPQ"
+ },
+ "rdfs:label" : "Uniform"
+ }, {
+ "@id" : "http://rdfh.ch/0001/Lz7WEqJETJqqsUZQYexBQg",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : [ {
+ "@id" : "http://rdfh.ch/0001/sHCLAGg-R5qJ6oPZPV-zOQ"
+ }, {
+ "@id" : "http://rdfh.ch/0001/start"
+ } ],
+ "rdfs:label" : "Foxtrot"
+ }, {
+ "@id" : "http://rdfh.ch/0001/MiBwAFcxQZGHNL-WfgFAPQ",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/nResNuvARcWYUdWyo0GWGw"
+ },
+ "rdfs:label" : "Whiskey"
+ }, {
+ "@id" : "http://rdfh.ch/0001/WLSHxQUgTOmG1T0lBU2r5w",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/A67ka6UQRHWf313tbhQBjw"
+ },
+ "anything:isPartOfOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/nVh9YJ1ZStSmCHb9hIdwWw"
+ },
+ "rdfs:label" : "Tango"
+ }, {
+ "@id" : "http://rdfh.ch/0001/YgL4JhwfSQq-1davjrp8Ow",
+ "@type" : "anything:Thing",
+ "rdfs:label" : "November"
+ }, {
+ "@id" : "http://rdfh.ch/0001/ZYNUiHtsTROWblui9xHEuw",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/cL5AwEioRLOm6Vrqwl1RmQ"
+ },
+ "rdfs:label" : "excluded Alpha"
+ }, {
+ "@id" : "http://rdfh.ch/0001/cL5AwEioRLOm6Vrqwl1RmQ",
+ "@type" : "anything:Thing",
+ "rdfs:label" : "Oscar"
+ }, {
+ "@id" : "http://rdfh.ch/0001/nResNuvARcWYUdWyo0GWGw",
+ "@type" : "anything:Thing",
+ "rdfs:label" : "X-ray"
+ }, {
+ "@id" : "http://rdfh.ch/0001/nVh9YJ1ZStSmCHb9hIdwWw",
+ "@type" : "anything:Thing",
+ "rdfs:label" : "excluded Charlie"
+ }, {
+ "@id" : "http://rdfh.ch/0001/sHCLAGg-R5qJ6oPZPV-zOQ",
+ "@type" : "anything:Thing",
+ "rdfs:label" : "Golf"
+ }, {
+ "@id" : "http://rdfh.ch/0001/start",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : [ {
+ "@id" : "http://rdfh.ch/0001/0C-0L1kORryKzJAJxxRyRQ"
+ }, {
+ "@id" : "http://rdfh.ch/0001/WLSHxQUgTOmG1T0lBU2r5w"
+ }, {
+ "@id" : "http://rdfh.ch/0001/tPfZeNMvRVujCQqbIbvO0A"
+ } ],
+ "anything:isPartOfOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/9eSuQ_J7T0aOqoImTkwPxA"
+ },
+ "rdfs:label" : "Romeo"
+ }, {
+ "@id" : "http://rdfh.ch/0001/tPfZeNMvRVujCQqbIbvO0A",
+ "@type" : "anything:Thing",
+ "anything:hasOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/Lz7WEqJETJqqsUZQYexBQg"
+ },
+ "anything:isPartOfOtherThing" : {
+ "@id" : "http://rdfh.ch/0001/ZYNUiHtsTROWblui9xHEuw"
+ },
+ "rdfs:label" : "Echo"
+ } ],
+ "@context" : {
+ "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+ "knora-api" : "http://api.knora.org/ontology/knora-api/v2#",
+ "rdfs" : "http://www.w3.org/2000/01/rdf-schema#",
+ "xsd" : "http://www.w3.org/2001/XMLSchema#",
+ "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#"
+ }
+}
\ No newline at end of file
diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala
index 0c65a3c176..1622649fe5 100644
--- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala
+++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala
@@ -26,12 +26,13 @@ import akka.actor.ActorSystem
import akka.http.scaladsl.model.headers.{Accept, BasicHttpCredentials}
import akka.http.scaladsl.model.{HttpEntity, HttpResponse, MediaRange, StatusCodes}
import akka.http.scaladsl.testkit.RouteTestTimeout
+import com.typesafe.config.{Config, ConfigFactory}
import org.knora.webapi._
import org.knora.webapi.e2e.v2.ResponseCheckerR2RV2.compareJSONLDForResourcesResponse
import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject
import org.knora.webapi.routing.RouteUtilV2
import org.knora.webapi.util.IriConversions._
-import org.knora.webapi.util.jsonld.{JsonLDConstants, JsonLDDocument}
+import org.knora.webapi.util.jsonld.{JsonLDConstants, JsonLDDocument, JsonLDUtil}
import org.knora.webapi.util.{FileUtil, StringFormatter}
import scala.concurrent.ExecutionContextExecutor
@@ -39,7 +40,7 @@ import scala.concurrent.ExecutionContextExecutor
/**
* Tests the API v2 resources route.
*/
-class ResourcesRouteV2E2ESpec extends E2ESpec {
+class ResourcesRouteV2E2ESpec extends E2ESpec(ResourcesRouteV2E2ESpec.config) {
private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance
implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout(settings.defaultTimeout)
@@ -233,6 +234,99 @@ class ResourcesRouteV2E2ESpec extends E2ESpec {
compareJSONLDForResourcesResponse(expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = responseAsString)
}
+ "return a graph of resources reachable via links from/to a given resource" in {
+ val request = Get(s"$baseApiUrl/v2/graph/${URLEncoder.encode("http://rdfh.ch/0001/start", "UTF-8")}?direction=both")
+ val response: HttpResponse = singleAwaitingRequest(request)
+ val responseAsString = responseToString(response)
+ assert(response.status == StatusCodes.OK, responseAsString)
+ val parsedReceivedJsonLD = JsonLDUtil.parseJsonLD(responseAsString)
+
+ val expectedAnswerJSONLD = readOrWriteTextFile(responseAsString, new File("src/test/resources/test-data/resourcesR2RV2/ThingGraphBoth.jsonld"), writeTestDataFiles)
+ val parsedExpectedJsonLD = JsonLDUtil.parseJsonLD(expectedAnswerJSONLD)
+
+ assert(parsedReceivedJsonLD == parsedExpectedJsonLD)
+ }
+
+ "return a graph of resources reachable via links from a given resource" in {
+ val request = Get(s"$baseApiUrl/v2/graph/${URLEncoder.encode("http://rdfh.ch/0001/start", "UTF-8")}?direction=outbound")
+ val response: HttpResponse = singleAwaitingRequest(request)
+ val responseAsString = responseToString(response)
+ assert(response.status == StatusCodes.OK, responseAsString)
+ val parsedReceivedJsonLD = JsonLDUtil.parseJsonLD(responseAsString)
+
+ val expectedAnswerJSONLD = readOrWriteTextFile(responseAsString, new File("src/test/resources/test-data/resourcesR2RV2/ThingGraphOutbound.jsonld"), writeTestDataFiles)
+ val parsedExpectedJsonLD = JsonLDUtil.parseJsonLD(expectedAnswerJSONLD)
+
+ assert(parsedReceivedJsonLD == parsedExpectedJsonLD)
+ }
+
+ "return a graph of resources reachable via links to a given resource" in {
+ val request = Get(s"$baseApiUrl/v2/graph/${URLEncoder.encode("http://rdfh.ch/0001/start", "UTF-8")}?direction=inbound")
+ val response: HttpResponse = singleAwaitingRequest(request)
+ val responseAsString = responseToString(response)
+ assert(response.status == StatusCodes.OK, responseAsString)
+ val parsedReceivedJsonLD = JsonLDUtil.parseJsonLD(responseAsString)
+
+ val expectedAnswerJSONLD = readOrWriteTextFile(responseAsString, new File("src/test/resources/test-data/resourcesR2RV2/ThingGraphInbound.jsonld"), writeTestDataFiles)
+ val parsedExpectedJsonLD = JsonLDUtil.parseJsonLD(expectedAnswerJSONLD)
+
+ assert(parsedReceivedJsonLD == parsedExpectedJsonLD)
+ }
+
+ "return a graph of resources reachable via links to/from a given resource, excluding a specified property" in {
+ val request = Get(s"$baseApiUrl/v2/graph/${URLEncoder.encode("http://rdfh.ch/0001/start", "UTF-8")}?direction=both&excludeProperty=${URLEncoder.encode("http://0.0.0.0:3333/ontology/0001/anything/v2#isPartOfOtherThing", "UTF-8")}")
+ val response: HttpResponse = singleAwaitingRequest(request)
+ val responseAsString = responseToString(response)
+ assert(response.status == StatusCodes.OK, responseAsString)
+ val parsedReceivedJsonLD = JsonLDUtil.parseJsonLD(responseAsString)
+
+ val expectedAnswerJSONLD = readOrWriteTextFile(responseAsString, new File("src/test/resources/test-data/resourcesR2RV2/ThingGraphBothWithExcludedProp.jsonld"), writeTestDataFiles)
+ val parsedExpectedJsonLD = JsonLDUtil.parseJsonLD(expectedAnswerJSONLD)
+
+ assert(parsedReceivedJsonLD == parsedExpectedJsonLD)
+ }
+
+ "return a graph of resources reachable via links from a given resource, specifying search depth" in {
+ val request = Get(s"$baseApiUrl/v2/graph/${URLEncoder.encode("http://rdfh.ch/0001/start", "UTF-8")}?direction=both&depth=2")
+ val response: HttpResponse = singleAwaitingRequest(request)
+ val responseAsString = responseToString(response)
+ assert(response.status == StatusCodes.OK, responseAsString)
+ val parsedReceivedJsonLD = JsonLDUtil.parseJsonLD(responseAsString)
+
+ val expectedAnswerJSONLD = readOrWriteTextFile(responseAsString, new File("src/test/resources/test-data/resourcesR2RV2/ThingGraphBothWithDepth.jsonld"), writeTestDataFiles)
+ val parsedExpectedJsonLD = JsonLDUtil.parseJsonLD(expectedAnswerJSONLD)
+
+ assert(parsedReceivedJsonLD == parsedExpectedJsonLD)
+ }
+
+ "not accept a graph request with an invalid direction" in {
+ val request = Get(s"$baseApiUrl/v2/graph/${URLEncoder.encode("http://rdfh.ch/0001/start", "UTF-8")}?direction=foo")
+ val response: HttpResponse = singleAwaitingRequest(request)
+ val responseAsString = responseToString(response)
+ assert(response.status == StatusCodes.BadRequest, responseAsString)
+ }
+
+ "not accept a graph request with an invalid depth (< 1)" in {
+ val request = Get(s"$baseApiUrl/v2/graph/${URLEncoder.encode("http://rdfh.ch/0001/start", "UTF-8")}?depth=0")
+ val response: HttpResponse = singleAwaitingRequest(request)
+ val responseAsString = responseToString(response)
+ assert(response.status == StatusCodes.BadRequest, responseAsString)
+ }
+
+ "not accept a graph request with an invalid depth (> max)" in {
+ val request = Get(s"$baseApiUrl/v2/graph/${URLEncoder.encode("http://rdfh.ch/0001/start", "UTF-8")}?depth=${settings.maxGraphBreadth + 1}")
+ val response: HttpResponse = singleAwaitingRequest(request)
+ val responseAsString = responseToString(response)
+ assert(response.status == StatusCodes.BadRequest, responseAsString)
+ }
+
+ "not accept a graph request with an invalid property to exclude" in {
+ val request = Get(s"$baseApiUrl/v2/graph/${URLEncoder.encode("http://rdfh.ch/0001/start", "UTF-8")}?excludeProperty=foo")
+ val response: HttpResponse = singleAwaitingRequest(request)
+ val responseAsString = responseToString(response)
+ assert(response.status == StatusCodes.BadRequest, responseAsString)
+ }
+
"create a resource with values" in {
val jsonLdEntity =
"""{
@@ -339,4 +433,12 @@ class ResourcesRouteV2E2ESpec extends E2ESpec {
assert(resourceIri.toSmartIri.isKnoraDataIri)
}
}
-}
\ No newline at end of file
+}
+
+object ResourcesRouteV2E2ESpec {
+ val config: Config = ConfigFactory.parseString(
+ """akka.loglevel = "DEBUG"
+ |akka.stdout-loglevel = "DEBUG"
+ |app.triplestore.profile-queries = false
+ """.stripMargin)
+}
diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala
index c66105253f..afe2921725 100644
--- a/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala
+++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala
@@ -25,7 +25,6 @@ import akka.testkit.{ImplicitSender, TestActorRef}
import org.knora.webapi._
import org.knora.webapi.messages.admin.responder.usersmessages.UserADM
import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject
-import org.knora.webapi.messages.v1.responder.valuemessages.{KnoraCalendarV1, KnoraPrecisionV1}
import org.knora.webapi.messages.v2.responder.resourcemessages._
import org.knora.webapi.messages.v2.responder.standoffmessages.{GetMappingRequestV2, GetMappingResponseV2, MappingXMLtoStandoff, StandoffDataTypeClasses}
import org.knora.webapi.messages.v2.responder.valuemessages._
@@ -79,6 +78,327 @@ object ResourcesResponderV2Spec {
)
}
+class GraphTestData {
+ private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance
+
+ val graphForAnythingUser1 = GraphDataGetResponseV2(
+ edges = Vector(
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/tPfZeNMvRVujCQqbIbvO0A",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/nResNuvARcWYUdWyo0GWGw"
+ ),
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/cmfk1DMHRBiR4-_6HXpEFA",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/5IEswyQFQp2bxXDrOyEfEA"
+ ),
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/7uuGcnFcQJq08dMOralyCQ",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/sHCLAGg-R5qJ6oPZPV-zOQ"
+ ),
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/nResNuvARcWYUdWyo0GWGw",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/MiBwAFcxQZGHNL-WfgFAPQ"
+ ),
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/0C-0L1kORryKzJAJxxRyRQ",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/start"
+ ),
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/iqW_PBiHRdyTFzik8tuSog",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/L5xU7Qe5QUu6Wz3cDaCxbA"
+ ),
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/start",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/Lz7WEqJETJqqsUZQYexBQg"
+ ),
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/sHCLAGg-R5qJ6oPZPV-zOQ",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/Lz7WEqJETJqqsUZQYexBQg"
+ ),
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/WLSHxQUgTOmG1T0lBU2r5w",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/start"
+ ),
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/A67ka6UQRHWf313tbhQBjw",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/WLSHxQUgTOmG1T0lBU2r5w"
+ ),
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/MiBwAFcxQZGHNL-WfgFAPQ",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/LOV-6aLYQFW15jwdyS51Yw"
+ ),
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/start",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/iqW_PBiHRdyTFzik8tuSog"
+ ),
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/Lz7WEqJETJqqsUZQYexBQg",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/tPfZeNMvRVujCQqbIbvO0A"
+ ),
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/tPfZeNMvRVujCQqbIbvO0A",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/start"
+ ),
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/L5xU7Qe5QUu6Wz3cDaCxbA",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/cmfk1DMHRBiR4-_6HXpEFA"
+ )
+ ),
+ nodes = Vector(
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Tango",
+ resourceIri = "http://rdfh.ch/0001/WLSHxQUgTOmG1T0lBU2r5w"
+ ),
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Foxtrot",
+ resourceIri = "http://rdfh.ch/0001/Lz7WEqJETJqqsUZQYexBQg"
+ ),
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Echo",
+ resourceIri = "http://rdfh.ch/0001/tPfZeNMvRVujCQqbIbvO0A"
+ ),
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Golf",
+ resourceIri = "http://rdfh.ch/0001/sHCLAGg-R5qJ6oPZPV-zOQ"
+ ),
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Whiskey",
+ resourceIri = "http://rdfh.ch/0001/MiBwAFcxQZGHNL-WfgFAPQ"
+ ),
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Mike",
+ resourceIri = "http://rdfh.ch/0001/cmfk1DMHRBiR4-_6HXpEFA"
+ ),
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "X-ray",
+ resourceIri = "http://rdfh.ch/0001/nResNuvARcWYUdWyo0GWGw"
+ ),
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Uniform",
+ resourceIri = "http://rdfh.ch/0001/LOV-6aLYQFW15jwdyS51Yw"
+ ),
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Sierra",
+ resourceIri = "http://rdfh.ch/0001/0C-0L1kORryKzJAJxxRyRQ"
+ ),
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Romeo",
+ resourceIri = "http://rdfh.ch/0001/start"
+ ),
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Quebec",
+ resourceIri = "http://rdfh.ch/0001/iqW_PBiHRdyTFzik8tuSog"
+ ),
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Hotel",
+ resourceIri = "http://rdfh.ch/0001/7uuGcnFcQJq08dMOralyCQ"
+ ),
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Papa",
+ resourceIri = "http://rdfh.ch/0001/L5xU7Qe5QUu6Wz3cDaCxbA"
+ ),
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Victor",
+ resourceIri = "http://rdfh.ch/0001/A67ka6UQRHWf313tbhQBjw"
+ ),
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Delta",
+ resourceIri = "http://rdfh.ch/0001/5IEswyQFQp2bxXDrOyEfEA"
+ )
+ ),
+ ontologySchema = InternalSchema
+ )
+
+ val graphForIncunabulaUser = GraphDataGetResponseV2(
+ edges = Vector(
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/tPfZeNMvRVujCQqbIbvO0A",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/nResNuvARcWYUdWyo0GWGw"
+ ),
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/nResNuvARcWYUdWyo0GWGw",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/MiBwAFcxQZGHNL-WfgFAPQ"
+ ),
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/0C-0L1kORryKzJAJxxRyRQ",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/start"
+ ),
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/iqW_PBiHRdyTFzik8tuSog",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/L5xU7Qe5QUu6Wz3cDaCxbA"
+ ),
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/start",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/Lz7WEqJETJqqsUZQYexBQg"
+ ),
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/sHCLAGg-R5qJ6oPZPV-zOQ",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/Lz7WEqJETJqqsUZQYexBQg"
+ ),
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/WLSHxQUgTOmG1T0lBU2r5w",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/start"
+ ),
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/A67ka6UQRHWf313tbhQBjw",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/WLSHxQUgTOmG1T0lBU2r5w"
+ ),
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/MiBwAFcxQZGHNL-WfgFAPQ",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/LOV-6aLYQFW15jwdyS51Yw"
+ ),
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/start",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/iqW_PBiHRdyTFzik8tuSog"
+ ),
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/Lz7WEqJETJqqsUZQYexBQg",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/tPfZeNMvRVujCQqbIbvO0A"
+ ),
+ GraphEdgeV2(
+ target = "http://rdfh.ch/0001/tPfZeNMvRVujCQqbIbvO0A",
+ propertyIri = "http://www.knora.org/ontology/0001/anything#hasOtherThing".toSmartIri,
+ source = "http://rdfh.ch/0001/start"
+ )
+ ),
+ nodes = Vector(
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Tango",
+ resourceIri = "http://rdfh.ch/0001/WLSHxQUgTOmG1T0lBU2r5w"
+ ),
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Foxtrot",
+ resourceIri = "http://rdfh.ch/0001/Lz7WEqJETJqqsUZQYexBQg"
+ ),
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Echo",
+ resourceIri = "http://rdfh.ch/0001/tPfZeNMvRVujCQqbIbvO0A"
+ ),
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Golf",
+ resourceIri = "http://rdfh.ch/0001/sHCLAGg-R5qJ6oPZPV-zOQ"
+ ),
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Whiskey",
+ resourceIri = "http://rdfh.ch/0001/MiBwAFcxQZGHNL-WfgFAPQ"
+ ),
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "X-ray",
+ resourceIri = "http://rdfh.ch/0001/nResNuvARcWYUdWyo0GWGw"
+ ),
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Uniform",
+ resourceIri = "http://rdfh.ch/0001/LOV-6aLYQFW15jwdyS51Yw"
+ ),
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Sierra",
+ resourceIri = "http://rdfh.ch/0001/0C-0L1kORryKzJAJxxRyRQ"
+ ),
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Romeo",
+ resourceIri = "http://rdfh.ch/0001/start"
+ ),
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Quebec",
+ resourceIri = "http://rdfh.ch/0001/iqW_PBiHRdyTFzik8tuSog"
+ ),
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Papa",
+ resourceIri = "http://rdfh.ch/0001/L5xU7Qe5QUu6Wz3cDaCxbA"
+ ),
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Victor",
+ resourceIri = "http://rdfh.ch/0001/A67ka6UQRHWf313tbhQBjw"
+ )
+ ),
+ ontologySchema = InternalSchema
+ )
+
+ val graphWithStandoffLink = GraphDataGetResponseV2(
+ edges = Vector(GraphEdgeV2(
+ target = "http://rdfh.ch/0001/a-thing",
+ propertyIri = "http://www.knora.org/ontology/knora-base#hasStandoffLinkTo".toSmartIri,
+ source = "http://rdfh.ch/0001/a-thing-with-text-values"
+ )),
+ nodes = Vector(
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Ein Ding f\u00FCr jemanden, dem die Dinge gefallen",
+ resourceIri = "http://rdfh.ch/0001/a-thing-with-text-values"
+ ),
+ GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "A thing",
+ resourceIri = "http://rdfh.ch/0001/a-thing"
+ )
+ ),
+ ontologySchema = InternalSchema
+ )
+
+ val graphWithOneNode = GraphDataGetResponseV2(
+ edges = Nil,
+ nodes = Vector(GraphNodeV2(
+ resourceClassIri = "http://www.knora.org/ontology/0001/anything#Thing".toSmartIri,
+ resourceLabel = "Another thing",
+ resourceIri = "http://rdfh.ch/0001/another-thing"
+ )),
+ ontologySchema = InternalSchema
+ )
+}
+
/**
* Tests [[ResourcesResponderV2]].
*/
@@ -94,6 +414,8 @@ class ResourcesResponderV2Spec extends CoreSpec() with ImplicitSender {
private var standardMapping: Option[MappingXMLtoStandoff] = None
+ private val graphTestData = new GraphTestData
+
override lazy val rdfDataObjects = List(
RdfDataObject(path = "_test_data/all_data/incunabula-data.ttl", name = "http://www.knora.org/data/0803/incunabula"),
RdfDataObject(path = "_test_data/demo_data/images-demo-data.ttl", name = "http://www.knora.org/data/00FF/images"),
@@ -289,6 +611,72 @@ class ResourcesResponderV2Spec extends CoreSpec() with ImplicitSender {
}
+ "return a graph of resources reachable via links from/to a given resource" in {
+ actorUnderTest ! GraphDataGetRequestV2(
+ resourceIri = "http://rdfh.ch/0001/start",
+ depth = 6,
+ inbound = true,
+ outbound = true,
+ excludeProperty = Some(OntologyConstants.KnoraApiV2WithValueObjects.IsPartOf.toSmartIri),
+ requestingUser = SharedTestDataADM.anythingUser1
+ )
+
+ val response = expectMsgType[GraphDataGetResponseV2](timeout)
+ val edges = response.edges
+ val nodes = response.nodes
+
+ edges should contain theSameElementsAs graphTestData.graphForAnythingUser1.edges
+ nodes should contain theSameElementsAs graphTestData.graphForAnythingUser1.nodes
+ }
+
+ "return a graph of resources reachable via links from/to a given resource, filtering the results according to the user's permissions" in {
+ actorUnderTest ! GraphDataGetRequestV2(
+ resourceIri = "http://rdfh.ch/0001/start",
+ depth = 6,
+ inbound = true,
+ outbound = true,
+ excludeProperty = Some(OntologyConstants.KnoraApiV2WithValueObjects.IsPartOf.toSmartIri),
+ requestingUser = SharedTestDataADM.incunabulaProjectAdminUser
+ )
+
+ val response = expectMsgType[GraphDataGetResponseV2](timeout)
+ val edges = response.edges
+ val nodes = response.nodes
+
+ edges should contain theSameElementsAs graphTestData.graphForIncunabulaUser.edges
+ nodes should contain theSameElementsAs graphTestData.graphForIncunabulaUser.nodes
+ }
+
+ "return a graph containing a standoff link" in {
+ actorUnderTest ! GraphDataGetRequestV2(
+ resourceIri = "http://rdfh.ch/0001/a-thing",
+ depth = 4,
+ inbound = true,
+ outbound = true,
+ excludeProperty = Some(OntologyConstants.KnoraApiV2WithValueObjects.IsPartOf.toSmartIri),
+ requestingUser = SharedTestDataADM.anythingUser1
+ )
+
+ expectMsgPF(timeout) {
+ case response: GraphDataGetResponseV2 => response should ===(graphTestData.graphWithStandoffLink)
+ }
+ }
+
+ "return a graph containing just one node" in {
+ actorUnderTest ! GraphDataGetRequestV2(
+ resourceIri = "http://rdfh.ch/0001/another-thing",
+ depth = 4,
+ inbound = true,
+ outbound = true,
+ excludeProperty = Some(OntologyConstants.KnoraApiV2WithValueObjects.IsPartOf.toSmartIri),
+ requestingUser = SharedTestDataADM.anythingUser1
+ )
+
+ expectMsgPF(timeout) {
+ case response: GraphDataGetResponseV2 => response should ===(graphTestData.graphWithOneNode)
+ }
+ }
+
"create a resource with no values" in {
// Create the resource.
@@ -1003,5 +1391,6 @@ class ResourcesResponderV2Spec extends CoreSpec() with ImplicitSender {
}
}
+
}
}