diff --git a/core/http/client/src/main/java/org/eclipse/rdf4j/http/client/shacl/RemoteValidation.java b/core/http/client/src/main/java/org/eclipse/rdf4j/http/client/shacl/RemoteValidation.java index 75968f4a9be..54e95d4624b 100644 --- a/core/http/client/src/main/java/org/eclipse/rdf4j/http/client/shacl/RemoteValidation.java +++ b/core/http/client/src/main/java/org/eclipse/rdf4j/http/client/shacl/RemoteValidation.java @@ -16,8 +16,12 @@ import org.eclipse.rdf4j.common.annotation.InternalUseOnly; import org.eclipse.rdf4j.model.Model; +import org.eclipse.rdf4j.model.impl.SimpleValueFactory; +import org.eclipse.rdf4j.rio.ParserConfig; import org.eclipse.rdf4j.rio.RDFFormat; import org.eclipse.rdf4j.rio.Rio; +import org.eclipse.rdf4j.rio.helpers.BasicParserSettings; +import org.eclipse.rdf4j.rio.helpers.ParseErrorLogger; @InternalUseOnly class RemoteValidation { @@ -37,7 +41,9 @@ class RemoteValidation { Model asModel() { if (model == null) { try { - model = Rio.parse(stringReader, baseUri, format); + ParserConfig parserConfig = new ParserConfig().set(BasicParserSettings.PRESERVE_BNODE_IDS, true); + model = Rio.parse(stringReader, baseUri, format, parserConfig, SimpleValueFactory.getInstance(), + new ParseErrorLogger()); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/ProtocolExceptionResolver.java b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/ProtocolExceptionResolver.java index 6163ad51a7b..4edb356141e 100644 --- a/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/ProtocolExceptionResolver.java +++ b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/ProtocolExceptionResolver.java @@ -78,15 +78,14 @@ public ModelAndView resolveException(HttpServletRequest request, HttpServletResp StringWriter stringWriter = new StringWriter(); - // We choose NQUADS because we want to support streaming in the future, and because there could be a use for - // different graphs in the future - Rio.write(validationReportModel, stringWriter, RDFFormat.NQUADS); + // We choose RDFJSON because this format doesn't rename blank nodes. + Rio.write(validationReportModel, stringWriter, RDFFormat.RDFJSON); statusCode = HttpServletResponse.SC_CONFLICT; errMsg = stringWriter.toString(); Map headers = new HashMap<>(); - headers.put("Content-Type", "application/shacl-validation-report+n-quads"); + headers.put("Content-Type", "application/shacl-validation-report+rdf+json"); model.put(SimpleResponseView.CUSTOM_HEADERS_KEY, headers); } diff --git a/tools/server/src/test/java/org/eclipse/rdf4j/http/server/ShaclValidationReportIT.java b/tools/server/src/test/java/org/eclipse/rdf4j/http/server/ShaclValidationReportIT.java index a0427ccf1d6..a9ca28eecf5 100644 --- a/tools/server/src/test/java/org/eclipse/rdf4j/http/server/ShaclValidationReportIT.java +++ b/tools/server/src/test/java/org/eclipse/rdf4j/http/server/ShaclValidationReportIT.java @@ -15,20 +15,30 @@ import java.io.IOException; import java.io.StringReader; +import java.util.List; +import java.util.stream.Collectors; import org.eclipse.rdf4j.common.exception.ValidationException; +import org.eclipse.rdf4j.http.client.shacl.RemoteShaclValidationException; import org.eclipse.rdf4j.http.protocol.Protocol; +import org.eclipse.rdf4j.model.BNode; +import org.eclipse.rdf4j.model.Model; +import org.eclipse.rdf4j.model.Statement; +import org.eclipse.rdf4j.model.Value; import org.eclipse.rdf4j.model.ValueFactory; import org.eclipse.rdf4j.model.impl.SimpleValueFactory; +import org.eclipse.rdf4j.model.util.Values; 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.RepositoryException; import org.eclipse.rdf4j.repository.http.HTTPRepository; import org.eclipse.rdf4j.rio.RDFFormat; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -65,11 +75,12 @@ public static void stopServer() throws Exception { "ex:PersonShape\n" + "\ta sh:NodeShape ;\n" + "\tsh:targetClass rdfs:Resource ;\n" + - "\tsh:property ex:PersonShapeProperty .\n" + + "\tsh:property _:bnode .\n" + "\n" + "\n" + - "ex:PersonShapeProperty\n" + + "_:bnode\n" + " sh:path rdfs:label ;\n" + + " rdfs:label \"abc\" ;\n" + " sh:minCount 1 ."; @Test @@ -128,4 +139,54 @@ public void testAddingData() throws IOException { } + @Test + public void testBlankNodeIdsPreserved() throws IOException { + + Repository repository = new HTTPRepository( + Protocol.getRepositoryLocation(TestServer.SERVER_URL, TestServer.TEST_SHACL_REPO_ID)); + + try (RepositoryConnection connection = repository.getConnection()) { + connection.begin(); + connection.add(new StringReader(shacl), "", RDFFormat.TURTLE, RDF4J.SHACL_SHAPE_GRAPH); + connection.commit(); + } + + try (RepositoryConnection connection = repository.getConnection()) { + connection.begin(); + connection.add(RDFS.RESOURCE, RDF.TYPE, RDFS.RESOURCE); + connection.commit(); + } catch (RepositoryException repositoryException) { + + Model validationReport = ((RemoteShaclValidationException) repositoryException.getCause()) + .validationReportAsModel(); + + BNode shapeBnode = (BNode) validationReport + .filter(null, SHACL.SOURCE_SHAPE, null) + .objects() + .stream() + .findAny() + .orElseThrow(); + + try (RepositoryConnection connection = repository.getConnection()) { + List collect = connection + .getStatements(shapeBnode, null, null, RDF4J.SHACL_SHAPE_GRAPH) + .stream() + .collect(Collectors.toList()); + + Assertions.assertEquals(3, collect.size()); + + Value rdfsLabel = collect + .stream() + .filter(s -> s.getPredicate().equals(RDFS.LABEL)) + .map(Statement::getObject) + .findAny() + .orElseThrow(); + + Assertions.assertEquals(Values.literal("abc"), rdfsLabel); + + } + } + + } + } diff --git a/tools/server/src/test/java/org/eclipse/rdf4j/http/server/TransactionSettingsIT.java b/tools/server/src/test/java/org/eclipse/rdf4j/http/server/TransactionSettingsIT.java index 2573b6b916b..8510bf09e68 100644 --- a/tools/server/src/test/java/org/eclipse/rdf4j/http/server/TransactionSettingsIT.java +++ b/tools/server/src/test/java/org/eclipse/rdf4j/http/server/TransactionSettingsIT.java @@ -12,17 +12,26 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import java.io.IOException; import java.io.StringReader; +import java.util.List; +import java.util.stream.Collectors; import org.eclipse.rdf4j.common.transaction.IsolationLevels; import org.eclipse.rdf4j.http.client.shacl.RemoteShaclValidationException; import org.eclipse.rdf4j.http.protocol.Protocol; +import org.eclipse.rdf4j.model.BNode; +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.ValueFactory; import org.eclipse.rdf4j.model.impl.SimpleValueFactory; +import org.eclipse.rdf4j.model.util.Values; 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.RepositoryException; @@ -30,6 +39,7 @@ import org.eclipse.rdf4j.rio.RDFFormat; import org.eclipse.rdf4j.sail.shacl.ShaclSail; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -67,11 +77,12 @@ public static void stopServer() throws Exception { "ex:PersonShape\n" + "\ta sh:NodeShape ;\n" + "\tsh:targetClass rdfs:Resource ;\n" + - "\tsh:property ex:PersonShapeProperty .\n" + + "\tsh:property _:bnode .\n" + "\n" + "\n" + - "ex:PersonShapeProperty\n" + + "_:bnode\n" + " sh:path rdfs:label ;\n" + + " rdfs:label \"abc\" ;\n" + " sh:minCount 1 ."; @BeforeEach @@ -231,4 +242,54 @@ public void testValidationDisabledSnapshotSerializableValidation() throws Throwa } + @Test + public void testBlankNodeIdsPreserved() throws IOException { + + Repository repository = new HTTPRepository( + Protocol.getRepositoryLocation(TestServer.SERVER_URL, TestServer.TEST_SHACL_REPO_ID)); + + try (RepositoryConnection connection = repository.getConnection()) { + connection.begin(); + connection.add(new StringReader(shacl), "", RDFFormat.TURTLE, RDF4J.SHACL_SHAPE_GRAPH); + connection.commit(); + } + + try (RepositoryConnection connection = repository.getConnection()) { + connection.begin(); + connection.add(RDFS.RESOURCE, RDF.TYPE, RDFS.RESOURCE); + connection.commit(); + } catch (RepositoryException repositoryException) { + + Model validationReport = ((RemoteShaclValidationException) repositoryException.getCause()) + .validationReportAsModel(); + + BNode shapeBnode = (BNode) validationReport + .filter(null, SHACL.SOURCE_SHAPE, null) + .objects() + .stream() + .findAny() + .orElseThrow(); + + try (RepositoryConnection connection = repository.getConnection()) { + List collect = connection + .getStatements(shapeBnode, null, null, RDF4J.SHACL_SHAPE_GRAPH) + .stream() + .collect(Collectors.toList()); + + Assertions.assertEquals(3, collect.size()); + + Value rdfsLabel = collect + .stream() + .filter(s -> s.getPredicate().equals(RDFS.LABEL)) + .map(Statement::getObject) + .findAny() + .orElseThrow(); + + Assertions.assertEquals(Values.literal("abc"), rdfsLabel); + + } + } + + } + }