Skip to content

Commit 938a327

Browse files
committed
GH-4622 change approach to writing the validation exception to more align with how RDF data is typically transmitted over HTTP
1 parent 4dc6141 commit 938a327

File tree

8 files changed

+211
-100
lines changed

8 files changed

+211
-100
lines changed

core/http/client/src/main/java/org/eclipse/rdf4j/http/client/SPARQLProtocolSession.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,6 +1076,13 @@ protected HttpResponse execute(HttpUriRequest method) throws IOException, RDF4JE
10761076
case HttpURLConnection.HTTP_UNAVAILABLE: // 503
10771077
throw new QueryInterruptedException();
10781078
default:
1079+
1080+
if (contentTypeIs(response, "application/shacl-validation-report")
1081+
&& getContentTypeSerialisation(response) == RDFFormat.BINARY) {
1082+
throw new RepositoryException(new RemoteShaclValidationException(
1083+
response.getEntity().getContent(), "", RDFFormat.BINARY));
1084+
}
1085+
10791086
ErrorInfo errInfo = getErrorInfo(response);
10801087
// Throw appropriate exception
10811088
if (errInfo.getErrorType() == ErrorType.MALFORMED_DATA) {
@@ -1087,10 +1094,10 @@ protected HttpResponse execute(HttpUriRequest method) throws IOException, RDF4JE
10871094
} else if (errInfo.getErrorType() == ErrorType.UNSUPPORTED_QUERY_LANGUAGE) {
10881095
throw new UnsupportedQueryLanguageException(errInfo.getErrorMessage());
10891096
} else if (contentTypeIs(response, "application/shacl-validation-report")) {
1097+
// Legacy support for validation exceptions prior to 4.3.3
10901098
RDFFormat format = getContentTypeSerialisation(response);
1091-
throw new RepositoryException(new RemoteShaclValidationException(
1092-
new StringReader(errInfo.toString()), "", format));
1093-
1099+
throw new RepositoryException(
1100+
new RemoteShaclValidationException(new StringReader(errInfo.toString()), "", format));
10941101
} else if (errInfo.toString().length() > 0) {
10951102
throw new RepositoryException(errInfo.toString());
10961103
} else {

core/http/client/src/main/java/org/eclipse/rdf4j/http/client/shacl/RemoteShaclValidationException.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

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

14+
import java.io.InputStream;
1415
import java.io.StringReader;
1516

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

38+
public RemoteShaclValidationException(InputStream stringReader, String s, RDFFormat format) {
39+
remoteValidation = new RemoteValidation(stringReader, s, format);
40+
}
41+
3742
/**
3843
* @return A Model containing the validation report as specified by the SHACL Recommendation
3944
*/

core/http/client/src/main/java/org/eclipse/rdf4j/http/client/shacl/RemoteValidation.java

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
package org.eclipse.rdf4j.http.client.shacl;
1313

1414
import java.io.IOException;
15+
import java.io.InputStream;
1516
import java.io.StringReader;
1617

1718
import org.eclipse.rdf4j.common.annotation.InternalUseOnly;
@@ -25,30 +26,29 @@
2526

2627
@InternalUseOnly
2728
class RemoteValidation {
28-
29-
StringReader stringReader;
30-
String baseUri;
31-
RDFFormat format;
32-
3329
Model model;
3430

35-
RemoteValidation(StringReader stringReader, String baseUri, RDFFormat format) {
36-
this.stringReader = stringReader;
37-
this.baseUri = baseUri;
38-
this.format = format;
31+
RemoteValidation(InputStream inputStream, String baseUri, RDFFormat format) {
32+
try {
33+
ParserConfig parserConfig = new ParserConfig().set(BasicParserSettings.PRESERVE_BNODE_IDS, true);
34+
model = Rio.parse(inputStream, baseUri, format, parserConfig, SimpleValueFactory.getInstance(),
35+
new ParseErrorLogger());
36+
} catch (IOException e) {
37+
throw new RuntimeException(e);
38+
}
3939
}
4040

41-
Model asModel() {
42-
if (model == null) {
43-
try {
44-
ParserConfig parserConfig = new ParserConfig().set(BasicParserSettings.PRESERVE_BNODE_IDS, true);
45-
model = Rio.parse(stringReader, baseUri, format, parserConfig, SimpleValueFactory.getInstance(),
46-
new ParseErrorLogger());
47-
} catch (IOException e) {
48-
throw new RuntimeException(e);
49-
}
41+
RemoteValidation(StringReader stringReader, String baseUri, RDFFormat format) {
42+
try {
43+
ParserConfig parserConfig = new ParserConfig().set(BasicParserSettings.PRESERVE_BNODE_IDS, true);
44+
model = Rio.parse(stringReader, baseUri, format, parserConfig, SimpleValueFactory.getInstance(),
45+
new ParseErrorLogger());
46+
} catch (IOException e) {
47+
throw new RuntimeException(e);
5048
}
49+
}
5150

51+
Model asModel() {
5252
return model;
5353
}
5454

tools/server-spring/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@
2727
<artifactId>rdf4j-config</artifactId>
2828
<version>${project.version}</version>
2929
</dependency>
30+
<dependency>
31+
<groupId>${project.groupId}</groupId>
32+
<artifactId>rdf4j-rio-binary</artifactId>
33+
<version>${project.version}</version>
34+
</dependency>
3035
<dependency>
3136
<groupId>javax.servlet</groupId>
3237
<artifactId>javax.servlet-api</artifactId>

tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/ProtocolExceptionResolver.java

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
*******************************************************************************/
1111
package org.eclipse.rdf4j.http.server;
1212

13-
import java.io.StringWriter;
1413
import java.util.HashMap;
1514
import java.util.Map;
1615

@@ -19,9 +18,10 @@
1918

2019
import org.eclipse.rdf4j.common.exception.ValidationException;
2120
import org.eclipse.rdf4j.common.webapp.views.SimpleResponseView;
22-
import org.eclipse.rdf4j.model.Model;
21+
import org.eclipse.rdf4j.http.server.repository.statements.ValidationExceptionView;
2322
import org.eclipse.rdf4j.rio.RDFFormat;
24-
import org.eclipse.rdf4j.rio.Rio;
23+
import org.eclipse.rdf4j.rio.RDFWriterFactory;
24+
import org.eclipse.rdf4j.rio.RDFWriterRegistry;
2525
import org.slf4j.Logger;
2626
import org.slf4j.LoggerFactory;
2727
import org.springframework.web.servlet.HandlerExceptionResolver;
@@ -72,25 +72,21 @@ public ModelAndView resolveException(HttpServletRequest request, HttpServletResp
7272
}
7373

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

79-
StringWriter stringWriter = new StringWriter();
76+
model.put(SimpleResponseView.SC_KEY, HttpServletResponse.SC_CONFLICT);
8077

81-
// We choose RDFJSON because this format doesn't rename blank nodes.
82-
Rio.write(validationReportModel, stringWriter, RDFFormat.RDFJSON);
78+
ProtocolUtil.logRequestParameters(request);
8379

84-
statusCode = HttpServletResponse.SC_CONFLICT;
85-
errMsg = stringWriter.toString();
80+
RDFWriterFactory rdfWriterFactory = RDFWriterRegistry.getInstance().get(RDFFormat.BINARY).orElseThrow();
8681

87-
Map<String, String> headers = new HashMap<>();
88-
headers.put("Content-Type", "application/shacl-validation-report+rdf+json");
89-
model.put(SimpleResponseView.CUSTOM_HEADERS_KEY, headers);
90-
}
82+
model.put(ValidationExceptionView.FACTORY_KEY, rdfWriterFactory);
83+
model.put(ValidationExceptionView.VALIDATION_EXCEPTION, temp);
84+
return new ModelAndView(ValidationExceptionView.getInstance(), model);
9185

92-
model.put(SimpleResponseView.SC_KEY, statusCode);
93-
model.put(SimpleResponseView.CONTENT_KEY, errMsg);
86+
} else {
87+
model.put(SimpleResponseView.SC_KEY, statusCode);
88+
model.put(SimpleResponseView.CONTENT_KEY, errMsg);
89+
}
9490

9591
return new ModelAndView(SimpleResponseView.getInstance(), model);
9692
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2015 Eclipse RDF4J contributors, Aduna, and others.
3+
*
4+
* All rights reserved. This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Distribution License v1.0
6+
* which accompanies this distribution, and is available at
7+
* http://www.eclipse.org/org/documents/edl-v10.php.
8+
*
9+
* SPDX-License-Identifier: BSD-3-Clause
10+
*******************************************************************************/
11+
package org.eclipse.rdf4j.http.server.repository.statements;
12+
13+
import java.io.ByteArrayOutputStream;
14+
import java.io.OutputStream;
15+
import java.nio.charset.Charset;
16+
import java.util.Map;
17+
18+
import javax.servlet.http.HttpServletRequest;
19+
import javax.servlet.http.HttpServletResponse;
20+
21+
import org.eclipse.rdf4j.common.exception.ValidationException;
22+
import org.eclipse.rdf4j.common.webapp.views.SimpleResponseView;
23+
import org.eclipse.rdf4j.model.Model;
24+
import org.eclipse.rdf4j.model.Namespace;
25+
import org.eclipse.rdf4j.model.Statement;
26+
import org.eclipse.rdf4j.rio.RDFFormat;
27+
import org.eclipse.rdf4j.rio.RDFWriter;
28+
import org.eclipse.rdf4j.rio.RDFWriterFactory;
29+
import org.springframework.web.servlet.View;
30+
31+
/**
32+
* View used to export a ValidationException.
33+
*
34+
* @author Håvard Ottestad
35+
*/
36+
public class ValidationExceptionView implements View {
37+
38+
public static final String FACTORY_KEY = "factory";
39+
40+
public static final String VALIDATION_EXCEPTION = "validationException";
41+
42+
private static final ValidationExceptionView INSTANCE = new ValidationExceptionView();
43+
44+
public static ValidationExceptionView getInstance() {
45+
return INSTANCE;
46+
}
47+
48+
private ValidationExceptionView() {
49+
}
50+
51+
@Override
52+
public String getContentType() {
53+
return null;
54+
}
55+
56+
@SuppressWarnings("rawtypes")
57+
@Override
58+
public void render(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception {
59+
60+
RDFWriterFactory rdfWriterFactory = (RDFWriterFactory) model.get(FACTORY_KEY);
61+
62+
RDFFormat rdfFormat = rdfWriterFactory.getRDFFormat();
63+
64+
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
65+
RDFWriter rdfWriter = rdfWriterFactory.getWriter(baos);
66+
67+
ValidationException validationException = (ValidationException) model.get(VALIDATION_EXCEPTION);
68+
69+
Model validationReportModel = validationException.validationReportAsModel();
70+
71+
rdfWriter.startRDF();
72+
for (Namespace namespace : validationReportModel.getNamespaces()) {
73+
rdfWriter.handleNamespace(namespace.getPrefix(), namespace.getName());
74+
}
75+
for (Statement statement : validationReportModel) {
76+
rdfWriter.handleStatement(statement);
77+
}
78+
rdfWriter.endRDF();
79+
80+
try (OutputStream out = response.getOutputStream()) {
81+
response.setStatus((int) model.get(SimpleResponseView.SC_KEY));
82+
83+
String mimeType = rdfFormat.getDefaultMIMEType();
84+
if (rdfFormat.hasCharset()) {
85+
Charset charset = rdfFormat.getCharset();
86+
mimeType += "; charset=" + charset.name();
87+
}
88+
89+
assert mimeType.startsWith("application/");
90+
response.setContentType("application/shacl-validation-report+" + mimeType.replace("application/", ""));
91+
92+
out.write(baos.toByteArray());
93+
}
94+
}
95+
}
96+
97+
}

tools/server/src/test/java/org/eclipse/rdf4j/http/server/ShaclValidationReportIT.java

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,30 @@
1515

1616
import java.io.IOException;
1717
import java.io.StringReader;
18+
import java.util.List;
19+
import java.util.stream.Collectors;
1820

1921
import org.eclipse.rdf4j.common.exception.ValidationException;
22+
import org.eclipse.rdf4j.http.client.shacl.RemoteShaclValidationException;
2023
import org.eclipse.rdf4j.http.protocol.Protocol;
24+
import org.eclipse.rdf4j.model.BNode;
25+
import org.eclipse.rdf4j.model.Model;
26+
import org.eclipse.rdf4j.model.Statement;
27+
import org.eclipse.rdf4j.model.Value;
2128
import org.eclipse.rdf4j.model.ValueFactory;
2229
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
30+
import org.eclipse.rdf4j.model.util.Values;
2331
import org.eclipse.rdf4j.model.vocabulary.RDF;
2432
import org.eclipse.rdf4j.model.vocabulary.RDF4J;
2533
import org.eclipse.rdf4j.model.vocabulary.RDFS;
2634
import org.eclipse.rdf4j.model.vocabulary.SHACL;
2735
import org.eclipse.rdf4j.repository.Repository;
2836
import org.eclipse.rdf4j.repository.RepositoryConnection;
37+
import org.eclipse.rdf4j.repository.RepositoryException;
2938
import org.eclipse.rdf4j.repository.http.HTTPRepository;
3039
import org.eclipse.rdf4j.rio.RDFFormat;
3140
import org.junit.jupiter.api.AfterAll;
41+
import org.junit.jupiter.api.Assertions;
3242
import org.junit.jupiter.api.BeforeAll;
3343
import org.junit.jupiter.api.Test;
3444

@@ -65,11 +75,12 @@ public static void stopServer() throws Exception {
6575
"ex:PersonShape\n" +
6676
"\ta sh:NodeShape ;\n" +
6777
"\tsh:targetClass rdfs:Resource ;\n" +
68-
"\tsh:property ex:PersonShapeProperty .\n" +
78+
"\tsh:property _:bnode .\n" +
6979
"\n" +
7080
"\n" +
71-
"ex:PersonShapeProperty\n" +
81+
"_:bnode\n" +
7282
" sh:path rdfs:label ;\n" +
83+
" rdfs:label \"abc\" ;\n" +
7384
" sh:minCount 1 .";
7485

7586
@Test
@@ -128,4 +139,54 @@ public void testAddingData() throws IOException {
128139

129140
}
130141

142+
@Test
143+
public void testBlankNodeIdsPreserved() throws IOException {
144+
145+
Repository repository = new HTTPRepository(
146+
Protocol.getRepositoryLocation(TestServer.SERVER_URL, TestServer.TEST_SHACL_REPO_ID));
147+
148+
try (RepositoryConnection connection = repository.getConnection()) {
149+
connection.begin();
150+
connection.add(new StringReader(shacl), "", RDFFormat.TURTLE, RDF4J.SHACL_SHAPE_GRAPH);
151+
connection.commit();
152+
}
153+
154+
try (RepositoryConnection connection = repository.getConnection()) {
155+
connection.begin();
156+
connection.add(RDFS.RESOURCE, RDF.TYPE, RDFS.RESOURCE);
157+
connection.commit();
158+
} catch (RepositoryException repositoryException) {
159+
160+
Model validationReport = ((RemoteShaclValidationException) repositoryException.getCause())
161+
.validationReportAsModel();
162+
163+
BNode shapeBnode = (BNode) validationReport
164+
.filter(null, SHACL.SOURCE_SHAPE, null)
165+
.objects()
166+
.stream()
167+
.findAny()
168+
.orElseThrow();
169+
170+
try (RepositoryConnection connection = repository.getConnection()) {
171+
List<Statement> collect = connection
172+
.getStatements(shapeBnode, null, null, RDF4J.SHACL_SHAPE_GRAPH)
173+
.stream()
174+
.collect(Collectors.toList());
175+
176+
Assertions.assertEquals(3, collect.size());
177+
178+
Value rdfsLabel = collect
179+
.stream()
180+
.filter(s -> s.getPredicate().equals(RDFS.LABEL))
181+
.map(Statement::getObject)
182+
.findAny()
183+
.orElseThrow();
184+
185+
Assertions.assertEquals(Values.literal("abc"), rdfsLabel);
186+
187+
}
188+
}
189+
190+
}
191+
131192
}

0 commit comments

Comments
 (0)