diff --git a/api/src/main/java/com/inrupt/client/ClientHttpException.java b/api/src/main/java/com/inrupt/client/ClientHttpException.java new file mode 100644 index 00000000000..abe1fbbc505 --- /dev/null +++ b/api/src/main/java/com/inrupt/client/ClientHttpException.java @@ -0,0 +1,54 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.inrupt.client; + +/** + * A runtime exception representing an HTTP error response carrying a structured representation of the problem. The + * problem description is embedded in a {@link ProblemDetails} instance. + */ +public class ClientHttpException extends InruptClientException { + private final ProblemDetails problemDetails; + + /** + * Create a ClientHttpException. + * @param problemDetails the {@link ProblemDetails} instance + * @param message the exception message + */ + public ClientHttpException(final ProblemDetails problemDetails, final String message) { + super(message); + this.problemDetails = problemDetails; + } + + /** + * Create a ClientHttpException. + * @param problemDetails the {@link ProblemDetails} instance + * @param message the exception message + * @param cause a wrapped exception cause + */ + public ClientHttpException(final ProblemDetails problemDetails, final String message, final Exception cause) { + super(message, cause); + this.problemDetails = problemDetails; + } + + public ProblemDetails getProblemDetails() { + return this.problemDetails; + } +} diff --git a/api/src/main/java/com/inrupt/client/HttpStatus.java b/api/src/main/java/com/inrupt/client/HttpStatus.java new file mode 100644 index 00000000000..52f72c14464 --- /dev/null +++ b/api/src/main/java/com/inrupt/client/HttpStatus.java @@ -0,0 +1,81 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.inrupt.client; + +import java.util.Arrays; + +public final class HttpStatus { + + public static final int BAD_REQUEST = 400; + public static final int UNAUTHORIZED = 401; + public static final int FORBIDDEN = 403; + public static final int NOT_FOUND = 404; + public static final int METHOD_NOT_ALLOWED = 405; + public static final int NOT_ACCEPTABLE = 406; + public static final int CONFLICT = 409; + public static final int GONE = 410; + public static final int PRECONDITION_FAILED = 412; + public static final int UNSUPPORTED_MEDIA_TYPE = 415; + public static final int TOO_MANY_REQUESTS = 429; + public static final int INTERNAL_SERVER_ERROR = 500; + + enum StatusMessages { + BAD_REQUEST(HttpStatus.BAD_REQUEST, "Bad Request"), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "Unauthorized"), + FORBIDDEN(HttpStatus.FORBIDDEN, "Forbidden"), + NOT_FOUND(HttpStatus.NOT_FOUND, "Not Found"), + METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "Method Not Allowed"), + NOT_ACCEPTABLE(HttpStatus.NOT_ACCEPTABLE, "Not Acceptable"), + CONFLICT(HttpStatus.CONFLICT, "Conflict"), + GONE(HttpStatus.GONE, "Gone"), + PRECONDITION_FAILED(HttpStatus.PRECONDITION_FAILED, "Precondition Failed"), + UNSUPPORTED_MEDIA_TYPE(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "Unsupported Media Type"), + TOO_MANY_REQUESTS(HttpStatus.TOO_MANY_REQUESTS, "Too Many Requests"), + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal Server Error"); + + private final int code; + final String message; + + StatusMessages(final int code, final String message) { + this.code = code; + this.message = message; + } + + static String getStatusMessage(final int statusCode) { + return Arrays.stream(StatusMessages.values()) + .filter(status -> status.code == statusCode) + .findFirst() + .map(knownStatus -> knownStatus.message) + .orElseGet(() -> { + // If the status is unknown, default to 400 for client errors and 500 for server errors + if (statusCode >= 400 && statusCode <= 499) { + return BAD_REQUEST.message; + } + return INTERNAL_SERVER_ERROR.message; + }); + } + } + + // Prevents instantiation. + private HttpStatus() { + /* no-op */ + } +} diff --git a/api/src/main/java/com/inrupt/client/ProblemDetails.java b/api/src/main/java/com/inrupt/client/ProblemDetails.java new file mode 100644 index 00000000000..d804755ebee --- /dev/null +++ b/api/src/main/java/com/inrupt/client/ProblemDetails.java @@ -0,0 +1,132 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.inrupt.client; + +import com.inrupt.client.spi.JsonService; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * A data class representing a structured problem description sent by the server on error response. + * + * @see RFC 9457 Problem Details for HTTP APIs + */ +public class ProblemDetails { + public static final String MIME_TYPE = "application/problem+json"; + public static final String DEFAULT_TYPE = "about:blank"; + private final URI type; + private final String title; + private final String details; + private final int status; + private final URI instance; + + public ProblemDetails( + final URI type, + final String title, + final String details, + final int status, + final URI instance + ) { + // The `type` is not mandatory in RFC9457, so we want to set + // a default value here even when deserializing from JSON. + if (type != null) { + this.type = type; + } else { + this.type = URI.create(DEFAULT_TYPE); + } + this.title = title; + this.details = details; + this.status = status; + this.instance = instance; + } + + public URI getType() { + return this.type; + }; + + public String getTitle() { + return this.title; + }; + + public String getDetails() { + return this.details; + }; + + public int getStatus() { + return this.status; + }; + + public URI getInstance() { + return this.instance; + }; + + public static ProblemDetails fromErrorResponse( + final int statusCode, + final Headers headers, + final byte[] body, + final JsonService jsonService + ) { + if (jsonService == null + || (headers != null && !headers.allValues("Content-Type").contains(ProblemDetails.MIME_TYPE))) { + return new ProblemDetails( + null, + HttpStatus.StatusMessages.getStatusMessage(statusCode), + null, + statusCode, + null + ); + } + try { + // ProblemDetails doesn't have a default constructor, and we can't use JSON mapping annotations because + // the JSON service is an abstraction over JSON-B and Jackson, so we deserialize the JSON object in a Map + // and build the ProblemDetails from the Map values. + final Map pdData = jsonService.fromJson( + new ByteArrayInputStream(body), + new HashMap(){}.getClass().getGenericSuperclass() + ); + final String title = Optional.ofNullable((String) pdData.get("title")) + .orElse(HttpStatus.StatusMessages.getStatusMessage(statusCode)); + final String details = (String) pdData.get("details"); + final URI type = Optional.ofNullable(pdData.get("type")) + .map(t -> URI.create((String) t)) + .orElse(null); + final URI instance = Optional.ofNullable(pdData.get("instance")) + .map(i -> URI.create((String) i)) + .orElse(null); + // Note that the status code is disregarded from the body, and reused from the HTTP response directly, + // as they must be the same as per https://www.rfc-editor.org/rfc/rfc9457.html#name-status. + return new ProblemDetails(type, title, details, statusCode, instance); + } catch (IOException e) { + return new ProblemDetails( + null, + HttpStatus.StatusMessages.getStatusMessage(statusCode), + null, + statusCode, + null + ); + } + } +} diff --git a/api/src/test/java/com/inrupt/client/HttpStatusTest.java b/api/src/test/java/com/inrupt/client/HttpStatusTest.java new file mode 100644 index 00000000000..35126cb23c0 --- /dev/null +++ b/api/src/test/java/com/inrupt/client/HttpStatusTest.java @@ -0,0 +1,63 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.inrupt.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class HttpStatusTest { + @Test + void checkHttpStatusSearchKnownStatus() { + assertEquals( + HttpStatus.StatusMessages.getStatusMessage(HttpStatus.NOT_FOUND), + HttpStatus.StatusMessages.NOT_FOUND.message + ); + } + + @Test + void checkHttpStatusSearchUnknownClientError () { + assertEquals( + HttpStatus.StatusMessages.getStatusMessage(418), + HttpStatus.StatusMessages.BAD_REQUEST.message + ); + } + + @Test + void checkHttpStatusSearchUnknownServerError () { + assertEquals( + HttpStatus.StatusMessages.getStatusMessage(555), + HttpStatus.StatusMessages.INTERNAL_SERVER_ERROR.message + ); + assertEquals( + HttpStatus.StatusMessages.getStatusMessage(999), + HttpStatus.StatusMessages.INTERNAL_SERVER_ERROR.message + ); + assertEquals( + HttpStatus.StatusMessages.getStatusMessage(-1), + HttpStatus.StatusMessages.INTERNAL_SERVER_ERROR.message + ); + assertEquals( + HttpStatus.StatusMessages.getStatusMessage(15), + HttpStatus.StatusMessages.INTERNAL_SERVER_ERROR.message + ); + } +} diff --git a/solid/pom.xml b/solid/pom.xml index 15163ab2e65..3be4e20184b 100644 --- a/solid/pom.xml +++ b/solid/pom.xml @@ -79,6 +79,12 @@ ${project.version} test + + com.inrupt.client + inrupt-client-jackson + ${project.version} + test + org.slf4j slf4j-api diff --git a/solid/src/main/java/com/inrupt/client/solid/BadRequestException.java b/solid/src/main/java/com/inrupt/client/solid/BadRequestException.java index 3fc0ad547f1..3e86a0ec5b4 100644 --- a/solid/src/main/java/com/inrupt/client/solid/BadRequestException.java +++ b/solid/src/main/java/com/inrupt/client/solid/BadRequestException.java @@ -21,6 +21,8 @@ package com.inrupt.client.solid; import com.inrupt.client.Headers; +import com.inrupt.client.HttpStatus; +import com.inrupt.client.ProblemDetails; import java.net.URI; @@ -32,7 +34,7 @@ public class BadRequestException extends SolidClientException { private static final long serialVersionUID = -3379457428921025570L; - public static final int STATUS_CODE = 400; + public static final int STATUS_CODE = HttpStatus.BAD_REQUEST; /** * Create a BadRequestException exception. @@ -41,6 +43,7 @@ public class BadRequestException extends SolidClientException { * @param uri the uri * @param headers the response headers * @param body the body + * @deprecated */ public BadRequestException( final String message, @@ -49,4 +52,22 @@ public BadRequestException( final String body) { super(message, uri, STATUS_CODE, headers, body); } + + /** + * Create a BadRequestException exception. + * + * @param message the message + * @param pd the ProblemDetails instance + * @param uri the uri + * @param headers the response headers + * @param body the body + */ + public BadRequestException( + final String message, + final ProblemDetails pd, + final URI uri, + final Headers headers, + final String body) { + super(message, pd, uri, headers, body); + } } diff --git a/solid/src/main/java/com/inrupt/client/solid/ConflictException.java b/solid/src/main/java/com/inrupt/client/solid/ConflictException.java index d88386eb724..80b63b7decf 100644 --- a/solid/src/main/java/com/inrupt/client/solid/ConflictException.java +++ b/solid/src/main/java/com/inrupt/client/solid/ConflictException.java @@ -21,6 +21,8 @@ package com.inrupt.client.solid; import com.inrupt.client.Headers; +import com.inrupt.client.HttpStatus; +import com.inrupt.client.ProblemDetails; import java.net.URI; @@ -32,7 +34,7 @@ public class ConflictException extends SolidClientException { private static final long serialVersionUID = -203198307847520748L; - public static final int STATUS_CODE = 409; + public static final int STATUS_CODE = HttpStatus.CONFLICT; /** * Create a ConflictException exception. @@ -41,6 +43,7 @@ public class ConflictException extends SolidClientException { * @param uri the uri * @param headers the response headers * @param body the body + * @deprecated */ public ConflictException( final String message, @@ -49,4 +52,22 @@ public ConflictException( final String body) { super(message, uri, STATUS_CODE, headers, body); } + + /** + * Create a ConflictException exception. + * + * @param message the message + * @param pd the ProblemDetails instance + * @param uri the uri + * @param headers the response headers + * @param body the body + */ + public ConflictException( + final String message, + final ProblemDetails pd, + final URI uri, + final Headers headers, + final String body) { + super(message, pd, uri, headers, body); + } } diff --git a/solid/src/main/java/com/inrupt/client/solid/ForbiddenException.java b/solid/src/main/java/com/inrupt/client/solid/ForbiddenException.java index 7aeb3219bec..9fd94a4fb9e 100644 --- a/solid/src/main/java/com/inrupt/client/solid/ForbiddenException.java +++ b/solid/src/main/java/com/inrupt/client/solid/ForbiddenException.java @@ -21,6 +21,8 @@ package com.inrupt.client.solid; import com.inrupt.client.Headers; +import com.inrupt.client.HttpStatus; +import com.inrupt.client.ProblemDetails; import java.net.URI; @@ -32,7 +34,7 @@ public class ForbiddenException extends SolidClientException { private static final long serialVersionUID = 3299286274724874244L; - public static final int STATUS_CODE = 403; + public static final int STATUS_CODE = HttpStatus.FORBIDDEN; /** * Create a ForbiddenException exception. @@ -41,6 +43,7 @@ public class ForbiddenException extends SolidClientException { * @param uri the uri * @param headers the response headers * @param body the body + * @deprecated */ public ForbiddenException( final String message, @@ -49,4 +52,22 @@ public ForbiddenException( final String body) { super(message, uri, STATUS_CODE, headers, body); } + + /** + * Create a ForbiddenException exception. + * + * @param message the message + * @param pd the ProblemDetails instance + * @param uri the uri + * @param headers the response headers + * @param body the body + */ + public ForbiddenException( + final String message, + final ProblemDetails pd, + final URI uri, + final Headers headers, + final String body) { + super(message, pd, uri, headers, body); + } } diff --git a/solid/src/main/java/com/inrupt/client/solid/GoneException.java b/solid/src/main/java/com/inrupt/client/solid/GoneException.java index 0882302e27b..511b5ca947a 100644 --- a/solid/src/main/java/com/inrupt/client/solid/GoneException.java +++ b/solid/src/main/java/com/inrupt/client/solid/GoneException.java @@ -21,6 +21,8 @@ package com.inrupt.client.solid; import com.inrupt.client.Headers; +import com.inrupt.client.HttpStatus; +import com.inrupt.client.ProblemDetails; import java.net.URI; @@ -32,7 +34,7 @@ public class GoneException extends SolidClientException { private static final long serialVersionUID = -6892345582498100242L; - public static final int STATUS_CODE = 410; + public static final int STATUS_CODE = HttpStatus.GONE; /** * Create a GoneException exception. @@ -41,6 +43,7 @@ public class GoneException extends SolidClientException { * @param uri the uri * @param headers the response headers * @param body the body + * @deprecated */ public GoneException( final String message, @@ -49,4 +52,22 @@ public GoneException( final String body) { super(message, uri, STATUS_CODE, headers, body); } + + /** + * Create a GoneException exception. + * + * @param message the message + * @param pd the ProblemDetails instance + * @param uri the uri + * @param headers the response headers + * @param body the body + */ + public GoneException( + final String message, + final ProblemDetails pd, + final URI uri, + final Headers headers, + final String body) { + super(message, pd, uri, headers, body); + } } diff --git a/solid/src/main/java/com/inrupt/client/solid/InternalServerErrorException.java b/solid/src/main/java/com/inrupt/client/solid/InternalServerErrorException.java index ea9acb1c56e..43a216fd307 100644 --- a/solid/src/main/java/com/inrupt/client/solid/InternalServerErrorException.java +++ b/solid/src/main/java/com/inrupt/client/solid/InternalServerErrorException.java @@ -21,6 +21,8 @@ package com.inrupt.client.solid; import com.inrupt.client.Headers; +import com.inrupt.client.HttpStatus; +import com.inrupt.client.ProblemDetails; import java.net.URI; @@ -32,7 +34,7 @@ public class InternalServerErrorException extends SolidClientException { private static final long serialVersionUID = -6672490715281719330L; - public static final int STATUS_CODE = 500; + public static final int STATUS_CODE = HttpStatus.INTERNAL_SERVER_ERROR; /** * Create an InternalServerErrorException exception. @@ -41,6 +43,7 @@ public class InternalServerErrorException extends SolidClientException { * @param uri the uri * @param headers the response headers * @param body the body + * @deprecated */ public InternalServerErrorException( final String message, @@ -49,4 +52,22 @@ public InternalServerErrorException( final String body) { super(message, uri, STATUS_CODE, headers, body); } + + /** + * Create an InternalServerErrorException exception. + * + * @param message the message + * @param pd the ProblemDetails instance + * @param uri the uri + * @param headers the response headers + * @param body the body + */ + public InternalServerErrorException( + final String message, + final ProblemDetails pd, + final URI uri, + final Headers headers, + final String body) { + super(message, pd, uri, headers, body); + } } diff --git a/solid/src/main/java/com/inrupt/client/solid/MethodNotAllowedException.java b/solid/src/main/java/com/inrupt/client/solid/MethodNotAllowedException.java index d472b5fcbd3..378e166b2a0 100644 --- a/solid/src/main/java/com/inrupt/client/solid/MethodNotAllowedException.java +++ b/solid/src/main/java/com/inrupt/client/solid/MethodNotAllowedException.java @@ -21,6 +21,8 @@ package com.inrupt.client.solid; import com.inrupt.client.Headers; +import com.inrupt.client.HttpStatus; +import com.inrupt.client.ProblemDetails; import java.net.URI; @@ -32,7 +34,7 @@ public class MethodNotAllowedException extends SolidClientException { private static final long serialVersionUID = -9125437562813923030L; - public static final int STATUS_CODE = 405; + public static final int STATUS_CODE = HttpStatus.METHOD_NOT_ALLOWED; /** * Create a MethodNotAllowedException exception. @@ -41,6 +43,7 @@ public class MethodNotAllowedException extends SolidClientException { * @param uri the uri * @param headers the response headers * @param body the body + * @deprecated */ public MethodNotAllowedException( final String message, @@ -49,4 +52,22 @@ public MethodNotAllowedException( final String body) { super(message, uri, STATUS_CODE, headers, body); } + + /** + * Create a MethodNotAllowedException exception. + * + * @param message the message + * @param pd the ProblemDetails instance + * @param uri the uri + * @param headers the response headers + * @param body the body + */ + public MethodNotAllowedException( + final String message, + final ProblemDetails pd, + final URI uri, + final Headers headers, + final String body) { + super(message, pd, uri, headers, body); + } } diff --git a/solid/src/main/java/com/inrupt/client/solid/NotAcceptableException.java b/solid/src/main/java/com/inrupt/client/solid/NotAcceptableException.java index 53744c0b2e8..f1509073ef0 100644 --- a/solid/src/main/java/com/inrupt/client/solid/NotAcceptableException.java +++ b/solid/src/main/java/com/inrupt/client/solid/NotAcceptableException.java @@ -21,6 +21,8 @@ package com.inrupt.client.solid; import com.inrupt.client.Headers; +import com.inrupt.client.HttpStatus; +import com.inrupt.client.ProblemDetails; import java.net.URI; @@ -32,7 +34,7 @@ public class NotAcceptableException extends SolidClientException { private static final long serialVersionUID = 6594993822477388733L; - public static final int STATUS_CODE = 406; + public static final int STATUS_CODE = HttpStatus.NOT_ACCEPTABLE; /** * Create a NotAcceptableException exception. @@ -41,6 +43,7 @@ public class NotAcceptableException extends SolidClientException { * @param uri the uri * @param headers the response headers * @param body the body + * @deprecated */ public NotAcceptableException( final String message, @@ -49,4 +52,22 @@ public NotAcceptableException( final String body) { super(message, uri, STATUS_CODE, headers, body); } + + /** + * Create a NotAcceptableException exception. + * + * @param message the message + * @param pd the ProblemDetails instance + * @param uri the uri + * @param headers the response headers + * @param body the body + */ + public NotAcceptableException( + final String message, + final ProblemDetails pd, + final URI uri, + final Headers headers, + final String body) { + super(message, pd, uri, headers, body); + } } diff --git a/solid/src/main/java/com/inrupt/client/solid/NotFoundException.java b/solid/src/main/java/com/inrupt/client/solid/NotFoundException.java index 466bed74894..71e4dc16361 100644 --- a/solid/src/main/java/com/inrupt/client/solid/NotFoundException.java +++ b/solid/src/main/java/com/inrupt/client/solid/NotFoundException.java @@ -21,6 +21,8 @@ package com.inrupt.client.solid; import com.inrupt.client.Headers; +import com.inrupt.client.HttpStatus; +import com.inrupt.client.ProblemDetails; import java.net.URI; @@ -32,7 +34,7 @@ public class NotFoundException extends SolidClientException { private static final long serialVersionUID = -2256628528500739683L; - public static final int STATUS_CODE = 404; + public static final int STATUS_CODE = HttpStatus.NOT_FOUND; /** * Create a NotFoundException exception. @@ -41,6 +43,7 @@ public class NotFoundException extends SolidClientException { * @param uri the uri * @param headers the response headers * @param body the body + * @deprecated */ public NotFoundException( final String message, @@ -49,4 +52,22 @@ public NotFoundException( final String body) { super(message, uri, STATUS_CODE, headers, body); } + + /** + * Create a NotFoundException exception. + * + * @param message the message + * @param pd the ProblemDetails instance + * @param uri the uri + * @param headers the response headers + * @param body the body + */ + public NotFoundException( + final String message, + final ProblemDetails pd, + final URI uri, + final Headers headers, + final String body) { + super(message, pd, uri, headers, body); + } } diff --git a/solid/src/main/java/com/inrupt/client/solid/PreconditionFailedException.java b/solid/src/main/java/com/inrupt/client/solid/PreconditionFailedException.java index 461c937a0eb..4c1d30f0065 100644 --- a/solid/src/main/java/com/inrupt/client/solid/PreconditionFailedException.java +++ b/solid/src/main/java/com/inrupt/client/solid/PreconditionFailedException.java @@ -21,6 +21,8 @@ package com.inrupt.client.solid; import com.inrupt.client.Headers; +import com.inrupt.client.HttpStatus; +import com.inrupt.client.ProblemDetails; import java.net.URI; @@ -32,7 +34,7 @@ public class PreconditionFailedException extends SolidClientException { private static final long serialVersionUID = 4205761003697773528L; - public static final int STATUS_CODE = 412; + public static final int STATUS_CODE = HttpStatus.PRECONDITION_FAILED; /** * Create a PreconditionFailedException exception. @@ -41,6 +43,7 @@ public class PreconditionFailedException extends SolidClientException { * @param uri the uri * @param headers the response headers * @param body the body + * @deprecated */ public PreconditionFailedException( final String message, @@ -49,4 +52,22 @@ public PreconditionFailedException( final String body) { super(message, uri, STATUS_CODE, headers, body); } + + /** + * Create a PreconditionFailedException exception. + * + * @param message the message + * @param pd the ProblemDetails instance + * @param uri the uri + * @param headers the response headers + * @param body the body + */ + public PreconditionFailedException( + final String message, + final ProblemDetails pd, + final URI uri, + final Headers headers, + final String body) { + super(message, pd, uri, headers, body); + } } diff --git a/solid/src/main/java/com/inrupt/client/solid/SolidClientException.java b/solid/src/main/java/com/inrupt/client/solid/SolidClientException.java index 668df265d3d..f6c67683626 100644 --- a/solid/src/main/java/com/inrupt/client/solid/SolidClientException.java +++ b/solid/src/main/java/com/inrupt/client/solid/SolidClientException.java @@ -20,15 +20,16 @@ */ package com.inrupt.client.solid; +import com.inrupt.client.ClientHttpException; import com.inrupt.client.Headers; -import com.inrupt.client.InruptClientException; +import com.inrupt.client.ProblemDetails; import java.net.URI; /** * A runtime exception for use with SolidClient HTTP operations. */ -public class SolidClientException extends InruptClientException { +public class SolidClientException extends ClientHttpException { private static final long serialVersionUID = 2868432164225689934L; @@ -45,16 +46,31 @@ public class SolidClientException extends InruptClientException { * @param statusCode the HTTP status code * @param headers the response headers * @param body the body + * @deprecated */ public SolidClientException(final String message, final URI uri, final int statusCode, final Headers headers, final String body) { - super(message); + super(null, message); this.uri = uri; this.statusCode = statusCode; this.headers = headers; this.body = body; } + public SolidClientException( + final String message, + final ProblemDetails pd, + final URI uri, + final Headers headers, + final String body + ) { + super(pd, message); + this.uri = uri; + this.statusCode = pd.getStatus(); + this.headers = headers; + this.body = body; + } + /** * Retrieve the URI associated with this exception. * @@ -91,6 +107,16 @@ public String getBody() { return body; } + /** + * + * @param message the resulting exception message + * @param uri the request URL + * @param statusCode the response status code + * @param headers the response {@link Headers} + * @param body the response body + * @return an appropriate exception based on the status code. + * @deprecated + */ public static SolidClientException handle( final String message, final URI uri, @@ -126,5 +152,51 @@ public static SolidClientException handle( return new SolidClientException(message, uri, statusCode, headers, body); } } + + /** + * + * @param message the resulting exception message + * @param pd the {@link ProblemDetails} instance + * @param uri the request URL + * @param headers the response {@link Headers} + * @param body the response body + * @return an appropriate exception based on the status code. + */ + public static SolidClientException handle( + final String message, + final ProblemDetails pd, + final URI uri, + final Headers headers, + final String body + ) { + switch (pd.getStatus()) { + case BadRequestException.STATUS_CODE: + return new BadRequestException(message, pd, uri, headers, body); + case UnauthorizedException.STATUS_CODE: + return new UnauthorizedException(message, pd, uri, headers, body); + case ForbiddenException.STATUS_CODE: + return new ForbiddenException(message, pd, uri, headers, body); + case NotFoundException.STATUS_CODE: + return new NotFoundException(message, pd, uri, headers, body); + case MethodNotAllowedException.STATUS_CODE: + return new MethodNotAllowedException(message, pd, uri, headers, body); + case NotAcceptableException.STATUS_CODE: + return new NotAcceptableException(message, pd, uri, headers, body); + case ConflictException.STATUS_CODE: + return new ConflictException(message, pd, uri, headers, body); + case GoneException.STATUS_CODE: + return new GoneException(message, pd, uri, headers, body); + case PreconditionFailedException.STATUS_CODE: + return new PreconditionFailedException(message, pd, uri, headers, body); + case UnsupportedMediaTypeException.STATUS_CODE: + return new UnsupportedMediaTypeException(message, pd, uri, headers, body); + case TooManyRequestsException.STATUS_CODE: + return new TooManyRequestsException(message, pd, uri, headers, body); + case InternalServerErrorException.STATUS_CODE: + return new InternalServerErrorException(message, pd, uri, headers, body); + default: + return new SolidClientException(message, pd, uri, headers, body); + } + } } diff --git a/solid/src/main/java/com/inrupt/client/solid/TooManyRequestsException.java b/solid/src/main/java/com/inrupt/client/solid/TooManyRequestsException.java index 4f299fc9492..d05b2f255fd 100644 --- a/solid/src/main/java/com/inrupt/client/solid/TooManyRequestsException.java +++ b/solid/src/main/java/com/inrupt/client/solid/TooManyRequestsException.java @@ -21,6 +21,8 @@ package com.inrupt.client.solid; import com.inrupt.client.Headers; +import com.inrupt.client.HttpStatus; +import com.inrupt.client.ProblemDetails; import java.net.URI; @@ -32,7 +34,7 @@ public class TooManyRequestsException extends SolidClientException { private static final long serialVersionUID = -1798491190232642824L; - public static final int STATUS_CODE = 429; + public static final int STATUS_CODE = HttpStatus.TOO_MANY_REQUESTS; /** * Create a TooManyRequestsException exception. @@ -41,6 +43,7 @@ public class TooManyRequestsException extends SolidClientException { * @param uri the uri * @param headers the response headers * @param body the body + * @deprecated */ public TooManyRequestsException( final String message, @@ -49,4 +52,22 @@ public TooManyRequestsException( final String body) { super(message, uri, STATUS_CODE, headers, body); } + + /** + * Create a TooManyRequestsException exception. + * + * @param message the message + * @param pd the ProblemDetails instance + * @param uri the uri + * @param headers the response headers + * @param body the body + */ + public TooManyRequestsException( + final String message, + final ProblemDetails pd, + final URI uri, + final Headers headers, + final String body) { + super(message, pd, uri, headers, body); + } } diff --git a/solid/src/main/java/com/inrupt/client/solid/UnauthorizedException.java b/solid/src/main/java/com/inrupt/client/solid/UnauthorizedException.java index db8615df958..e9d2d98571d 100644 --- a/solid/src/main/java/com/inrupt/client/solid/UnauthorizedException.java +++ b/solid/src/main/java/com/inrupt/client/solid/UnauthorizedException.java @@ -21,6 +21,8 @@ package com.inrupt.client.solid; import com.inrupt.client.Headers; +import com.inrupt.client.HttpStatus; +import com.inrupt.client.ProblemDetails; import java.net.URI; @@ -32,7 +34,7 @@ public class UnauthorizedException extends SolidClientException { private static final long serialVersionUID = -3219668517323678497L; - public static final int STATUS_CODE = 401; + public static final int STATUS_CODE = HttpStatus.UNAUTHORIZED; /** * Create an UnauthorizedException exception. @@ -41,6 +43,7 @@ public class UnauthorizedException extends SolidClientException { * @param uri the uri * @param headers the response headers * @param body the body + * @deprecated */ public UnauthorizedException( final String message, @@ -49,4 +52,22 @@ public UnauthorizedException( final String body) { super(message, uri, STATUS_CODE, headers, body); } + + /** + * Create a UnauthorizedException exception. + * + * @param message the message + * @param pd the ProblemDetails instance + * @param uri the uri + * @param headers the response headers + * @param body the body + */ + public UnauthorizedException( + final String message, + final ProblemDetails pd, + final URI uri, + final Headers headers, + final String body) { + super(message, pd, uri, headers, body); + } } diff --git a/solid/src/main/java/com/inrupt/client/solid/UnsupportedMediaTypeException.java b/solid/src/main/java/com/inrupt/client/solid/UnsupportedMediaTypeException.java index 27e9c96953d..4d90c434efc 100644 --- a/solid/src/main/java/com/inrupt/client/solid/UnsupportedMediaTypeException.java +++ b/solid/src/main/java/com/inrupt/client/solid/UnsupportedMediaTypeException.java @@ -21,6 +21,8 @@ package com.inrupt.client.solid; import com.inrupt.client.Headers; +import com.inrupt.client.HttpStatus; +import com.inrupt.client.ProblemDetails; import java.net.URI; @@ -32,7 +34,7 @@ public class UnsupportedMediaTypeException extends SolidClientException { private static final long serialVersionUID = 1312856145838280673L; - public static final int STATUS_CODE = 415; + public static final int STATUS_CODE = HttpStatus.UNSUPPORTED_MEDIA_TYPE; /** * Create an UnsupportedMediaTypeException exception. @@ -41,6 +43,7 @@ public class UnsupportedMediaTypeException extends SolidClientException { * @param uri the uri * @param headers the response headers * @param body the body + * @deprecated */ public UnsupportedMediaTypeException( final String message, @@ -49,4 +52,22 @@ public UnsupportedMediaTypeException( final String body) { super(message, uri, STATUS_CODE, headers, body); } + + /** + * Create a UnsupportedMediaTypeException exception. + * + * @param message the message + * @param pd the ProblemDetails instance + * @param uri the uri + * @param headers the response headers + * @param body the body + */ + public UnsupportedMediaTypeException( + final String message, + final ProblemDetails pd, + final URI uri, + final Headers headers, + final String body) { + super(message, pd, uri, headers, body); + } } diff --git a/solid/src/test/java/com/inrupt/client/solid/ProblemDetailsTest.java b/solid/src/test/java/com/inrupt/client/solid/ProblemDetailsTest.java new file mode 100644 index 00000000000..2cd6f500a86 --- /dev/null +++ b/solid/src/test/java/com/inrupt/client/solid/ProblemDetailsTest.java @@ -0,0 +1,164 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.inrupt.client.solid; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.inrupt.client.Headers; +import com.inrupt.client.ProblemDetails; +import com.inrupt.client.spi.JsonService; +import com.inrupt.client.spi.ServiceProvider; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +// Ideally, this class should be in the api module, +// but it creates a circular dependency with the JSON module implementation. +public class ProblemDetailsTest { + Headers mockProblemDetailsHeader() { + final List headerValues = new ArrayList<>(); + headerValues.add("application/problem+json"); + final Map> headerMap = new HashMap<>(); + headerMap.put("Content-Type", headerValues); + return Headers.of(headerMap); + } + + final JsonService jsonService = ServiceProvider.getJsonService(); + @Test + void testEmptyProblemDetails() { + final int statusCode = 400; + final ProblemDetails pd = ProblemDetails.fromErrorResponse( + statusCode, + mockProblemDetailsHeader(), + "{}".getBytes(), + jsonService + ); + assertEquals(ProblemDetails.DEFAULT_TYPE, pd.getType().toString()); + assertEquals(statusCode, pd.getStatus()); + Assertions.assertEquals("Bad Request", pd.getTitle()); + assertNull(pd.getDetails()); + assertNull(pd.getInstance()); + } + @Test + void testCompleteProblemDetails() { + final int statusCode = 400; + final ProblemDetails pd = ProblemDetails.fromErrorResponse( + statusCode, + mockProblemDetailsHeader(), + ("{" + + "\"title\":\"Some title\"," + + "\"status\":400," + + "\"details\":\"Some details\"," + + "\"instance\":\"https://example.org/instance\"," + + "\"type\":\"https://example.org/type\"" + + "}").getBytes(), + jsonService + ); + assertEquals("https://example.org/type", pd.getType().toString()); + assertEquals(statusCode, pd.getStatus()); + Assertions.assertEquals("Some title", pd.getTitle()); + assertEquals("Some details", pd.getDetails()); + assertEquals("https://example.org/instance", pd.getInstance().toString()); + } + + @Test + void testIgnoreUnknownProblemDetails() { + final int statusCode = 400; + final ProblemDetails pd = ProblemDetails.fromErrorResponse( + statusCode, + mockProblemDetailsHeader(), + ("{" + + "\"title\":\"Some title\"," + + "\"status\":400," + + "\"details\":\"Some details\"," + + "\"instance\":\"https://example.org/instance\"," + + "\"type\":\"https://example.org/type\"," + + "\"unknown\":\"Some unknown property\"" + + "}").getBytes(), + jsonService + ); + assertEquals("https://example.org/type", pd.getType().toString()); + assertEquals(statusCode, pd.getStatus()); + Assertions.assertEquals("Some title", pd.getTitle()); + assertEquals("Some details", pd.getDetails()); + assertEquals("https://example.org/instance", pd.getInstance().toString()); + } + + @Test + void testInvalidStatusProblemDetails() { + final int statusCode = 400; + final ProblemDetails pd = ProblemDetails.fromErrorResponse( + statusCode, + mockProblemDetailsHeader(), + ("{" + + "\"status\":\"Some invalid status\"," + + "}").getBytes(), + jsonService + ); + assertEquals(statusCode, pd.getStatus()); + } + + @Test + void testMismatchingStatusProblemDetails() { + final int statusCode = 400; + final ProblemDetails pd = ProblemDetails.fromErrorResponse( + statusCode, + mockProblemDetailsHeader(), + ("{" + + "\"status\":500," + + "}").getBytes(), + jsonService + ); + assertEquals(statusCode, pd.getStatus()); + } + + @Test + void testInvalidTypeProblemDetails() { + final ProblemDetails pd = ProblemDetails.fromErrorResponse( + 400, + mockProblemDetailsHeader(), + ("{" + + "\"type\":\"Some invalid type\"," + + "}").getBytes(), + jsonService + ); + assertEquals(ProblemDetails.DEFAULT_TYPE, pd.getType().toString()); + } + + @Test + void testInvalidInstanceProblemDetails() { + final ProblemDetails pd = ProblemDetails.fromErrorResponse( + 400, + mockProblemDetailsHeader(), + ("{" + + "\"instance\":\"Some invalid instance\"," + + "}").getBytes(), + jsonService + ); + assertNull(pd.getInstance()); + } +} diff --git a/solid/src/test/java/com/inrupt/client/solid/SolidExceptionTest.java b/solid/src/test/java/com/inrupt/client/solid/SolidExceptionTest.java index 15220345d68..bfab701eb77 100644 --- a/solid/src/test/java/com/inrupt/client/solid/SolidExceptionTest.java +++ b/solid/src/test/java/com/inrupt/client/solid/SolidExceptionTest.java @@ -22,6 +22,10 @@ import static org.junit.jupiter.api.Assertions.*; +import com.inrupt.client.ProblemDetails; + +import java.net.URI; + import org.junit.jupiter.api.Test; class SolidExceptionTest { @@ -41,4 +45,21 @@ void checkSolidWrappedException() { assertEquals(upstream, err.getCause()); assertEquals(msg, err.getMessage()); } + + @Test + void checkSolidClientException() { + final String msg = "Error"; + final ProblemDetails pd = new ProblemDetails( + URI.create("https://example.org/problem"), + "Some title", + "Some details", + 123, + URI.create("https://example.org/instance") + ); + final SolidClientException err = new SolidClientException( + msg, pd, URI.create("https://example.org/request"), null, "some body" + ); + assertEquals(msg, err.getMessage()); + assertEquals(pd, err.getProblemDetails()); + } } 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..619814b1f16 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; @@ -44,35 +45,48 @@ public int getPort() { return wireMockServer.port(); } + private static final String CONTENT_TYPE = "Content-Type"; + 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() {