Skip to content

Commit bfd3192

Browse files
author
Benjamin Geer
authored
feat(rdf-api): Add a general-purpose SHACL validation utility (DSP-930) (#1762)
1 parent 346873d commit bfd3192

31 files changed

Lines changed: 848 additions & 110 deletions

‎docs/05-internals/design/principles/rdf-api.md‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,12 @@ The API is in the package `org.knora.webapi.messages.util.rdf`. It includes:
4949
- `JsonLDUtil`, which provides specialised functionality for working
5050
with RDF in JSON-LD format, and for converting between RDF models
5151
and JSON-LD documents. `RdfFormatUtil` uses `JsonLDUtil` when appropriate.
52+
53+
- `ShaclValidator`, which validates RDF models using SHACL shapes.
5254

5355
To work with RDF models, start with `RdfFeatureFactory`, which returns instances
54-
of `RdfNodeFactory`, `RdfModelFactory`, and `RdfFormatUtil`, using feature toggle
55-
configuration. `JsonLDUtil` does not need a feature factory.
56+
of `RdfNodeFactory`, `RdfModelFactory`, `RdfFormatUtil`, and `ShaclValidator`,
57+
using feature toggle configuration. `JsonLDUtil` does not need a feature factory.
5658

5759
To iterate efficiently over the statements in an `RdfModel`, use its `iterator` method.
5860
An `RdfModel` cannot be modified while you are iterating over it.
@@ -85,6 +87,15 @@ an `RdfModel`. To do this, use the `RdfModel.asRepository` method, which
8587
returns an `RdfRepository` that can run `SELECT` queries.
8688

8789

90+
## SHACL validation
91+
92+
On startup, graphs of SHACL shapes are loaded from Turtle files in a directory specified
93+
by `app.shacl.shapes-dir` in `application.conf`, and in subdirectories of
94+
that directory. To validate the default graph of an `RdfModel` using a graph of
95+
SHACL shapes, call `ShaclValidator.validate`, specifying the relative path of the
96+
Turtle file containing the graph of shapes.
97+
98+
8899
## Implementations
89100

90101
- The Jena-based implementation, in package `org.knora.webapi.messages.util.rdf.jenaimpl`.

‎test_data/shacl/test/person.ttl‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
@prefix ex: <http://example.com/ns#> .
2+
@prefix sh: <http://www.w3.org/ns/shacl#> .
3+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
4+
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
5+
6+
ex:PersonShape a sh:NodeShape ;
7+
sh:targetClass foaf:Person ;
8+
sh:property ex:PersonShapeProperty .
9+
10+
ex:PersonShapeProperty sh:path foaf:age ;
11+
sh:datatype xsd:int ;
12+
sh:maxCount 1 ;
13+
sh:minCount 1 .

‎third_party/dependencies.bzl‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ def dependencies():
8484
"org.xmlunit:xmlunit-core:2.1.1",
8585

8686
# other
87-
"org.eclipse.rdf4j:rdf4j-runtime:3.0.0",
87+
"org.eclipse.rdf4j:rdf4j-runtime:3.4.4",
88+
"org.eclipse.rdf4j:rdf4j-client:3.4.4",
89+
"org.eclipse.rdf4j:rdf4j-shacl:3.4.4",
8890
"org.rogach:scallop_2.12:3.2.0",
8991
"com.google.gwt:gwt-servlet:2.8.0",
9092
"net.sf.saxon:Saxon-HE:9.9.0-2",

‎webapi/BUILD.bazel‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ scala_library(
212212
"//webapi/src/main/scala/org/knora/webapi/app",
213213
"//webapi/src/main/scala/org/knora/webapi/core",
214214
"//webapi/src/main/scala/org/knora/webapi/exceptions",
215+
"//webapi/src/main/scala/org/knora/webapi/feature",
215216
"//webapi/src/main/scala/org/knora/webapi/instrumentation",
216217
"//webapi/src/main/scala/org/knora/webapi/messages",
217218
"//webapi/src/main/scala/org/knora/webapi/routing",

‎webapi/src/it/scala/org/knora/webapi/ITKnoraFakeSpec.scala‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package org.knora.webapi
2121

2222
import akka.actor.ActorSystem
23+
import akka.event.LoggingAdapter
2324
import akka.http.scaladsl.Http
2425
import akka.http.scaladsl.client.RequestBuilding
2526
import akka.http.scaladsl.model.{HttpRequest, HttpResponse, StatusCodes}
@@ -33,6 +34,7 @@ import org.scalatest.wordspec.AnyWordSpecLike
3334
import org.scalatest.{BeforeAndAfterAll, Suite}
3435
import spray.json.{JsObject, _}
3536
import org.knora.webapi.messages.StringFormatter
37+
import org.knora.webapi.messages.util.rdf.RdfFeatureFactory
3638

3739
import scala.concurrent.duration.{Duration, _}
3840
import scala.concurrent.{Await, ExecutionContext}
@@ -63,8 +65,9 @@ class ITKnoraFakeSpec(_system: ActorSystem) extends Core with KnoraFakeCore with
6365

6466
/* Needs to be initialized before any responders */
6567
StringFormatter.initForTest()
68+
RdfFeatureFactory.init(settings)
6669

67-
val log = akka.event.Logging(system, this.getClass)
70+
val log: LoggingAdapter = akka.event.Logging(system, this.getClass)
6871

6972
protected val baseApiUrl: String = settings.internalKnoraApiBaseUrl
7073
protected val baseInternalSipiUrl: String = settings.internalSipiBaseUrl

‎webapi/src/it/scala/org/knora/webapi/ITKnoraLiveSpec.scala‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import org.knora.webapi.exceptions.AssertionException
3636
import org.knora.webapi.messages.StringFormatter
3737
import org.knora.webapi.messages.app.appmessages.{AppStart, AppStop, SetAllowReloadOverHTTPState}
3838
import org.knora.webapi.messages.store.triplestoremessages.{RdfDataObject, TriplestoreJsonProtocol}
39-
import org.knora.webapi.messages.util.rdf.{JsonLDDocument, JsonLDUtil}
39+
import org.knora.webapi.messages.util.rdf.{JsonLDDocument, JsonLDUtil, RdfFeatureFactory}
4040
import org.knora.webapi.settings._
4141
import org.knora.webapi.util.StartupUtils
4242
import org.scalatest.matchers.should.Matchers
@@ -75,6 +75,7 @@ class ITKnoraLiveSpec(_system: ActorSystem) extends Core with StartupUtils with
7575

7676
/* Needs to be initialized before any responders */
7777
StringFormatter.initForTest()
78+
RdfFeatureFactory.init(settings)
7879

7980
val log: LoggingAdapter = akka.event.Logging(system, this.getClass)
8081

‎webapi/src/main/resources/application.conf‎

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -264,35 +264,41 @@ akka-http-cors {
264264
}
265265

266266
app {
267-
feature-toggles {
268-
new-list-admin-routes {
269-
description = "Replace the old list admin routes with new ones."
270-
271-
available-versions = [ 1 ]
272-
default-version = 1
273-
enabled-by-default = no
274-
override-allowed = yes
275-
expiration-date = "2021-12-01T00:00:00Z"
276-
277-
developer-emails = [
278-
"Sepideh Alassi <sepideh.alassi@dasch.swiss>"
279-
"Benjamin Geer <benjamin.geer@dasch.swiss>"
280-
]
281-
}
282-
283-
jena-rdf-library {
284-
description = "Use the Jena API for RDF processing. If turned off, use the RDF4J API."
285-
286-
available-versions = [ 1 ]
287-
default-version = 1
288-
enabled-by-default = no
289-
override-allowed = yes
290-
291-
developer-emails = [
292-
"Benjamin Geer <benjamin.geer@dasch.swiss>"
293-
]
294-
}
295-
}
267+
feature-toggles {
268+
new-list-admin-routes {
269+
description = "Replace the old list admin routes with new ones."
270+
271+
available-versions = [ 1 ]
272+
default-version = 1
273+
enabled-by-default = no
274+
override-allowed = yes
275+
expiration-date = "2021-12-01T00:00:00Z"
276+
277+
developer-emails = [
278+
"Sepideh Alassi <sepideh.alassi@dasch.swiss>"
279+
"Benjamin Geer <benjamin.geer@dasch.swiss>"
280+
]
281+
}
282+
283+
jena-rdf-library {
284+
description = "Use the Jena API for RDF processing. If turned off, use the RDF4J API."
285+
286+
available-versions = [ 1 ]
287+
default-version = 1
288+
enabled-by-default = no
289+
override-allowed = yes
290+
291+
developer-emails = [
292+
"Benjamin Geer <benjamin.geer@dasch.swiss>"
293+
]
294+
}
295+
}
296+
297+
shacl {
298+
# The directory that SHACL shapes are loaded from.
299+
shapes-dir = "shacl"
300+
shapes-dir = ${?KNORA_WEBAPI_SHACLE_SHAPES_DIR}
301+
}
296302

297303
print-extended-config = false // If true, an extended list of configuration parameters will be printed out at startup.
298304
print-extended-config = ${?KNORA_WEBAPI_PRINT_EXTENDED_CONFIG}

‎webapi/src/main/scala/org/knora/webapi/app/LiveCore.scala‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ import org.knora.webapi.settings.{KnoraDispatchers, KnoraSettings, KnoraSettings
2727
import scala.concurrent.ExecutionContext
2828
import scala.language.postfixOps
2929
import scala.languageFeature.postfixOps
30-
3130
import org.knora.webapi.core.Core
31+
import org.knora.webapi.messages.util.rdf.RdfFeatureFactory
3232

3333

3434
/**
@@ -57,8 +57,10 @@ trait LiveCore extends Core {
5757
implicit val executionContext: ExecutionContext = system.dispatchers.lookup(KnoraDispatchers.KnoraActorDispatcher)
5858

5959

60-
// Initialise StringFormatter with the system settings. This must happen before any responders are constructed.
60+
// Initialise StringFormatter and RdfFeatureFactory with the system settings.
61+
// This must happen before any responders are constructed.
6162
StringFormatter.init(settings)
63+
RdfFeatureFactory.init(settings)
6264

6365
/**
6466
* The main application supervisor actor which is at the top of the actor

‎webapi/src/main/scala/org/knora/webapi/messages/BUILD.bazel‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ scala_library(
3737
"@maven//:org_eclipse_rdf4j_rdf4j_repository_sail",
3838
"@maven//:org_eclipse_rdf4j_rdf4j_sail_api",
3939
"@maven//:org_eclipse_rdf4j_rdf4j_sail_memory",
40+
"@maven//:org_eclipse_rdf4j_rdf4j_shacl",
4041
"@maven//:org_jodd_jodd",
4142
"@maven//:org_scala_lang_modules_scala_xml_2_12",
4243
"@maven//:org_scala_lang_scala_library",

‎webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,16 @@ object OntologyConstants {
125125
val DateTimeStamp: IRI = XsdPrefixExpansion + "dateTimeStamp"
126126
}
127127

128+
object Shacl {
129+
val ShaclPrefixExpansion: IRI = "http://www.w3.org/ns/shacl#"
130+
131+
val Conforms: IRI = ShaclPrefixExpansion + "conforms"
132+
val Result: IRI = ShaclPrefixExpansion + "result"
133+
val SourceConstraintComponent: IRI = ShaclPrefixExpansion + "sourceConstraintComponent"
134+
val DatatypeConstraintComponent: IRI = ShaclPrefixExpansion + "DatatypeConstraintComponent"
135+
val MaxCountConstraintComponent: IRI = ShaclPrefixExpansion + "MaxCountConstraintComponent"
136+
}
137+
128138
/**
129139
* http://schema.org
130140
*/

0 commit comments

Comments
 (0)