Skip to content

Commit 16a6143

Browse files
committed
Data classes for HTTP Problem Details support
Initial ClientHttpException class Create HTTP status enum Add some tests Add javadoc Refactor HttpStatus Add specialized builder to ProblemDetails Add missing header Make problem details parsing more resilient Add tests for problem details parsing The tests are in the solid-client module, because it will need the additional json service anyways in the test dependencies for testing the rest of this feature, and putting these tests in the api module creates a circular dependency. License header
1 parent 4eb74d7 commit 16a6143

File tree

6 files changed

+499
-0
lines changed

6 files changed

+499
-0
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright Inrupt Inc.
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal in
6+
* the Software without restriction, including without limitation the rights to use,
7+
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
8+
* Software, and to permit persons to whom the Software is furnished to do so,
9+
* subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15+
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
16+
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
17+
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
18+
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
19+
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
package com.inrupt.client;
22+
23+
/**
24+
* A runtime exception representing an HTTP error response carrying a structured representation of the problem. The
25+
* problem description is embedded in a {@link ProblemDetails} instance.
26+
*/
27+
public class ClientHttpException extends InruptClientException {
28+
private final ProblemDetails problemDetails;
29+
30+
/**
31+
* Create a ClientHttpException.
32+
* @param problemDetails the {@link ProblemDetails} instance
33+
* @param message the exception message
34+
*/
35+
public ClientHttpException(final ProblemDetails problemDetails, final String message) {
36+
super(message);
37+
this.problemDetails = problemDetails;
38+
}
39+
40+
/**
41+
* Create a ClientHttpException.
42+
* @param problemDetails the {@link ProblemDetails} instance
43+
* @param message the exception message
44+
* @param cause a wrapped exception cause
45+
*/
46+
public ClientHttpException(final ProblemDetails problemDetails, final String message, final Exception cause) {
47+
super(message, cause);
48+
this.problemDetails = problemDetails;
49+
}
50+
51+
public ProblemDetails getProblemDetails() {
52+
return this.problemDetails;
53+
}
54+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright Inrupt Inc.
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal in
6+
* the Software without restriction, including without limitation the rights to use,
7+
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
8+
* Software, and to permit persons to whom the Software is furnished to do so,
9+
* subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15+
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
16+
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
17+
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
18+
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
19+
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
package com.inrupt.client;
22+
23+
import java.util.Arrays;
24+
25+
public final class HttpStatus {
26+
27+
public static final int BAD_REQUEST = 400;
28+
public static final int UNAUTHORIZED = 401;
29+
public static final int FORBIDDEN = 403;
30+
public static final int NOT_FOUND = 404;
31+
public static final int METHOD_NOT_ALLOWED = 405;
32+
public static final int NOT_ACCEPTABLE = 406;
33+
public static final int CONFLICT = 409;
34+
public static final int GONE = 410;
35+
public static final int PRECONDITION_FAILED = 412;
36+
public static final int UNSUPPORTED_MEDIA_TYPE = 415;
37+
public static final int TOO_MANY_REQUESTS = 429;
38+
public static final int INTERNAL_SERVER_ERROR = 500;
39+
40+
enum StatusMessages {
41+
BAD_REQUEST(HttpStatus.BAD_REQUEST, "Bad Request"),
42+
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "Unauthorized"),
43+
FORBIDDEN(HttpStatus.FORBIDDEN, "Forbidden"),
44+
NOT_FOUND(HttpStatus.NOT_FOUND, "Not Found"),
45+
METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "Method Not Allowed"),
46+
NOT_ACCEPTABLE(HttpStatus.NOT_ACCEPTABLE, "Not Acceptable"),
47+
CONFLICT(HttpStatus.CONFLICT, "Conflict"),
48+
GONE(HttpStatus.GONE, "Gone"),
49+
PRECONDITION_FAILED(HttpStatus.PRECONDITION_FAILED, "Precondition Failed"),
50+
UNSUPPORTED_MEDIA_TYPE(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "Unsupported Media Type"),
51+
TOO_MANY_REQUESTS(HttpStatus.TOO_MANY_REQUESTS, "Too Many Requests"),
52+
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal Server Error");
53+
54+
private final int code;
55+
final String message;
56+
57+
StatusMessages(final int code, final String message) {
58+
this.code = code;
59+
this.message = message;
60+
}
61+
62+
static String getStatusMessage(final int statusCode) {
63+
return Arrays.stream(StatusMessages.values())
64+
.filter(status -> status.code == statusCode)
65+
.findFirst()
66+
.map(knownStatus -> knownStatus.message)
67+
.orElseGet(() -> {
68+
// If the status is unknown, default to 400 for client errors and 500 for server errors
69+
if (statusCode >= 400 && statusCode <= 499) {
70+
return BAD_REQUEST.message;
71+
}
72+
return INTERNAL_SERVER_ERROR.message;
73+
});
74+
}
75+
}
76+
77+
private HttpStatus() {
78+
// noop
79+
}
80+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Copyright Inrupt Inc.
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal in
6+
* the Software without restriction, including without limitation the rights to use,
7+
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
8+
* Software, and to permit persons to whom the Software is furnished to do so,
9+
* subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15+
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
16+
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
17+
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
18+
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
19+
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
package com.inrupt.client;
22+
23+
import com.inrupt.client.spi.JsonService;
24+
25+
import java.io.ByteArrayInputStream;
26+
import java.io.IOException;
27+
import java.net.URI;
28+
import java.util.HashMap;
29+
import java.util.Map;
30+
import java.util.Optional;
31+
32+
/**
33+
* A data class representing a structured problem description sent by the server on error response.
34+
*
35+
* @see <a href="https://www.rfc-editor.org/rfc/rfc9457">RFC 9457 Problem Details for HTTP APIs</a>
36+
*/
37+
public class ProblemDetails {
38+
public static final String MIME_TYPE = "application/problem+json";
39+
public static final String DEFAULT_TYPE = "about:blank";
40+
private final URI type;
41+
private final String title;
42+
private final String details;
43+
private final int status;
44+
private final URI instance;
45+
46+
public ProblemDetails(
47+
final URI type,
48+
final String title,
49+
final String details,
50+
final int status,
51+
final URI instance
52+
) {
53+
// The `type` is not mandatory in RFC9457, so we want to set
54+
// a default value here even when deserializing from JSON.
55+
if (type != null) {
56+
this.type = type;
57+
} else {
58+
this.type = URI.create(DEFAULT_TYPE);
59+
}
60+
this.title = title;
61+
this.details = details;
62+
this.status = status;
63+
this.instance = instance;
64+
}
65+
66+
public URI getType() {
67+
return this.type;
68+
};
69+
70+
public String getTitle() {
71+
return this.title;
72+
};
73+
74+
public String getDetails() {
75+
return this.details;
76+
};
77+
78+
public int getStatus() {
79+
return this.status;
80+
};
81+
82+
public URI getInstance() {
83+
return this.instance;
84+
};
85+
86+
public static ProblemDetails fromErrorResponse(
87+
final int statusCode,
88+
final Headers headers,
89+
final byte[] body,
90+
final JsonService jsonService
91+
) {
92+
if (jsonService == null
93+
|| (headers != null && !headers.allValues("Content-Type").contains(ProblemDetails.MIME_TYPE))) {
94+
return new ProblemDetails(
95+
null,
96+
HttpStatus.StatusMessages.getStatusMessage(statusCode),
97+
null,
98+
statusCode,
99+
null
100+
);
101+
}
102+
try {
103+
// ProblemDetails doesn't have a default constructor, and we can't use JSON mapping annotations because
104+
// the JSON service is an abstraction over JSON-B and Jackson, so we deserialize the JSON object in a Map
105+
// and build the ProblemDetails from the Map values.
106+
final Map<String, Object> pdData = jsonService.fromJson(
107+
new ByteArrayInputStream(body),
108+
new HashMap<String, Object>(){}.getClass().getGenericSuperclass()
109+
);
110+
final String title = Optional.ofNullable((String) pdData.get("title"))
111+
.orElse(HttpStatus.StatusMessages.getStatusMessage(statusCode));
112+
final String details = (String) pdData.get("details");
113+
final URI type = Optional.ofNullable((String) pdData.get("type"))
114+
.map(URI::create)
115+
.orElse(null);
116+
final URI instance = Optional.ofNullable((String) pdData.get("instance"))
117+
.map(URI::create)
118+
.orElse(null);
119+
// Note that the status code is disregarded from the body, and reused from the HTTP response directly,
120+
// as they must be the same as per https://www.rfc-editor.org/rfc/rfc9457.html#name-status.
121+
return new ProblemDetails(type, title, details, statusCode, instance);
122+
} catch (IOException e) {
123+
return new ProblemDetails(
124+
null,
125+
HttpStatus.StatusMessages.getStatusMessage(statusCode),
126+
null,
127+
statusCode,
128+
null
129+
);
130+
}
131+
}
132+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright Inrupt Inc.
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal in
6+
* the Software without restriction, including without limitation the rights to use,
7+
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
8+
* Software, and to permit persons to whom the Software is furnished to do so,
9+
* subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15+
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
16+
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
17+
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
18+
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
19+
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
package com.inrupt.client;
22+
23+
import static org.junit.jupiter.api.Assertions.assertEquals;
24+
25+
import org.junit.jupiter.api.Test;
26+
27+
public class HttpStatusTest {
28+
@Test
29+
void checkHttpStatusSearchKnownStatus() {
30+
assertEquals(
31+
HttpStatus.StatusMessages.getStatusMessage(HttpStatus.NOT_FOUND),
32+
HttpStatus.StatusMessages.NOT_FOUND.message
33+
);
34+
}
35+
36+
@Test
37+
void checkHttpStatusSearchUnknownClientError () {
38+
assertEquals(
39+
HttpStatus.StatusMessages.getStatusMessage(418),
40+
HttpStatus.StatusMessages.BAD_REQUEST.message
41+
);
42+
}
43+
44+
@Test
45+
void checkHttpStatusSearchUnknownServerError () {
46+
assertEquals(
47+
HttpStatus.StatusMessages.getStatusMessage(555),
48+
HttpStatus.StatusMessages.INTERNAL_SERVER_ERROR.message
49+
);
50+
assertEquals(
51+
HttpStatus.StatusMessages.getStatusMessage(999),
52+
HttpStatus.StatusMessages.INTERNAL_SERVER_ERROR.message
53+
);
54+
assertEquals(
55+
HttpStatus.StatusMessages.getStatusMessage(-1),
56+
HttpStatus.StatusMessages.INTERNAL_SERVER_ERROR.message
57+
);
58+
assertEquals(
59+
HttpStatus.StatusMessages.getStatusMessage(15),
60+
HttpStatus.StatusMessages.INTERNAL_SERVER_ERROR.message
61+
);
62+
}
63+
}

solid/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@
7979
<version>${project.version}</version>
8080
<scope>test</scope>
8181
</dependency>
82+
<dependency>
83+
<groupId>com.inrupt.client</groupId>
84+
<artifactId>inrupt-client-jackson</artifactId>
85+
<version>${project.version}</version>
86+
<scope>test</scope>
87+
</dependency>
8288
<dependency>
8389
<groupId>org.slf4j</groupId>
8490
<artifactId>slf4j-api</artifactId>

0 commit comments

Comments
 (0)