Skip to content

Commit 89534b4

Browse files
authored
JCL-402: HTTP error handling in JenaBodyHandlers (#1159)
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.
1 parent 27dde17 commit 89534b4

File tree

4 files changed

+195
-42
lines changed

4 files changed

+195
-42
lines changed

jena/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@
6767
<version>${slf4j.version}</version>
6868
<scope>test</scope>
6969
</dependency>
70+
<dependency>
71+
<groupId>com.inrupt.client</groupId>
72+
<artifactId>inrupt-client-jackson</artifactId>
73+
<version>${project.version}</version>
74+
<scope>test</scope>
75+
</dependency>
7076
<dependency>
7177
<groupId>org.wiremock</groupId>
7278
<artifactId>wiremock</artifactId>

jena/src/main/java/com/inrupt/client/jena/JenaBodyHandlers.java

Lines changed: 87 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@
2020
*/
2121
package com.inrupt.client.jena;
2222

23+
import com.inrupt.client.ClientHttpException;
2324
import com.inrupt.client.Response;
2425

2526
import java.io.ByteArrayInputStream;
2627
import java.io.IOException;
2728
import java.io.UncheckedIOException;
29+
import java.nio.charset.StandardCharsets;
2830

2931
import org.apache.jena.atlas.web.ContentType;
3032
import org.apache.jena.graph.Graph;
@@ -44,13 +46,20 @@ public final class JenaBodyHandlers {
4446

4547
private static final String CONTENT_TYPE = "Content-Type";
4648

47-
/**
48-
* Populate a Jena {@link Model} with an HTTP response body.
49-
*
50-
* @return an HTTP body handler
51-
*/
52-
public static Response.BodyHandler<Model> ofModel() {
53-
return responseInfo -> responseInfo.headers().firstValue(CONTENT_TYPE)
49+
private static void throwOnError(final Response.ResponseInfo responseInfo) {
50+
if (!Response.isSuccess(responseInfo.statusCode())) {
51+
throw new ClientHttpException(
52+
"Could not map to a Jena entity.",
53+
responseInfo.uri(),
54+
responseInfo.statusCode(),
55+
responseInfo.headers(),
56+
new String(responseInfo.body().array(), StandardCharsets.UTF_8)
57+
);
58+
}
59+
}
60+
61+
private static Model responseToModel(final Response.ResponseInfo responseInfo) {
62+
return responseInfo.headers().firstValue(CONTENT_TYPE)
5463
.map(JenaBodyHandlers::toJenaLang).map(lang -> {
5564
try (final var input = new ByteArrayInputStream(responseInfo.body().array())) {
5665
final var model = ModelFactory.createDefaultModel();
@@ -65,12 +74,29 @@ public static Response.BodyHandler<Model> ofModel() {
6574
}
6675

6776
/**
68-
* Populate a Jena {@link Graph} with an HTTP response.
77+
* Populate a Jena {@link Model} with an HTTP response body.
6978
*
7079
* @return an HTTP body handler
80+
* @deprecated Use {@link JenaBodyHandlers#ofJenaModel()} instead for consistent HTTP error handling.
7181
*/
72-
public static Response.BodyHandler<Graph> ofGraph() {
73-
return responseInfo -> responseInfo.headers().firstValue(CONTENT_TYPE)
82+
public static Response.BodyHandler<Model> ofModel() {
83+
return JenaBodyHandlers::responseToModel;
84+
}
85+
86+
/**
87+
* Populate a Jena {@link Model} with an HTTP response body.
88+
*
89+
* @return an HTTP body handler
90+
*/
91+
public static Response.BodyHandler<Model> ofJenaModel() {
92+
return responseInfo -> {
93+
JenaBodyHandlers.throwOnError(responseInfo);
94+
return JenaBodyHandlers.responseToModel(responseInfo);
95+
};
96+
}
97+
98+
private static Graph responseToGraph(final Response.ResponseInfo responseInfo) {
99+
return responseInfo.headers().firstValue(CONTENT_TYPE)
74100
.map(JenaBodyHandlers::toJenaLang).map(lang -> {
75101
try (final var input = new ByteArrayInputStream(responseInfo.body().array())) {
76102
final var graph = GraphMemFactory.createDefaultGraph();
@@ -84,24 +110,63 @@ public static Response.BodyHandler<Graph> ofGraph() {
84110
.orElseGet(GraphMemFactory::createDefaultGraph);
85111
}
86112

113+
/**
114+
* Populate a Jena {@link Graph} with an HTTP response.
115+
*
116+
* @return an HTTP body handler
117+
* @deprecated Use {@link JenaBodyHandlers#ofJenaGraph} instead for consistent HTTP error handling.
118+
*/
119+
public static Response.BodyHandler<Graph> ofGraph() {
120+
return JenaBodyHandlers::responseToGraph;
121+
}
122+
123+
/**
124+
* Populate a Jena {@link Graph} with an HTTP response.
125+
*
126+
* @return an HTTP body handler
127+
*/
128+
public static Response.BodyHandler<Graph> ofJenaGraph() {
129+
return responseInfo -> {
130+
JenaBodyHandlers.throwOnError(responseInfo);
131+
return JenaBodyHandlers.responseToGraph(responseInfo);
132+
};
133+
}
134+
135+
private static Dataset responseToDataset(final Response.ResponseInfo responseInfo) {
136+
return responseInfo.headers().firstValue(CONTENT_TYPE)
137+
.map(JenaBodyHandlers::toJenaLang).map(lang -> {
138+
try (final var input = new ByteArrayInputStream(responseInfo.body().array())) {
139+
final var dataset = DatasetFactory.create();
140+
RDFDataMgr.read(dataset, input, responseInfo.uri().toString(), lang);
141+
return dataset;
142+
} catch (final IOException ex) {
143+
throw new UncheckedIOException(
144+
"An I/O error occurred while data was read from the InputStream into a Dataset", ex);
145+
}
146+
})
147+
.orElseGet(DatasetFactory::create);
148+
}
149+
87150
/**
88151
* Populate a Jena {@link Dataset} with an HTTP response.
89152
*
90153
* @return an HTTP body handler
154+
* @deprecated Use {@link JenaBodyHandlers#ofJenaDataset} instead for consistent HTTP error handling.
91155
*/
92156
public static Response.BodyHandler<Dataset> ofDataset() {
93-
return responseInfo -> responseInfo.headers().firstValue(CONTENT_TYPE)
94-
.map(JenaBodyHandlers::toJenaLang).map(lang -> {
95-
try (final var input = new ByteArrayInputStream(responseInfo.body().array())) {
96-
final var dataset = DatasetFactory.create();
97-
RDFDataMgr.read(dataset, input, responseInfo.uri().toString(), lang);
98-
return dataset;
99-
} catch (final IOException ex) {
100-
throw new UncheckedIOException(
101-
"An I/O error occurred while data was read from the InputStream into a Dataset", ex);
102-
}
103-
})
104-
.orElseGet(DatasetFactory::create);
157+
return JenaBodyHandlers::responseToDataset;
158+
}
159+
160+
/**
161+
* Populate a Jena {@link Dataset} with an HTTP response.
162+
*
163+
* @return an HTTP body handler
164+
*/
165+
public static Response.BodyHandler<Dataset> ofJenaDataset() {
166+
return responseInfo -> {
167+
JenaBodyHandlers.throwOnError(responseInfo);
168+
return JenaBodyHandlers.responseToDataset(responseInfo);
169+
};
105170
}
106171

107172
static Lang toJenaLang(final String mediaType) {

jena/src/test/java/com/inrupt/client/jena/JenaBodyHandlersTest.java

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import static org.junit.jupiter.api.Assertions.*;
2424

25+
import com.inrupt.client.ClientHttpException;
2526
import com.inrupt.client.Request;
2627
import com.inrupt.client.Response;
2728
import com.inrupt.client.spi.HttpService;
@@ -32,6 +33,7 @@
3233
import java.net.URI;
3334
import java.util.HashMap;
3435
import java.util.Map;
36+
import java.util.concurrent.CompletionException;
3537
import java.util.concurrent.ExecutionException;
3638

3739
import org.apache.jena.graph.NodeFactory;
@@ -57,14 +59,14 @@ static void teardown() {
5759
}
5860

5961
@Test
60-
void testOfModelHandler() throws IOException,
62+
void testOfJenaModelHandler() throws IOException,
6163
InterruptedException {
6264
final Request request = Request.newBuilder()
6365
.uri(URI.create(config.get("rdf_uri") + "/oneTriple"))
6466
.GET()
6567
.build();
6668

67-
final var response = client.send(request, JenaBodyHandlers.ofModel())
69+
final var response = client.send(request, JenaBodyHandlers.ofJenaModel())
6870
.toCompletableFuture().join();
6971

7072
assertEquals(200, response.statusCode());
@@ -78,15 +80,15 @@ void testOfModelHandler() throws IOException,
7880
}
7981

8082
@Test
81-
void testOfModelHandlerAsync() throws IOException,
83+
void testOfJenaModelHandlerAsync() throws IOException,
8284
InterruptedException, ExecutionException {
8385
final Request request = Request.newBuilder()
8486
.uri(URI.create(config.get("rdf_uri") + "/oneTriple"))
8587
.header("Accept", "text/turtle")
8688
.GET()
8789
.build();
8890

89-
final var asyncResponse = client.send(request, JenaBodyHandlers.ofModel());
91+
final var asyncResponse = client.send(request, JenaBodyHandlers.ofJenaModel());
9092

9193
final int statusCode = asyncResponse.thenApply(Response::statusCode).toCompletableFuture().join();
9294
assertEquals(200, statusCode);
@@ -101,13 +103,13 @@ void testOfModelHandlerAsync() throws IOException,
101103
}
102104

103105
@Test
104-
void testOfModelHandlerWithURL() throws IOException, InterruptedException {
106+
void testOfJenaModelHandlerWithURL() throws IOException, InterruptedException {
105107
final Request request = Request.newBuilder()
106108
.uri(URI.create(config.get("rdf_uri") + "/example"))
107109
.GET()
108110
.build();
109111

110-
final var response = client.send(request, JenaBodyHandlers.ofModel())
112+
final var response = client.send(request, JenaBodyHandlers.ofJenaModel())
111113
.toCompletableFuture().join();
112114

113115
assertEquals(200, response.statusCode());
@@ -120,14 +122,36 @@ void testOfModelHandlerWithURL() throws IOException, InterruptedException {
120122
}
121123

122124
@Test
123-
void testOfDatasetHandler() throws IOException,
125+
void testOfJenaModelHandlerError() throws IOException,
126+
InterruptedException {
127+
final Request request = Request.newBuilder()
128+
.uri(URI.create(config.get("rdf_uri") + "/error"))
129+
.GET()
130+
.build();
131+
132+
final CompletionException completionException = assertThrows(
133+
CompletionException.class,
134+
() -> client.send(request, JenaBodyHandlers.ofJenaModel()).toCompletableFuture().join()
135+
);
136+
137+
final ClientHttpException httpException = (ClientHttpException) completionException.getCause();
138+
139+
assertEquals(429, httpException.getProblemDetails().getStatus());
140+
assertEquals("Too Many Requests", httpException.getProblemDetails().getTitle());
141+
assertEquals("Some details", httpException.getProblemDetails().getDetails());
142+
assertEquals("https://example.org/type", httpException.getProblemDetails().getType().toString());
143+
assertEquals("https://example.org/instance", httpException.getProblemDetails().getInstance().toString());
144+
}
145+
146+
@Test
147+
void testOfJenaDatasetHandler() throws IOException,
124148
InterruptedException {
125149
final Request request = Request.newBuilder()
126150
.uri(URI.create(config.get("rdf_uri") + "/oneTriple"))
127151
.GET()
128152
.build();
129153

130-
final var response = client.send(request, JenaBodyHandlers.ofDataset())
154+
final var response = client.send(request, JenaBodyHandlers.ofJenaDataset())
131155
.toCompletableFuture().join();
132156

133157
assertEquals(200, response.statusCode());
@@ -142,13 +166,13 @@ void testOfDatasetHandler() throws IOException,
142166
}
143167

144168
@Test
145-
void testOfDatasetHandlerWithURL() throws IOException, InterruptedException {
169+
void testOfJenaDatasetHandlerWithURL() throws IOException, InterruptedException {
146170
final Request request = Request.newBuilder()
147171
.uri(URI.create(config.get("rdf_uri") + "/example"))
148172
.GET()
149173
.build();
150174

151-
final var response = client.send(request, JenaBodyHandlers.ofDataset())
175+
final var response = client.send(request, JenaBodyHandlers.ofJenaDataset())
152176
.toCompletableFuture().join();
153177

154178
assertEquals(200, response.statusCode());
@@ -163,15 +187,37 @@ void testOfDatasetHandlerWithURL() throws IOException, InterruptedException {
163187
}
164188

165189
@Test
166-
void testOfGraphHandlerAsync() throws IOException,
190+
void testOfJenaDatasetHandlerError() throws IOException,
191+
InterruptedException {
192+
final Request request = Request.newBuilder()
193+
.uri(URI.create(config.get("rdf_uri") + "/error"))
194+
.GET()
195+
.build();
196+
197+
final CompletionException completionException = assertThrows(
198+
CompletionException.class,
199+
() -> client.send(request, JenaBodyHandlers.ofJenaDataset()).toCompletableFuture().join()
200+
);
201+
202+
final ClientHttpException httpException = (ClientHttpException) completionException.getCause();
203+
204+
assertEquals(429, httpException.getProblemDetails().getStatus());
205+
assertEquals("Too Many Requests", httpException.getProblemDetails().getTitle());
206+
assertEquals("Some details", httpException.getProblemDetails().getDetails());
207+
assertEquals("https://example.org/type", httpException.getProblemDetails().getType().toString());
208+
assertEquals("https://example.org/instance", httpException.getProblemDetails().getInstance().toString());
209+
}
210+
211+
@Test
212+
void testOfJenaGraphHandlerAsync() throws IOException,
167213
InterruptedException, ExecutionException {
168214
final Request request = Request.newBuilder()
169215
.uri(URI.create(config.get("rdf_uri") + "/oneTriple"))
170216
.header("Accept", "text/turtle")
171217
.GET()
172218
.build();
173219

174-
final var asyncResponse = client.send(request, JenaBodyHandlers.ofGraph());
220+
final var asyncResponse = client.send(request, JenaBodyHandlers.ofJenaGraph());
175221

176222
final int statusCode = asyncResponse.thenApply(Response::statusCode).toCompletableFuture().join();
177223
assertEquals(200, statusCode);
@@ -186,14 +232,14 @@ void testOfGraphHandlerAsync() throws IOException,
186232
}
187233

188234
@Test
189-
void testOfGraphHandler() throws IOException,
235+
void testOfJenaGraphHandler() throws IOException,
190236
InterruptedException {
191237
final Request request = Request.newBuilder()
192238
.uri(URI.create(config.get("rdf_uri") + "/oneTriple"))
193239
.GET()
194240
.build();
195241

196-
final var response = client.send(request, JenaBodyHandlers.ofGraph())
242+
final var response = client.send(request, JenaBodyHandlers.ofJenaGraph())
197243
.toCompletableFuture().join();
198244

199245
assertEquals(200, response.statusCode());
@@ -207,13 +253,13 @@ void testOfGraphHandler() throws IOException,
207253
}
208254

209255
@Test
210-
void testOfGraphHandlerWithURL() throws IOException, InterruptedException {
256+
void testOfJenaGraphHandlerWithURL() throws IOException, InterruptedException {
211257
final Request request = Request.newBuilder()
212258
.uri(URI.create(config.get("rdf_uri") + "/example"))
213259
.GET()
214260
.build();
215261

216-
final var response = client.send(request, JenaBodyHandlers.ofGraph())
262+
final var response = client.send(request, JenaBodyHandlers.ofJenaGraph())
217263
.toCompletableFuture().join();
218264

219265
assertEquals(200, response.statusCode());
@@ -225,4 +271,26 @@ void testOfGraphHandlerWithURL() throws IOException, InterruptedException {
225271
null)
226272
);
227273
}
274+
275+
@Test
276+
void testOfJenaGraphHandlerError() throws IOException,
277+
InterruptedException {
278+
final Request request = Request.newBuilder()
279+
.uri(URI.create(config.get("rdf_uri") + "/error"))
280+
.GET()
281+
.build();
282+
283+
final CompletionException completionException = assertThrows(
284+
CompletionException.class,
285+
() -> client.send(request, JenaBodyHandlers.ofJenaGraph()).toCompletableFuture().join()
286+
);
287+
288+
final ClientHttpException httpException = (ClientHttpException) completionException.getCause();
289+
290+
assertEquals(429, httpException.getProblemDetails().getStatus());
291+
assertEquals("Too Many Requests", httpException.getProblemDetails().getTitle());
292+
assertEquals("Some details", httpException.getProblemDetails().getDetails());
293+
assertEquals("https://example.org/type", httpException.getProblemDetails().getType().toString());
294+
assertEquals("https://example.org/instance", httpException.getProblemDetails().getInstance().toString());
295+
}
228296
}

0 commit comments

Comments
 (0)