diff --git a/api/src/main/java/org/eclipse/microprofile/openapi/annotations/responses/APIResponse.java b/api/src/main/java/org/eclipse/microprofile/openapi/annotations/responses/APIResponse.java index c047a9360..b47ebcc2e 100644 --- a/api/src/main/java/org/eclipse/microprofile/openapi/annotations/responses/APIResponse.java +++ b/api/src/main/java/org/eclipse/microprofile/openapi/annotations/responses/APIResponse.java @@ -46,9 +46,14 @@ * } * *
- * When this annotation is applied to an ExceptionMapper
, it allows developers to describe the API response
- * that will be added to a generated OpenAPI operation based on a JAX-RS method that declares an Exception
- * of the type handled by the ExceptionMapper
.
+ * When this annotation is applied to a JAX-RS resource class, the response is added to the responses defined in all
+ * OpenAPI operations which correspond to a method on that class. If an operation already has a response with the
+ * specified responseCode the response is not added to that operation.
+ *
+ *
+ * When this annotation is applied to an ExceptionMapper
class or toResponse
method, it allows
+ * developers to describe the API response that will be added to a generated OpenAPI operation based on a JAX-RS method
+ * that declares an Exception
of the type handled by the ExceptionMapper
.
*
*
* @Provider @@ -67,7 +72,7 @@ * @see "https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#responseObject" * **/ -@Target({ElementType.METHOD}) +@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Repeatable(APIResponses.class) diff --git a/api/src/main/java/org/eclipse/microprofile/openapi/annotations/responses/APIResponses.java b/api/src/main/java/org/eclipse/microprofile/openapi/annotations/responses/APIResponses.java index d3dc34bc7..597eb7984 100644 --- a/api/src/main/java/org/eclipse/microprofile/openapi/annotations/responses/APIResponses.java +++ b/api/src/main/java/org/eclipse/microprofile/openapi/annotations/responses/APIResponses.java @@ -31,7 +31,7 @@ * @see Responses * Object **/ -@Target({ElementType.METHOD}) +@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface APIResponses { diff --git a/tck/src/main/java/org/eclipse/microprofile/openapi/apps/airlines/exception/ReviewRejectedException.java b/tck/src/main/java/org/eclipse/microprofile/openapi/apps/airlines/exception/ReviewRejectedException.java new file mode 100644 index 000000000..1b38ada1d --- /dev/null +++ b/tck/src/main/java/org/eclipse/microprofile/openapi/apps/airlines/exception/ReviewRejectedException.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2022 Contributors to the Eclipse Foundation + *+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + *
+ * http://www.apache.org/licenses/LICENSE-2.0 + *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations + * under the License. + */ +package org.eclipse.microprofile.openapi.apps.airlines.exception; + +@SuppressWarnings("serial") +public class ReviewRejectedException extends Exception { + + public ReviewRejectedException(String rejectionReason, Throwable cause) { + super(rejectionReason, cause); + } + + public ReviewRejectedException(String rejectionReason) { + super(rejectionReason); + } + +} diff --git a/tck/src/main/java/org/eclipse/microprofile/openapi/apps/airlines/exception/ReviewRejectedExceptionMapper.java b/tck/src/main/java/org/eclipse/microprofile/openapi/apps/airlines/exception/ReviewRejectedExceptionMapper.java new file mode 100644 index 000000000..862ebbfe5 --- /dev/null +++ b/tck/src/main/java/org/eclipse/microprofile/openapi/apps/airlines/exception/ReviewRejectedExceptionMapper.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2022 Contributors to the Eclipse Foundation + *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + *
+ * http://www.apache.org/licenses/LICENSE-2.0 + *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations + * under the License. + */ +package org.eclipse.microprofile.openapi.apps.airlines.exception; + +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.apps.airlines.exception.ReviewRejectedExceptionMapper.RejectionResponse; + +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.ext.ExceptionMapper; + +@APIResponse(responseCode = "400", description = "The review was rejected", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = RejectionResponse.class))) +public class ReviewRejectedExceptionMapper implements ExceptionMapper
{ + + @Override + public Response toResponse(ReviewRejectedException exception) { + RejectionResponse response = new RejectionResponse(); + response.setReason(exception.getMessage()); + return Response.status(Status.BAD_REQUEST).entity(response).build(); + } + + public static class RejectionResponse { + @Schema(description = "The reason the review was rejected") + private String reason; + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } + } + +} diff --git a/tck/src/main/java/org/eclipse/microprofile/openapi/apps/airlines/resources/ReviewResource.java b/tck/src/main/java/org/eclipse/microprofile/openapi/apps/airlines/resources/ReviewResource.java index c4556c95e..7e73f1a03 100644 --- a/tck/src/main/java/org/eclipse/microprofile/openapi/apps/airlines/resources/ReviewResource.java +++ b/tck/src/main/java/org/eclipse/microprofile/openapi/apps/airlines/resources/ReviewResource.java @@ -46,6 +46,7 @@ import org.eclipse.microprofile.openapi.annotations.servers.Servers; import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.eclipse.microprofile.openapi.annotations.tags.Tags; +import org.eclipse.microprofile.openapi.apps.airlines.exception.ReviewRejectedException; import org.eclipse.microprofile.openapi.apps.airlines.model.Airline; import org.eclipse.microprofile.openapi.apps.airlines.model.Review; import org.eclipse.microprofile.openapi.apps.airlines.model.User; @@ -74,6 +75,8 @@ @Tag(name = "Reviews", description = "All the review methods"), @Tag(name = "Ratings", description = "All the ratings methods") }) +@APIResponse(responseCode = "429", description = "Client is rate limited") +@APIResponse(responseCode = "500", description = "Server error") public class ReviewResource { private static Map reviews = new ConcurrentHashMap (); @@ -231,7 +234,7 @@ public Response getReviewByAirlineAndUser( @Operation(summary = "Create a Review", operationId = "createReview") @Consumes("application/json") @Produces("application/json") - public Response createReview(Review review) { + public Response createReview(Review review) throws ReviewRejectedException { reviews.put(currentId, review); return Response.status(Status.CREATED).entity("{\"id\":" + currentId++ + "}").build(); } diff --git a/tck/src/main/java/org/eclipse/microprofile/openapi/apps/airlines/resources/UserResource.java b/tck/src/main/java/org/eclipse/microprofile/openapi/apps/airlines/resources/UserResource.java index 146e4e21d..8f25ecafd 100644 --- a/tck/src/main/java/org/eclipse/microprofile/openapi/apps/airlines/resources/UserResource.java +++ b/tck/src/main/java/org/eclipse/microprofile/openapi/apps/airlines/resources/UserResource.java @@ -56,6 +56,7 @@ @Produces({"application/json", "application/xml"}) @SecurityScheme(description = "user security scheme", type = SecuritySchemeType.HTTP, securitySchemeName = "httpSchemeForTest", scheme = "testScheme") @SecurityRequirement(name = "httpSchemeForTest") +@APIResponse(responseCode = "400", description = "Invalid request") public class UserResource { private static UserData userData = new UserData(); diff --git a/tck/src/main/java/org/eclipse/microprofile/openapi/tck/AirlinesAppTest.java b/tck/src/main/java/org/eclipse/microprofile/openapi/tck/AirlinesAppTest.java index 7662f75c7..e4de74007 100644 --- a/tck/src/main/java/org/eclipse/microprofile/openapi/tck/AirlinesAppTest.java +++ b/tck/src/main/java/org/eclipse/microprofile/openapi/tck/AirlinesAppTest.java @@ -295,10 +295,17 @@ public void testAPIResponse(String type) { vr.body("paths.'/bookings'.post.responses", aMapWithSize(1)); vr.body("paths.'/bookings'.post.responses.'201'.description", equalTo("Booking created")); + // @APIResponse at class level overridden at method level vr.body("paths.'/user/{username}'.delete.responses", aMapWithSize(3)); vr.body("paths.'/user/{username}'.delete.responses.'200'.description", equalTo("User deleted successfully")); vr.body("paths.'/user/{username}'.delete.responses.'400'.description", equalTo("Invalid username supplied")); vr.body("paths.'/user/{username}'.delete.responses.'404'.description", equalTo("User not found")); + + // @APIResponse at class level combined with method level + vr.body("paths.'/user/{username}'.patch.responses", aMapWithSize(2)); + vr.body("paths.'/user/{username}'.patch.responses.'200'.description", + equalTo("Password was changed successfully")); + vr.body("paths.'/user/{username}'.patch.responses.'400'.description", equalTo("Invalid request")); } @RunAsClient @@ -310,14 +317,17 @@ public void testAPIResponses(String type) { vr.body("paths.'/bookings/{id}'.get.responses.'200'.description", equalTo("Booking retrieved")); vr.body("paths.'/bookings/{id}'.get.responses.'404'.description", equalTo("Booking not found")); - vr.body("paths.'/reviews/users/{user}'.get.responses", aMapWithSize(2)); - vr.body("paths.'/reviews/users/{user}'.get.responses.'200'.description", equalTo("Review(s) retrieved")); - vr.body("paths.'/reviews/users/{user}'.get.responses.'404'.description", equalTo("Review(s) not found")); - vr.body("paths.'/user/{username}'.put.responses", aMapWithSize(3)); vr.body("paths.'/user/{username}'.put.responses.'200'.description", equalTo("User updated successfully")); vr.body("paths.'/user/{username}'.put.responses.'400'.description", equalTo("Invalid user supplied")); vr.body("paths.'/user/{username}'.put.responses.'404'.description", equalTo("User not found")); + + // @APIResponses on body combined with annotations on method + vr.body("paths.'/reviews/users/{user}'.get.responses", aMapWithSize(4)); + vr.body("paths.'/reviews/users/{user}'.get.responses.'200'.description", equalTo("Review(s) retrieved")); + vr.body("paths.'/reviews/users/{user}'.get.responses.'404'.description", equalTo("Review(s) not found")); + vr.body("paths.'/reviews/users/{user}'.get.responses.'429'.description", equalTo("Client is rate limited")); + vr.body("paths.'/reviews/users/{user}'.get.responses.'500'.description", equalTo("Server error")); } @RunAsClient @@ -1019,6 +1029,14 @@ public void testExceptionMappers(String type) { vr.body("paths.'/user/{username}'.get.responses.'404'.description", equalTo("Not Found")); vr.body("paths.'/user/{id}'.get.responses.'404'.content.'application/json'.schema", notNullValue()); + + vr.body("paths.'/reviews'.post.responses.'400'.description", equalTo("The review was rejected")); + vr.body("paths.'/reviews'.post.responses.'400'.content.'application/json'.schema", notNullValue()); + + String rejectedReviewSchema = + dereference(vr, "paths.'/reviews'.post.responses.'400'.content.'application/json'.schema"); + vr.body(rejectedReviewSchema + ".type", equalTo("object")); + vr.body(rejectedReviewSchema + ".properties", hasKey("reason")); } }