Skip to content

Commit

Permalink
JCL-402: HTTP error handling in JenaBodyHandlers (#1159)
Browse files Browse the repository at this point in the history
This deprecates the current methods in `JenaBodyHandlers`, and replaces them with similar method throwing an appropriate exception with error details on HTTP error instead of returning an empty dataset. The new method now have `Jena` in their name for this not to be a breaking change: `ofModel` becomes `ofJenaModel`, etc.
  • Loading branch information
NSeydoux authored Apr 23, 2024
1 parent 27dde17 commit 89534b4
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 42 deletions.
6 changes: 6 additions & 0 deletions jena/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@
<version>${slf4j.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.inrupt.client</groupId>
<artifactId>inrupt-client-jackson</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock</artifactId>
Expand Down
109 changes: 87 additions & 22 deletions jena/src/main/java/com/inrupt/client/jena/JenaBodyHandlers.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Model> 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();
Expand All @@ -65,12 +74,29 @@ public static Response.BodyHandler<Model> 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<Graph> ofGraph() {
return responseInfo -> responseInfo.headers().firstValue(CONTENT_TYPE)
public static Response.BodyHandler<Model> ofModel() {
return JenaBodyHandlers::responseToModel;
}

/**
* Populate a Jena {@link Model} with an HTTP response body.
*
* @return an HTTP body handler
*/
public static Response.BodyHandler<Model> 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();
Expand All @@ -84,24 +110,63 @@ public static Response.BodyHandler<Graph> 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<Graph> ofGraph() {
return JenaBodyHandlers::responseToGraph;
}

/**
* Populate a Jena {@link Graph} with an HTTP response.
*
* @return an HTTP body handler
*/
public static Response.BodyHandler<Graph> 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<Dataset> 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<Dataset> ofJenaDataset() {
return responseInfo -> {
JenaBodyHandlers.throwOnError(responseInfo);
return JenaBodyHandlers.responseToDataset(responseInfo);
};
}

static Lang toJenaLang(final String mediaType) {
Expand Down
100 changes: 84 additions & 16 deletions jena/src/test/java/com/inrupt/client/jena/JenaBodyHandlersTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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());
Expand All @@ -78,15 +80,15 @@ 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"))
.header("Accept", "text/turtle")
.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);
Expand All @@ -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());
Expand All @@ -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());
Expand All @@ -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());
Expand All @@ -163,15 +187,37 @@ 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"))
.header("Accept", "text/turtle")
.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);
Expand All @@ -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());
Expand All @@ -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());
Expand All @@ -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());
}
}
Loading

0 comments on commit 89534b4

Please sign in to comment.