Skip to content

Commit

Permalink
GH-4622 change approach to writing the validation exception to more a…
Browse files Browse the repository at this point in the history
…lign with how RDF data is typically transmitted over HTTP
  • Loading branch information
hmottestad committed Jun 17, 2023
1 parent 4dc6141 commit 8865893
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 100 deletions.
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,6 +12,7 @@
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;
Expand All @@ -25,30 +26,29 @@

@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 {
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);
}
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,25 +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 RDFJSON because this format doesn't rename blank nodes.
Rio.write(validationReportModel, stringWriter, RDFFormat.RDFJSON);
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+rdf+json");
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);

}
}

}

}
Loading

0 comments on commit 8865893

Please sign in to comment.