diff --git a/core/http/client/src/main/java/org/eclipse/rdf4j/http/client/SPARQLProtocolSession.java b/core/http/client/src/main/java/org/eclipse/rdf4j/http/client/SPARQLProtocolSession.java index 29d621999f5..42adcc56671 100644 --- a/core/http/client/src/main/java/org/eclipse/rdf4j/http/client/SPARQLProtocolSession.java +++ b/core/http/client/src/main/java/org/eclipse/rdf4j/http/client/SPARQLProtocolSession.java @@ -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) { @@ -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 { diff --git a/core/http/client/src/main/java/org/eclipse/rdf4j/http/client/shacl/RemoteShaclValidationException.java b/core/http/client/src/main/java/org/eclipse/rdf4j/http/client/shacl/RemoteShaclValidationException.java index 3687b8011bc..e5fc8a59c56 100644 --- a/core/http/client/src/main/java/org/eclipse/rdf4j/http/client/shacl/RemoteShaclValidationException.java +++ b/core/http/client/src/main/java/org/eclipse/rdf4j/http/client/shacl/RemoteShaclValidationException.java @@ -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; @@ -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 */ 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 54e95d4624b..e0dbf19ae29 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 @@ -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; @@ -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; } diff --git a/tools/server-spring/pom.xml b/tools/server-spring/pom.xml index d2ee53151df..4892b210024 100644 --- a/tools/server-spring/pom.xml +++ b/tools/server-spring/pom.xml @@ -27,6 +27,11 @@ rdf4j-config ${project.version} + + ${project.groupId} + rdf4j-rio-binary + ${project.version} + javax.servlet javax.servlet-api 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 4edb356141e..90631033308 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 @@ -10,7 +10,6 @@ *******************************************************************************/ package org.eclipse.rdf4j.http.server; -import java.io.StringWriter; import java.util.HashMap; import java.util.Map; @@ -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; @@ -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 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); } diff --git a/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/statements/ValidationExceptionView.java b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/statements/ValidationExceptionView.java new file mode 100644 index 00000000000..4dcc2473dab --- /dev/null +++ b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/statements/ValidationExceptionView.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * 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.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 + */ +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()); + } + } + } + +} 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 8510bf09e68..de1ab4fafc8 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,26 +12,17 @@ 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; @@ -39,7 +30,6 @@ 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; @@ -242,54 +232,4 @@ 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); - - } - } - - } - }