Rdf4jShaclShapeGraphShapeSource.java
/*******************************************************************************
* Copyright (c) 2022 Eclipse RDF4J contributors.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Distribution License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*******************************************************************************/
package org.eclipse.rdf4j.sail.shacl.wrapper.shape;
import static org.eclipse.rdf4j.model.util.Values.iri;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.rdf4j.common.iteration.CloseableIteration;
import org.eclipse.rdf4j.common.transaction.IsolationLevels;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Model;
import org.eclipse.rdf4j.model.Resource;
import org.eclipse.rdf4j.model.Statement;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.util.Statements;
import org.eclipse.rdf4j.model.vocabulary.DASH;
import org.eclipse.rdf4j.model.vocabulary.RDF;
import org.eclipse.rdf4j.model.vocabulary.RDF4J;
import org.eclipse.rdf4j.model.vocabulary.RDFS;
import org.eclipse.rdf4j.model.vocabulary.SHACL;
import org.eclipse.rdf4j.repository.Repository;
import org.eclipse.rdf4j.repository.RepositoryConnection;
import org.eclipse.rdf4j.repository.sail.SailRepository;
import org.eclipse.rdf4j.rio.RDFFormat;
import org.eclipse.rdf4j.rio.Rio;
import org.eclipse.rdf4j.sail.SailConnection;
import org.eclipse.rdf4j.sail.inferencer.fc.SchemaCachingRDFSInferencer;
import org.eclipse.rdf4j.sail.inferencer.fc.SchemaCachingRDFSInferencerConnection;
import org.eclipse.rdf4j.sail.memory.MemoryStore;
public class Rdf4jShaclShapeGraphShapeSource implements ShapeSource {
// SHACL Vocabulary from W3C - https://www.w3.org/ns/shacl.ttl
private final static IRI shaclVocabularyGraph = iri(RDF4J.NAMESPACE, "shaclVocabularyGraph");
private final static SchemaCachingRDFSInferencer shaclVocabulary = createShaclVocabulary();
private final RepositoryConnection connection;
private final Resource[] context;
private final Repository repository;
public Rdf4jShaclShapeGraphShapeSource(RepositoryConnection connection) {
this.context = null;
assert connection.isActive();
repository = forwardChain(connection);
this.connection = repository.getConnection();
this.connection.begin(IsolationLevels.NONE);
}
public Rdf4jShaclShapeGraphShapeSource(SailConnection connection) {
this.context = null;
assert connection.isActive();
repository = forwardChain(connection);
this.connection = repository.getConnection();
this.connection.begin(IsolationLevels.NONE);
}
private Rdf4jShaclShapeGraphShapeSource(Repository repository, RepositoryConnection connection,
Resource[] context) {
this.connection = connection;
this.context = context;
this.repository = repository;
}
private SailRepository forwardChain(RepositoryConnection shapesRepoConnection) {
try (var statements = shapesRepoConnection.getStatements(null, null, null, false, RDF4J.SHACL_SHAPE_GRAPH)) {
return forwardChain(statements);
}
}
private SailRepository forwardChain(SailConnection shapesSailConnection) {
try (var statements = shapesSailConnection.getStatements(null, null, null, false, RDF4J.SHACL_SHAPE_GRAPH)) {
return forwardChain(statements);
}
}
private SailRepository forwardChain(CloseableIteration<? extends Statement> statements) {
if (!statements.hasNext()) {
return new SailRepository(new MemoryStore());
}
SailRepository shapesRepoWithReasoning = new SailRepository(
SchemaCachingRDFSInferencer.fastInstantiateFrom(shaclVocabulary, new MemoryStore(), false));
try (var shapesRepoWithReasoningConnection = shapesRepoWithReasoning.getConnection()) {
shapesRepoWithReasoningConnection.begin(IsolationLevels.NONE);
shapesRepoWithReasoningConnection.add(statements);
enrichShapes(shapesRepoWithReasoningConnection);
shapesRepoWithReasoningConnection.commit();
}
return shapesRepoWithReasoning;
}
private static SchemaCachingRDFSInferencer createShaclVocabulary() {
try (InputStream in = getResourceAsStream("shacl-sparql-inference/shaclVocabulary.ttl")) {
SchemaCachingRDFSInferencer schemaCachingRDFSInferencer = new SchemaCachingRDFSInferencer(
new MemoryStore());
try (var connection = schemaCachingRDFSInferencer.getConnection()) {
connection.begin(IsolationLevels.NONE);
Model model = Rio.parse(in, "", RDFFormat.TURTLE);
model.forEach(s -> connection.addStatement(s.getSubject(), s.getPredicate(), s.getObject(),
shaclVocabularyGraph));
connection.commit();
}
// warm up the fast instantiation
SchemaCachingRDFSInferencer fastInstantiated = SchemaCachingRDFSInferencer
.fastInstantiateFrom(schemaCachingRDFSInferencer, new MemoryStore());
try (SchemaCachingRDFSInferencerConnection connection = fastInstantiated.getConnection()) {
connection.begin(IsolationLevels.NONE);
connection.commit();
} finally {
fastInstantiated.shutDown();
}
return schemaCachingRDFSInferencer;
} catch (IOException e) {
throw new IllegalStateException("Resource could not be read: shacl-sparql-inference/shaclVocabulary.ttl",
e);
}
}
private static InputStream getResourceAsStream(String filename) {
InputStream resourceAsStream = Rdf4jShaclShapeGraphShapeSource.class.getClassLoader()
.getResourceAsStream(filename);
if (resourceAsStream == null) {
throw new IllegalStateException("Resource could not be found: " + filename);
}
return new BufferedInputStream(resourceAsStream);
}
private void enrichShapes(RepositoryConnection shaclSailConnection) {
// performance optimisation, running the queries below is time-consuming, even if the repo is empty
if (shaclSailConnection.isEmpty()) {
return;
}
shaclSailConnection.add(DASH_CONSTANTS, RDF4J.SHACL_SHAPE_GRAPH);
shaclSailConnection.add(DASH_CONSTANTS);
shaclSailConnection.add(DASH_CONSTANTS, new Resource[] { null });
try (Stream<Statement> stream = shaclSailConnection
.getStatements(null, SHACL.SHAPES_GRAPH, null, false)
.stream()) {
stream.forEach(s -> {
shaclSailConnection.add(DASH_CONSTANTS, ((IRI) s.getObject()));
});
}
implicitTargetClass(shaclSailConnection);
}
private void implicitTargetClass(RepositoryConnection shaclSailConnection) {
try (var stream = shaclSailConnection.getStatements(null, RDF.TYPE, RDFS.CLASS, false).stream()) {
stream
.map(Statement::getSubject)
.filter(s ->
shaclSailConnection.hasStatement(s, RDF.TYPE, SHACL.NODE_SHAPE, true)
|| shaclSailConnection.hasStatement(s, RDF.TYPE, SHACL.PROPERTY_SHAPE, true)
)
.forEach(s -> {
// TODO: This only works for the MemoryStore where we store the shape and not for other graphs
shaclSailConnection.add(s, SHACL.TARGET_CLASS, s, RDF4J.SHACL_SHAPE_GRAPH);
});
}
}
public Rdf4jShaclShapeGraphShapeSource withContext(Resource[] context) {
return new Rdf4jShaclShapeGraphShapeSource(repository, connection, context);
}
@Override
public Resource[] getActiveContexts() {
return context;
}
public Stream<ShapesGraph> getAllShapeContexts() {
assert context != null;
Stream<ShapesGraph> rsxDataAndShapesGraphLink = ShapeSource.getRsxDataAndShapesGraphLink(connection, context);
try (Stream<? extends Statement> stream = connection
.getStatements(null, SHACL.SHAPES_GRAPH, null, false, context)
.stream()) {
var collect = stream.collect(Collectors.toList());
return Stream.concat(
Stream.concat(rsxDataAndShapesGraphLink, Stream.of(new ShapesGraph(RDF4J.SHACL_SHAPE_GRAPH))),
collect.stream()
.collect(Collectors.groupingBy(Statement::getSubject))
.entrySet()
.stream()
.map(entry -> new ShapeSource.ShapesGraph(entry.getKey(), entry.getValue())));
}
}
public Stream<Resource> getTargetableShape() {
assert context != null;
return Stream
.of(getSubjects(Predicates.TARGET_NODE),
getSubjects(Predicates.TARGET_CLASS),
getSubjects(Predicates.TARGET_SUBJECTS_OF),
getSubjects(Predicates.TARGET_OBJECTS_OF),
getSubjects(Predicates.TARGET_PROP),
getSubjects(Predicates.RSX_targetShape)
)
.reduce(Stream::concat)
.get()
.distinct();
}
public boolean isType(Resource subject, IRI type) {
assert context != null;
if (connection.hasStatement(subject, RDF.TYPE, type, true, context)) {
return true;
}
if (type.equals(SHACL.NODE_SHAPE)) {
if (connection.hasStatement(subject, RDF.TYPE, SHACL.SHAPE, true, context)) {
return true;
}
try (Stream<Statement> stream = connection.getStatements(subject, null, null, true, context).stream()) {
return stream
.map(Statement::getPredicate)
.map(Value::stringValue)
.anyMatch(predicate -> predicate.startsWith(SHACL.NAMESPACE)
|| predicate.startsWith(DASH.NAMESPACE));
}
}
return false;
}
public Stream<Resource> getSubjects(Predicates predicate) {
assert context != null;
return connection.getStatements(null, predicate.getIRI(), null, true, context)
.stream()
.map(Statement::getSubject)
.distinct();
}
public Stream<Value> getObjects(Resource subject, Predicates predicate) {
assert context != null;
return connection.getStatements(subject, predicate.getIRI(), null, true, context)
.stream()
.map(Statement::getObject)
.distinct();
}
public Stream<Statement> getAllStatements(Resource id) {
assert context != null;
return connection.getStatements(id, null, null, true, context)
.stream()
.map(Statements::stripContext)
.distinct();
}
public Value getRdfFirst(Resource subject) {
return ShapeSourceHelper.getFirst(connection, subject, context);
}
public Resource getRdfRest(Resource subject) {
return ShapeSourceHelper.getRdfRest(connection, subject, context);
}
@Override
public void close() {
connection.commit();
connection.close();
repository.shutDown();
}
}