Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
39a699f
JCL-403: SolidClient builds ProblemDetails
NSeydoux Apr 10, 2024
162e37b
fixup! JCL-403: SolidClient builds ProblemDetails
NSeydoux Apr 16, 2024
983a474
Specify utf-8 encoding
NSeydoux Apr 16, 2024
ea661c2
Use throwing body mapper
NSeydoux Apr 16, 2024
4cec943
Lint
NSeydoux Apr 16, 2024
955214a
Provide exception mapper to throwing handler
NSeydoux Apr 17, 2024
fac8471
Remove unnecessary SolidClientException::handle
NSeydoux Apr 17, 2024
b9572ec
Remove default status message tests
NSeydoux Apr 17, 2024
3c26a2c
Specify encoding
NSeydoux Apr 18, 2024
7c6a31c
Remove FIXME
NSeydoux Apr 18, 2024
662ac78
Remove PII from error message
NSeydoux Apr 18, 2024
7c5e922
Lift `isSuccess` to api module
NSeydoux Apr 18, 2024
17128b9
Remove PII from read error message
NSeydoux Apr 19, 2024
6f3012a
Add missing Javadoc entry
NSeydoux Apr 19, 2024
e009dc1
fixup! Add missing Javadoc entry
NSeydoux Apr 19, 2024
ab5dd2e
fixup! fixup! Add missing Javadoc entry
NSeydoux Apr 19, 2024
1bb756b
fixup! fixup! fixup! Add missing Javadoc entry
NSeydoux Apr 19, 2024
5ce7371
Replace Arguments.of with Arguments.arguments
NSeydoux Apr 19, 2024
be110db
Improve readability of unit tests
NSeydoux Apr 19, 2024
05b9f0e
fixup! Improve readability of unit tests
NSeydoux Apr 19, 2024
febe60b
Remove throwing body handler
NSeydoux Apr 22, 2024
be04d54
Remove throwOnError from solid module
NSeydoux Apr 22, 2024
20675d9
Lint
NSeydoux Apr 22, 2024
9e0967d
Use try-with-resource
NSeydoux Apr 23, 2024
ea2d7a6
Lint
NSeydoux Apr 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions api/src/main/java/com/inrupt/client/ProblemDetails.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,13 @@
* @see <a href="https://www.rfc-editor.org/rfc/rfc9457">RFC 9457 Problem Details for HTTP APIs</a>
*/
public class ProblemDetails {
/**
* The <a href="https://www.rfc-editor.org/rfc/rfc9457">RFC9457</a> default MIME type.
*/
public static final String MIME_TYPE = "application/problem+json";
/**
* The <a href="https://www.rfc-editor.org/rfc/rfc9457">RFC9457</a> default problem type.
*/
public static final String DEFAULT_TYPE = "about:blank";
private final URI type;
private final String title;
Expand All @@ -44,6 +50,15 @@ public class ProblemDetails {
private static JsonService jsonService;
private static boolean isJsonServiceInitialized;

/**
* Build a ProblemDetails instance providing the expected fields as described in
* <a href="https://www.rfc-editor.org/rfc/rfc9457">RFC9457</a>.
* @param type the problem type
* @param title the problem title
* @param details the problem details
* @param status the error response status code
* @param instance the problem instance
*/
public ProblemDetails(
final URI type,
final String title,
Expand All @@ -58,22 +73,42 @@ public ProblemDetails(
this.instance = instance;
}

/**
* The problem type.
* @return the type
*/
public URI getType() {
return this.type;
};

/**
* The problem title.
* @return the title
*/
public String getTitle() {
return this.title;
};

/**
* The problem details.
* @return the details
*/
public String getDetails() {
return this.details;
};

/**
* The problem status code.
* @return the status code
*/
public int getStatus() {
return this.status;
};

/**
* The problem instance.
* @return the instance
*/
public URI getInstance() {
return this.instance;
};
Expand All @@ -82,10 +117,25 @@ public URI getInstance() {
* This inner class is only ever used for JSON deserialization. Please do not use in any other context.
*/
public static class Data {
/**
* The problem type.
*/
public URI type;
/**
* The problem title.
*/
public String title;
/**
* The problem details.
*/
public String details;
/**
* The problem status code.
*/
public int status;
/**
* The problem instance.
*/
public URI instance;
}

Expand All @@ -104,6 +154,13 @@ private static JsonService getJsonService() {
return ProblemDetails.jsonService;
}

/**
* Builds a {@link ProblemDetails} instance from an HTTP error response.
* @param statusCode the HTTP error response status code
* @param headers the HTTP error response headers
* @param body the HTTP error response body
* @return a {@link ProblemDetails} instance
*/
public static ProblemDetails fromErrorResponse(
final int statusCode,
final Headers headers,
Expand Down
56 changes: 9 additions & 47 deletions api/src/main/java/com/inrupt/client/Response.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@
import java.io.InputStream;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.function.Function;

/**
* An HTTP Response.
Expand Down Expand Up @@ -97,6 +95,15 @@ interface ResponseInfo {
ByteBuffer body();
}

/**
* Indicates whether a status code reflects a successful HTTP response.
* @param statusCode the HTTP response status code
* @return true if the status code is in the success range, namely [200, 299].
*/
static boolean isSuccess(final int statusCode) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this method?

Copy link
Contributor Author

@NSeydoux NSeydoux Apr 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It avoids some duplication, and my impression was that it is a quite common pattern to have this affordance on HTTP response object: JS Response has a .ok getter, and OkHttp has isSuccessful. I'm not strongly opposed to removing this, but I find it reads slightly better than checking the status range each time.

return statusCode >= 200 && statusCode < 300;
}

/**
* An interface for mapping an HTTP response into a specific Java type.
* @param <T> the body type
Expand Down Expand Up @@ -154,51 +161,6 @@ public static BodyHandler<Void> discarding() {
return responseInfo -> null;
}

/**
* Throws on HTTP error using the provided mapper, or apply the provided body handler.
* @param handler the body handler to apply on non-error HTTP responses
* @param isSuccess a callback determining error cases
* @param exceptionMapper the exception mapper
* @return the body handler
* @param <T> the type of the body handler
*/
public static <T> Response.BodyHandler<T> throwOnError(
final Response.BodyHandler<T> handler,
final Function<Response.ResponseInfo, Boolean> isSuccess,
final Function<Response.ResponseInfo, ClientHttpException> exceptionMapper
) {
return responseinfo -> {
if (!isSuccess.apply(responseinfo)) {
throw exceptionMapper.apply(responseinfo);
}
return handler.apply(responseinfo);
};
}

/**
* Throws on HTTP error, or apply the provided body handler.
* @param handler the body handler to apply on non-error HTTP responses
* @param isSuccess a callback determining error cases
* @return the body handler
* @param <T> the type of the body handler
*/
public static <T> Response.BodyHandler<T> throwOnError(
final Response.BodyHandler<T> handler,
final Function<Response.ResponseInfo, Boolean> isSuccess
) {
final Function<Response.ResponseInfo, ClientHttpException> defaultMapper = responseInfo ->
new ClientHttpException(
"An HTTP error has been returned, with status code " + responseInfo.statusCode(),
responseInfo.uri(),
responseInfo.statusCode(),
responseInfo.headers(),
new String(responseInfo.body().array(), StandardCharsets.UTF_8)
);
return throwOnError(handler, isSuccess, defaultMapper);
}



private BodyHandlers() {
// Prevent instantiation
}
Expand Down
111 changes: 59 additions & 52 deletions solid/src/main/java/com/inrupt/client/solid/SolidClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,14 @@
*/
package com.inrupt.client.solid;

import static java.nio.charset.StandardCharsets.UTF_8;

import com.inrupt.client.Client;
import com.inrupt.client.ClientProvider;
import com.inrupt.client.Headers;
import com.inrupt.client.RDFSource;
import com.inrupt.client.Request;
import com.inrupt.client.Resource;
import com.inrupt.client.Response;
import com.inrupt.client.ValidationResult;
import com.inrupt.client.*;
import com.inrupt.client.auth.Session;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -131,36 +123,43 @@ public <T extends Resource> CompletionStage<T> read(final URI identifier, final
headers.firstValue(USER_AGENT).ifPresent(agent -> builder.setHeader(USER_AGENT, agent));

final Request request = builder.build();
return client.send(request, Response.BodyHandlers.ofByteArray())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a large change, but essentially the error branch of the initial if has been moved to the wrapping throwOnError

.thenApply(response -> {
if (response.statusCode() >= ERROR_STATUS) {
throw SolidClientException.handle("Unable to read resource at " + request.uri(), request.uri(),
response.statusCode(), response.headers(), new String(response.body()));
} else {
final String contentType = response.headers().firstValue(CONTENT_TYPE)
.orElse("application/octet-stream");
try {
// Check that this is an RDFSoure
if (RDFSource.class.isAssignableFrom(clazz)) {
final Dataset dataset = SolidResourceHandlers.buildDataset(contentType, response.body(),
request.uri().toString()).orElse(null);
final T obj = construct(request.uri(), clazz, dataset, response.headers());
final ValidationResult res = RDFSource.class.cast(obj).validate();
if (!res.isValid()) {
throw new DataMappingException(
"Unable to map resource into type: [" + clazz.getSimpleName() + "] ",
res.getResults());
}
return obj;
// Otherwise, create a non-RDF-bearing resource
} else {
return construct(request.uri(), clazz, contentType,
new ByteArrayInputStream(response.body()), response.headers());
return client.send(
request,
Response.BodyHandlers.ofByteArray()
).thenApply(response -> {
if (!Response.isSuccess(response.statusCode())) {
throw SolidClientException.handle(
"Reading resource failed.",
response.uri(),
response.statusCode(),
response.headers(),
new String(response.body(), StandardCharsets.UTF_8)
);
}

final String contentType = response.headers().firstValue(CONTENT_TYPE)
.orElse("application/octet-stream");
try {
// Check that this is an RDFSoure
if (RDFSource.class.isAssignableFrom(clazz)) {
final Dataset dataset = SolidResourceHandlers.buildDataset(contentType, response.body(),
request.uri().toString()).orElse(null);
final T obj = construct(request.uri(), clazz, dataset, response.headers());
final ValidationResult res = RDFSource.class.cast(obj).validate();
if (!res.isValid()) {
throw new DataMappingException(
"Unable to map resource into type: [" + clazz.getSimpleName() + "] ",
res.getResults());
}
} catch (final ReflectiveOperationException ex) {
throw new SolidResourceException("Unable to read resource into type " + clazz.getName(),
ex);
return obj;
// Otherwise, create a non-RDF-bearing resource
} else {
return construct(request.uri(), clazz, contentType,
new ByteArrayInputStream(response.body()), response.headers());
}
} catch (final ReflectiveOperationException ex) {
throw new SolidResourceException("Unable to read resource into type " + clazz.getName(),
ex);
}
});
}
Expand Down Expand Up @@ -280,13 +279,21 @@ public <T extends Resource> CompletionStage<Void> delete(final T resource, final
defaultHeaders.firstValue(USER_AGENT).ifPresent(agent -> builder.setHeader(USER_AGENT, agent));
headers.firstValue(USER_AGENT).ifPresent(agent -> builder.setHeader(USER_AGENT, agent));

return client.send(builder.build(), Response.BodyHandlers.ofByteArray()).thenApply(res -> {
if (isSuccess(res.statusCode())) {
return null;
} else {
throw SolidClientException.handle("Unable to delete resource", resource.getIdentifier(),
res.statusCode(), res.headers(), new String(res.body(), UTF_8));

return client.send(
builder.build(),
Response.BodyHandlers.ofByteArray()
).thenApply(response -> {
if (!Response.isSuccess(response.statusCode())) {
throw SolidClientException.handle(
"Deleting resource failed.",
response.uri(),
response.statusCode(),
response.headers(),
new String(response.body(), StandardCharsets.UTF_8)
);
}
return null;
});
}

Expand Down Expand Up @@ -370,9 +377,14 @@ public SolidClient build() {
<T extends Resource> Function<Response<byte[]>, CompletionStage<T>> handleResponse(final T resource,
final Headers headers, final String message) {
return res -> {
if (!isSuccess(res.statusCode())) {
throw SolidClientException.handle(message, resource.getIdentifier(),
res.statusCode(), res.headers(), new String(res.body(), UTF_8));
if (!Response.isSuccess(res.statusCode())) {
throw SolidClientException.handle(
message,
resource.getIdentifier(),
res.statusCode(),
res.headers(),
new String(res.body(), StandardCharsets.UTF_8)
);
}

if (!fetchAfterWrite) {
Expand All @@ -382,7 +394,6 @@ <T extends Resource> Function<Response<byte[]>, CompletionStage<T>> handleRespon
@SuppressWarnings("unchecked")
final Class<T> clazz = (Class<T>) resource.getClass();
return read(resource.getIdentifier(), headers, clazz);

};
}

Expand Down Expand Up @@ -445,10 +456,6 @@ static void decorateHeaders(final Request.Builder builder, final Headers headers
}
}

static boolean isSuccess(final int statusCode) {
return statusCode >= 200 && statusCode < 300;
}

static Request.BodyPublisher cast(final Resource resource) {
try {
return Request.BodyPublishers.ofInputStream(resource.getEntity());
Expand Down
Loading