diff --git a/jena/pom.xml b/jena/pom.xml
index 5fe41539dde..99f1602e647 100644
--- a/jena/pom.xml
+++ b/jena/pom.xml
@@ -67,6 +67,12 @@
${slf4j.version}
test
+
+ com.inrupt.client
+ inrupt-client-jackson
+ ${project.version}
+ test
+
org.wiremock
wiremock
diff --git a/jena/src/main/java/com/inrupt/client/jena/JenaBodyHandlers.java b/jena/src/main/java/com/inrupt/client/jena/JenaBodyHandlers.java
index d734b7906fe..697e0f82f2b 100644
--- a/jena/src/main/java/com/inrupt/client/jena/JenaBodyHandlers.java
+++ b/jena/src/main/java/com/inrupt/client/jena/JenaBodyHandlers.java
@@ -20,11 +20,13 @@
*/
package com.inrupt.client.jena;
+import com.inrupt.client.ClientHttpException;
import com.inrupt.client.Response;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
import org.apache.jena.atlas.web.ContentType;
import org.apache.jena.graph.Graph;
@@ -44,13 +46,20 @@ public final class JenaBodyHandlers {
private static final String CONTENT_TYPE = "Content-Type";
- /**
- * Populate a Jena {@link Model} with an HTTP response body.
- *
- * @return an HTTP body handler
- */
- public static Response.BodyHandler ofModel() {
- return responseInfo -> responseInfo.headers().firstValue(CONTENT_TYPE)
+ private static void throwOnError(final Response.ResponseInfo responseInfo) {
+ if (!Response.isSuccess(responseInfo.statusCode())) {
+ throw new ClientHttpException(
+ "Could not map to a Jena entity.",
+ responseInfo.uri(),
+ responseInfo.statusCode(),
+ responseInfo.headers(),
+ new String(responseInfo.body().array(), StandardCharsets.UTF_8)
+ );
+ }
+ }
+
+ private static Model responseToModel(final Response.ResponseInfo responseInfo) {
+ return responseInfo.headers().firstValue(CONTENT_TYPE)
.map(JenaBodyHandlers::toJenaLang).map(lang -> {
try (final var input = new ByteArrayInputStream(responseInfo.body().array())) {
final var model = ModelFactory.createDefaultModel();
@@ -65,12 +74,29 @@ public static Response.BodyHandler ofModel() {
}
/**
- * Populate a Jena {@link Graph} with an HTTP response.
+ * Populate a Jena {@link Model} with an HTTP response body.
*
* @return an HTTP body handler
+ * @deprecated Use {@link JenaBodyHandlers#ofJenaModel()} instead for consistent HTTP error handling.
*/
- public static Response.BodyHandler ofGraph() {
- return responseInfo -> responseInfo.headers().firstValue(CONTENT_TYPE)
+ public static Response.BodyHandler ofModel() {
+ return JenaBodyHandlers::responseToModel;
+ }
+
+ /**
+ * Populate a Jena {@link Model} with an HTTP response body.
+ *
+ * @return an HTTP body handler
+ */
+ public static Response.BodyHandler ofJenaModel() {
+ return responseInfo -> {
+ JenaBodyHandlers.throwOnError(responseInfo);
+ return JenaBodyHandlers.responseToModel(responseInfo);
+ };
+ }
+
+ private static Graph responseToGraph(final Response.ResponseInfo responseInfo) {
+ return responseInfo.headers().firstValue(CONTENT_TYPE)
.map(JenaBodyHandlers::toJenaLang).map(lang -> {
try (final var input = new ByteArrayInputStream(responseInfo.body().array())) {
final var graph = GraphMemFactory.createDefaultGraph();
@@ -84,24 +110,63 @@ public static Response.BodyHandler ofGraph() {
.orElseGet(GraphMemFactory::createDefaultGraph);
}
+ /**
+ * Populate a Jena {@link Graph} with an HTTP response.
+ *
+ * @return an HTTP body handler
+ * @deprecated Use {@link JenaBodyHandlers#ofJenaGraph} instead for consistent HTTP error handling.
+ */
+ public static Response.BodyHandler ofGraph() {
+ return JenaBodyHandlers::responseToGraph;
+ }
+
+ /**
+ * Populate a Jena {@link Graph} with an HTTP response.
+ *
+ * @return an HTTP body handler
+ */
+ public static Response.BodyHandler ofJenaGraph() {
+ return responseInfo -> {
+ JenaBodyHandlers.throwOnError(responseInfo);
+ return JenaBodyHandlers.responseToGraph(responseInfo);
+ };
+ }
+
+ private static Dataset responseToDataset(final Response.ResponseInfo responseInfo) {
+ return responseInfo.headers().firstValue(CONTENT_TYPE)
+ .map(JenaBodyHandlers::toJenaLang).map(lang -> {
+ try (final var input = new ByteArrayInputStream(responseInfo.body().array())) {
+ final var dataset = DatasetFactory.create();
+ RDFDataMgr.read(dataset, input, responseInfo.uri().toString(), lang);
+ return dataset;
+ } catch (final IOException ex) {
+ throw new UncheckedIOException(
+ "An I/O error occurred while data was read from the InputStream into a Dataset", ex);
+ }
+ })
+ .orElseGet(DatasetFactory::create);
+ }
+
/**
* Populate a Jena {@link Dataset} with an HTTP response.
*
* @return an HTTP body handler
+ * @deprecated Use {@link JenaBodyHandlers#ofJenaDataset} instead for consistent HTTP error handling.
*/
public static Response.BodyHandler ofDataset() {
- return responseInfo -> responseInfo.headers().firstValue(CONTENT_TYPE)
- .map(JenaBodyHandlers::toJenaLang).map(lang -> {
- try (final var input = new ByteArrayInputStream(responseInfo.body().array())) {
- final var dataset = DatasetFactory.create();
- RDFDataMgr.read(dataset, input, responseInfo.uri().toString(), lang);
- return dataset;
- } catch (final IOException ex) {
- throw new UncheckedIOException(
- "An I/O error occurred while data was read from the InputStream into a Dataset", ex);
- }
- })
- .orElseGet(DatasetFactory::create);
+ return JenaBodyHandlers::responseToDataset;
+ }
+
+ /**
+ * Populate a Jena {@link Dataset} with an HTTP response.
+ *
+ * @return an HTTP body handler
+ */
+ public static Response.BodyHandler ofJenaDataset() {
+ return responseInfo -> {
+ JenaBodyHandlers.throwOnError(responseInfo);
+ return JenaBodyHandlers.responseToDataset(responseInfo);
+ };
}
static Lang toJenaLang(final String mediaType) {
diff --git a/jena/src/test/java/com/inrupt/client/jena/JenaBodyHandlersTest.java b/jena/src/test/java/com/inrupt/client/jena/JenaBodyHandlersTest.java
index 8c8a9e030bc..712541142b2 100644
--- a/jena/src/test/java/com/inrupt/client/jena/JenaBodyHandlersTest.java
+++ b/jena/src/test/java/com/inrupt/client/jena/JenaBodyHandlersTest.java
@@ -22,6 +22,7 @@
import static org.junit.jupiter.api.Assertions.*;
+import com.inrupt.client.ClientHttpException;
import com.inrupt.client.Request;
import com.inrupt.client.Response;
import com.inrupt.client.spi.HttpService;
@@ -32,6 +33,7 @@
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
+import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import org.apache.jena.graph.NodeFactory;
@@ -57,14 +59,14 @@ static void teardown() {
}
@Test
- void testOfModelHandler() throws IOException,
+ void testOfJenaModelHandler() throws IOException,
InterruptedException {
final Request request = Request.newBuilder()
.uri(URI.create(config.get("rdf_uri") + "/oneTriple"))
.GET()
.build();
- final var response = client.send(request, JenaBodyHandlers.ofModel())
+ final var response = client.send(request, JenaBodyHandlers.ofJenaModel())
.toCompletableFuture().join();
assertEquals(200, response.statusCode());
@@ -78,7 +80,7 @@ void testOfModelHandler() throws IOException,
}
@Test
- void testOfModelHandlerAsync() throws IOException,
+ void testOfJenaModelHandlerAsync() throws IOException,
InterruptedException, ExecutionException {
final Request request = Request.newBuilder()
.uri(URI.create(config.get("rdf_uri") + "/oneTriple"))
@@ -86,7 +88,7 @@ void testOfModelHandlerAsync() throws IOException,
.GET()
.build();
- final var asyncResponse = client.send(request, JenaBodyHandlers.ofModel());
+ final var asyncResponse = client.send(request, JenaBodyHandlers.ofJenaModel());
final int statusCode = asyncResponse.thenApply(Response::statusCode).toCompletableFuture().join();
assertEquals(200, statusCode);
@@ -101,13 +103,13 @@ void testOfModelHandlerAsync() throws IOException,
}
@Test
- void testOfModelHandlerWithURL() throws IOException, InterruptedException {
+ void testOfJenaModelHandlerWithURL() throws IOException, InterruptedException {
final Request request = Request.newBuilder()
.uri(URI.create(config.get("rdf_uri") + "/example"))
.GET()
.build();
- final var response = client.send(request, JenaBodyHandlers.ofModel())
+ final var response = client.send(request, JenaBodyHandlers.ofJenaModel())
.toCompletableFuture().join();
assertEquals(200, response.statusCode());
@@ -120,14 +122,36 @@ void testOfModelHandlerWithURL() throws IOException, InterruptedException {
}
@Test
- void testOfDatasetHandler() throws IOException,
+ void testOfJenaModelHandlerError() throws IOException,
+ InterruptedException {
+ final Request request = Request.newBuilder()
+ .uri(URI.create(config.get("rdf_uri") + "/error"))
+ .GET()
+ .build();
+
+ final CompletionException completionException = assertThrows(
+ CompletionException.class,
+ () -> client.send(request, JenaBodyHandlers.ofJenaModel()).toCompletableFuture().join()
+ );
+
+ final ClientHttpException httpException = (ClientHttpException) completionException.getCause();
+
+ assertEquals(429, httpException.getProblemDetails().getStatus());
+ assertEquals("Too Many Requests", httpException.getProblemDetails().getTitle());
+ assertEquals("Some details", httpException.getProblemDetails().getDetails());
+ assertEquals("https://example.org/type", httpException.getProblemDetails().getType().toString());
+ assertEquals("https://example.org/instance", httpException.getProblemDetails().getInstance().toString());
+ }
+
+ @Test
+ void testOfJenaDatasetHandler() throws IOException,
InterruptedException {
final Request request = Request.newBuilder()
.uri(URI.create(config.get("rdf_uri") + "/oneTriple"))
.GET()
.build();
- final var response = client.send(request, JenaBodyHandlers.ofDataset())
+ final var response = client.send(request, JenaBodyHandlers.ofJenaDataset())
.toCompletableFuture().join();
assertEquals(200, response.statusCode());
@@ -142,13 +166,13 @@ void testOfDatasetHandler() throws IOException,
}
@Test
- void testOfDatasetHandlerWithURL() throws IOException, InterruptedException {
+ void testOfJenaDatasetHandlerWithURL() throws IOException, InterruptedException {
final Request request = Request.newBuilder()
.uri(URI.create(config.get("rdf_uri") + "/example"))
.GET()
.build();
- final var response = client.send(request, JenaBodyHandlers.ofDataset())
+ final var response = client.send(request, JenaBodyHandlers.ofJenaDataset())
.toCompletableFuture().join();
assertEquals(200, response.statusCode());
@@ -163,7 +187,29 @@ void testOfDatasetHandlerWithURL() throws IOException, InterruptedException {
}
@Test
- void testOfGraphHandlerAsync() throws IOException,
+ void testOfJenaDatasetHandlerError() throws IOException,
+ InterruptedException {
+ final Request request = Request.newBuilder()
+ .uri(URI.create(config.get("rdf_uri") + "/error"))
+ .GET()
+ .build();
+
+ final CompletionException completionException = assertThrows(
+ CompletionException.class,
+ () -> client.send(request, JenaBodyHandlers.ofJenaDataset()).toCompletableFuture().join()
+ );
+
+ final ClientHttpException httpException = (ClientHttpException) completionException.getCause();
+
+ assertEquals(429, httpException.getProblemDetails().getStatus());
+ assertEquals("Too Many Requests", httpException.getProblemDetails().getTitle());
+ assertEquals("Some details", httpException.getProblemDetails().getDetails());
+ assertEquals("https://example.org/type", httpException.getProblemDetails().getType().toString());
+ assertEquals("https://example.org/instance", httpException.getProblemDetails().getInstance().toString());
+ }
+
+ @Test
+ void testOfJenaGraphHandlerAsync() throws IOException,
InterruptedException, ExecutionException {
final Request request = Request.newBuilder()
.uri(URI.create(config.get("rdf_uri") + "/oneTriple"))
@@ -171,7 +217,7 @@ void testOfGraphHandlerAsync() throws IOException,
.GET()
.build();
- final var asyncResponse = client.send(request, JenaBodyHandlers.ofGraph());
+ final var asyncResponse = client.send(request, JenaBodyHandlers.ofJenaGraph());
final int statusCode = asyncResponse.thenApply(Response::statusCode).toCompletableFuture().join();
assertEquals(200, statusCode);
@@ -186,14 +232,14 @@ void testOfGraphHandlerAsync() throws IOException,
}
@Test
- void testOfGraphHandler() throws IOException,
+ void testOfJenaGraphHandler() throws IOException,
InterruptedException {
final Request request = Request.newBuilder()
.uri(URI.create(config.get("rdf_uri") + "/oneTriple"))
.GET()
.build();
- final var response = client.send(request, JenaBodyHandlers.ofGraph())
+ final var response = client.send(request, JenaBodyHandlers.ofJenaGraph())
.toCompletableFuture().join();
assertEquals(200, response.statusCode());
@@ -207,13 +253,13 @@ void testOfGraphHandler() throws IOException,
}
@Test
- void testOfGraphHandlerWithURL() throws IOException, InterruptedException {
+ void testOfJenaGraphHandlerWithURL() throws IOException, InterruptedException {
final Request request = Request.newBuilder()
.uri(URI.create(config.get("rdf_uri") + "/example"))
.GET()
.build();
- final var response = client.send(request, JenaBodyHandlers.ofGraph())
+ final var response = client.send(request, JenaBodyHandlers.ofJenaGraph())
.toCompletableFuture().join();
assertEquals(200, response.statusCode());
@@ -225,4 +271,26 @@ void testOfGraphHandlerWithURL() throws IOException, InterruptedException {
null)
);
}
+
+ @Test
+ void testOfJenaGraphHandlerError() throws IOException,
+ InterruptedException {
+ final Request request = Request.newBuilder()
+ .uri(URI.create(config.get("rdf_uri") + "/error"))
+ .GET()
+ .build();
+
+ final CompletionException completionException = assertThrows(
+ CompletionException.class,
+ () -> client.send(request, JenaBodyHandlers.ofJenaGraph()).toCompletableFuture().join()
+ );
+
+ final ClientHttpException httpException = (ClientHttpException) completionException.getCause();
+
+ assertEquals(429, httpException.getProblemDetails().getStatus());
+ assertEquals("Too Many Requests", httpException.getProblemDetails().getTitle());
+ assertEquals("Some details", httpException.getProblemDetails().getDetails());
+ assertEquals("https://example.org/type", httpException.getProblemDetails().getType().toString());
+ assertEquals("https://example.org/instance", httpException.getProblemDetails().getInstance().toString());
+ }
}
diff --git a/test/src/main/java/com/inrupt/client/test/RdfMockService.java b/test/src/main/java/com/inrupt/client/test/RdfMockService.java
index 768a2ba1aec..7814e34ca73 100644
--- a/test/src/main/java/com/inrupt/client/test/RdfMockService.java
+++ b/test/src/main/java/com/inrupt/client/test/RdfMockService.java
@@ -24,6 +24,7 @@
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
+import com.inrupt.client.ProblemDetails;
import java.util.Collections;
import java.util.Map;
@@ -33,6 +34,8 @@
*/
public class RdfMockService {
+ private static final String CONTENT_TYPE = "Content-Type";
+
private final WireMockServer wireMockServer;
public RdfMockService() {
@@ -49,30 +52,41 @@ private void setupMocks() {
wireMockServer.stubFor(get(urlEqualTo("/oneTriple"))
.willReturn(aResponse()
.withStatus(200)
- .withHeader("Content-Type", "text/turtle")
+ .withHeader(CONTENT_TYPE, "text/turtle")
.withBody(" .")));
wireMockServer.stubFor(post(urlEqualTo("/postOneTriple"))
.withRequestBody(matching(
".*\\s+" +
"\\s+\"object\"\\s+\\..*"))
- .withHeader("Content-Type", containing("text/turtle"))
+ .withHeader(CONTENT_TYPE, containing("text/turtle"))
.willReturn(aResponse()
.withStatus(204)));
wireMockServer.stubFor(get(urlEqualTo("/example"))
.willReturn(aResponse()
.withStatus(200)
- .withHeader("Content-Type", "text/turtle")
+ .withHeader(CONTENT_TYPE, "text/turtle")
.withBody(getExampleTTL())));
wireMockServer.stubFor(patch(urlEqualTo("/sparqlUpdate"))
- .withHeader("Content-Type", containing("application/sparql-update"))
+ .withHeader(CONTENT_TYPE, containing("application/sparql-update"))
.withRequestBody(matching(
"INSERT DATA\\s+\\{\\s*\\s+" +
"\\s+\\s*\\.\\s*\\}\\s*"))
.willReturn(aResponse()
.withStatus(204)));
+
+ wireMockServer.stubFor(get(urlEqualTo("/error"))
+ .willReturn(aResponse()
+ .withStatus(429)
+ .withHeader(CONTENT_TYPE, ProblemDetails.MIME_TYPE)
+ .withBody("{" +
+ "\"title\":\"Too Many Requests\"," +
+ "\"status\":429," +
+ "\"details\":\"Some details\"," +
+ "\"instance\":\"https://example.org/instance\"," +
+ "\"type\":\"https://example.org/type\"}")));
}
private String getExampleTTL() {