Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GH-4622 SHACL Validation Report preserve blank node IDs with remote repositories #4624

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1076,6 +1076,13 @@ protected HttpResponse execute(HttpUriRequest method) throws IOException, RDF4JE
case HttpURLConnection.HTTP_UNAVAILABLE: // 503
throw new QueryInterruptedException();
default:

if (contentTypeIs(response, "application/shacl-validation-report")
&& getContentTypeSerialisation(response) == RDFFormat.BINARY) {
throw new RepositoryException(new RemoteShaclValidationException(
response.getEntity().getContent(), "", RDFFormat.BINARY));
}

ErrorInfo errInfo = getErrorInfo(response);
// Throw appropriate exception
if (errInfo.getErrorType() == ErrorType.MALFORMED_DATA) {
Expand All @@ -1087,10 +1094,10 @@ protected HttpResponse execute(HttpUriRequest method) throws IOException, RDF4JE
} else if (errInfo.getErrorType() == ErrorType.UNSUPPORTED_QUERY_LANGUAGE) {
throw new UnsupportedQueryLanguageException(errInfo.getErrorMessage());
} else if (contentTypeIs(response, "application/shacl-validation-report")) {
// Legacy support for validation exceptions prior to 4.3.3
RDFFormat format = getContentTypeSerialisation(response);
throw new RepositoryException(new RemoteShaclValidationException(
new StringReader(errInfo.toString()), "", format));

throw new RepositoryException(
new RemoteShaclValidationException(new StringReader(errInfo.toString()), "", format));
} else if (errInfo.toString().length() > 0) {
throw new RepositoryException(errInfo.toString());
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

package org.eclipse.rdf4j.http.client.shacl;

import java.io.InputStream;
import java.io.StringReader;

import org.eclipse.rdf4j.common.annotation.Experimental;
Expand All @@ -34,6 +35,10 @@ public RemoteShaclValidationException(StringReader stringReader, String s, RDFFo
remoteValidation = new RemoteValidation(stringReader, s, format);
}

public RemoteShaclValidationException(InputStream stringReader, String s, RDFFormat format) {
remoteValidation = new RemoteValidation(stringReader, s, format);
}

/**
* @return A Model containing the validation report as specified by the SHACL Recommendation
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,37 +12,43 @@
package org.eclipse.rdf4j.http.client.shacl;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;

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 {

StringReader stringReader;
String baseUri;
RDFFormat format;

Model model;

RemoteValidation(StringReader stringReader, String baseUri, RDFFormat format) {
this.stringReader = stringReader;
this.baseUri = baseUri;
this.format = format;
RemoteValidation(InputStream inputStream, String baseUri, RDFFormat format) {
try {
ParserConfig parserConfig = new ParserConfig().set(BasicParserSettings.PRESERVE_BNODE_IDS, true);
model = Rio.parse(inputStream, baseUri, format, parserConfig, SimpleValueFactory.getInstance(),
new ParseErrorLogger());
} catch (IOException e) {
throw new RuntimeException(e);
}
}

Model asModel() {
if (model == null) {
try {
model = Rio.parse(stringReader, baseUri, format);
} catch (IOException e) {
throw new RuntimeException(e);
}
RemoteValidation(StringReader stringReader, String baseUri, RDFFormat format) {
try {
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);
}
}

Model asModel() {
return model;
}

Expand Down
5 changes: 5 additions & 0 deletions tools/server-spring/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
<artifactId>rdf4j-config</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>rdf4j-rio-binary</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
*******************************************************************************/
package org.eclipse.rdf4j.http.server;

import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;

Expand All @@ -19,9 +18,10 @@

import org.eclipse.rdf4j.common.exception.ValidationException;
import org.eclipse.rdf4j.common.webapp.views.SimpleResponseView;
import org.eclipse.rdf4j.model.Model;
import org.eclipse.rdf4j.http.server.repository.statements.ValidationExceptionView;
import org.eclipse.rdf4j.rio.RDFFormat;
import org.eclipse.rdf4j.rio.Rio;
import org.eclipse.rdf4j.rio.RDFWriterFactory;
import org.eclipse.rdf4j.rio.RDFWriterRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerExceptionResolver;
Expand Down Expand Up @@ -72,26 +72,21 @@ public ModelAndView resolveException(HttpServletRequest request, HttpServletResp
}

if (temp instanceof ValidationException) {
// This is currently just a simple fix that causes the validation report to be printed.
// This should not be the final solution.
Model validationReportModel = ((ValidationException) temp).validationReportAsModel();

StringWriter stringWriter = new StringWriter();
model.put(SimpleResponseView.SC_KEY, HttpServletResponse.SC_CONFLICT);

// 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);
ProtocolUtil.logRequestParameters(request);

statusCode = HttpServletResponse.SC_CONFLICT;
errMsg = stringWriter.toString();
RDFWriterFactory rdfWriterFactory = RDFWriterRegistry.getInstance().get(RDFFormat.BINARY).orElseThrow();

Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/shacl-validation-report+n-quads");
model.put(SimpleResponseView.CUSTOM_HEADERS_KEY, headers);
}
model.put(ValidationExceptionView.FACTORY_KEY, rdfWriterFactory);
model.put(ValidationExceptionView.VALIDATION_EXCEPTION, temp);
return new ModelAndView(ValidationExceptionView.getInstance(), model);

model.put(SimpleResponseView.SC_KEY, statusCode);
model.put(SimpleResponseView.CONTENT_KEY, errMsg);
} else {
model.put(SimpleResponseView.SC_KEY, statusCode);
model.put(SimpleResponseView.CONTENT_KEY, errMsg);
}

return new ModelAndView(SimpleResponseView.getInstance(), model);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*******************************************************************************
* Copyright (c) 2015 Eclipse RDF4J contributors, Aduna, and others.
*
* 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.http.server.repository.statements;

import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.rdf4j.common.annotation.InternalUseOnly;
import org.eclipse.rdf4j.common.exception.ValidationException;
import org.eclipse.rdf4j.common.webapp.views.SimpleResponseView;
import org.eclipse.rdf4j.model.Model;
import org.eclipse.rdf4j.model.Namespace;
import org.eclipse.rdf4j.model.Statement;
import org.eclipse.rdf4j.rio.RDFFormat;
import org.eclipse.rdf4j.rio.RDFWriter;
import org.eclipse.rdf4j.rio.RDFWriterFactory;
import org.springframework.web.servlet.View;

/**
* View used to export a ValidationException.
*
* @author Håvard Ottestad
*/
@InternalUseOnly
public class ValidationExceptionView implements View {

public static final String FACTORY_KEY = "factory";

public static final String VALIDATION_EXCEPTION = "validationException";

private static final ValidationExceptionView INSTANCE = new ValidationExceptionView();

public static ValidationExceptionView getInstance() {
return INSTANCE;
}

private ValidationExceptionView() {
}

@Override
public String getContentType() {
return null;
}

@SuppressWarnings("rawtypes")
@Override
public void render(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception {

RDFWriterFactory rdfWriterFactory = (RDFWriterFactory) model.get(FACTORY_KEY);

RDFFormat rdfFormat = rdfWriterFactory.getRDFFormat();

try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
RDFWriter rdfWriter = rdfWriterFactory.getWriter(baos);

ValidationException validationException = (ValidationException) model.get(VALIDATION_EXCEPTION);

Model validationReportModel = validationException.validationReportAsModel();

rdfWriter.startRDF();
for (Namespace namespace : validationReportModel.getNamespaces()) {
rdfWriter.handleNamespace(namespace.getPrefix(), namespace.getName());
}
for (Statement statement : validationReportModel) {
rdfWriter.handleStatement(statement);
}
rdfWriter.endRDF();

try (OutputStream out = response.getOutputStream()) {
response.setStatus((int) model.get(SimpleResponseView.SC_KEY));

String mimeType = rdfFormat.getDefaultMIMEType();
if (rdfFormat.hasCharset()) {
Charset charset = rdfFormat.getCharset();
mimeType += "; charset=" + charset.name();
}

assert mimeType.startsWith("application/");
response.setContentType("application/shacl-validation-report+" + mimeType.replace("application/", ""));

out.write(baos.toByteArray());
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<Statement> 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);

}
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,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
Expand Down
Loading