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() {