This is an automated email from the ASF dual-hosted git repository. jiriondrusek pushed a commit to branch camel-main in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
commit 5c1b14cb6e89a9c5fa02206b3c40d62390c27a98 Author: James Netherton <jamesnether...@gmail.com> AuthorDate: Wed Sep 20 13:47:37 2023 +0100 Add initial support for request validation in rest-openapi extension --- .../openapi/deployment/RestOpenapiProcessor.java | 39 +++++++++++ extensions/rest-openapi/runtime/pom.xml | 5 ++ ...LoadingMessageSourceProviderSubstitutions.java} | 23 ++++--- .../component/rest/openapi/it/FruitResource.java | 13 ++++ .../rest/openapi/it/RestOpenApiRoutes.java | 3 + .../rest/openapi/it/RestOpenapiResource.java | 49 ++++++++++++-- .../component/rest/openapi/it/model/Fruit.java | 3 + .../rest-openapi/src/main/resources/openapi.json | 77 +++++++++++++++++++++- .../component/rest/openapi/it/RestOpenapiTest.java | 35 ++++++++++ 9 files changed, 228 insertions(+), 19 deletions(-) diff --git a/extensions/rest-openapi/deployment/src/main/java/org/apache/camel/quarkus/component/rest/openapi/deployment/RestOpenapiProcessor.java b/extensions/rest-openapi/deployment/src/main/java/org/apache/camel/quarkus/component/rest/openapi/deployment/RestOpenapiProcessor.java index 5f87825080..44417054ec 100644 --- a/extensions/rest-openapi/deployment/src/main/java/org/apache/camel/quarkus/component/rest/openapi/deployment/RestOpenapiProcessor.java +++ b/extensions/rest-openapi/deployment/src/main/java/org/apache/camel/quarkus/component/rest/openapi/deployment/RestOpenapiProcessor.java @@ -16,16 +16,55 @@ */ package org.apache.camel.quarkus.component.rest.openapi.deployment; +import java.util.List; + +import com.github.fge.msgsimple.load.MessageBundleLoader; +import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.IndexDependencyBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceDirectoryBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; class RestOpenapiProcessor { private static final String FEATURE = "camel-rest-openapi"; + private static final List<String> GROUP_IDS_TO_INDEX = List.of("com.github.java-json-tools", "com.atlassian.oai"); @BuildStep FeatureBuildItem feature() { return new FeatureBuildItem(FEATURE); } + @BuildStep + void indexDependencies(CurateOutcomeBuildItem curateOutcome, BuildProducer<IndexDependencyBuildItem> indexedDependency) { + curateOutcome.getApplicationModel() + .getDependencies() + .stream() + .filter(dependency -> GROUP_IDS_TO_INDEX.contains(dependency.getGroupId())) + .map(dependency -> new IndexDependencyBuildItem(dependency.getGroupId(), dependency.getArtifactId())) + .forEach(indexedDependency::produce); + } + + @BuildStep + void registerForReflection(CombinedIndexBuildItem combinedIndex, BuildProducer<ReflectiveClassBuildItem> reflectiveClass) { + combinedIndex.getIndex() + .getAllKnownImplementors(MessageBundleLoader.class) + .stream() + .map(classInfo -> ReflectiveClassBuildItem.builder(classInfo.name().toString()).build()) + .forEach(reflectiveClass::produce); + } + + @BuildStep + void nativeImageResources( + BuildProducer<NativeImageResourceDirectoryBuildItem> nativeImageResourceDirectory, + BuildProducer<NativeImageResourceBuildItem> nativeImageResource) { + nativeImageResourceDirectory.produce(new NativeImageResourceDirectoryBuildItem("swagger/validation")); + nativeImageResourceDirectory.produce(new NativeImageResourceDirectoryBuildItem("draftv3")); + nativeImageResourceDirectory.produce(new NativeImageResourceDirectoryBuildItem("draftv4")); + nativeImageResource.produce(new NativeImageResourceBuildItem("com/github/fge/uritemplate/messages.properties")); + } } diff --git a/extensions/rest-openapi/runtime/pom.xml b/extensions/rest-openapi/runtime/pom.xml index 8cc5fa17fa..79d63e1fb3 100644 --- a/extensions/rest-openapi/runtime/pom.xml +++ b/extensions/rest-openapi/runtime/pom.xml @@ -51,6 +51,11 @@ <groupId>org.apache.camel.quarkus</groupId> <artifactId>camel-quarkus-support-swagger</artifactId> </dependency> + <dependency> + <groupId>org.graalvm.sdk</groupId> + <artifactId>graal-sdk</artifactId> + <scope>provided</scope> + </dependency> </dependencies> <build> diff --git a/extensions/rest-openapi/deployment/src/main/java/org/apache/camel/quarkus/component/rest/openapi/deployment/RestOpenapiProcessor.java b/extensions/rest-openapi/runtime/src/main/java/org/apache/camel/quarkus/component/rest/openapi/graal/LoadingMessageSourceProviderSubstitutions.java similarity index 54% copy from extensions/rest-openapi/deployment/src/main/java/org/apache/camel/quarkus/component/rest/openapi/deployment/RestOpenapiProcessor.java copy to extensions/rest-openapi/runtime/src/main/java/org/apache/camel/quarkus/component/rest/openapi/graal/LoadingMessageSourceProviderSubstitutions.java index 5f87825080..b950bd394a 100644 --- a/extensions/rest-openapi/deployment/src/main/java/org/apache/camel/quarkus/component/rest/openapi/deployment/RestOpenapiProcessor.java +++ b/extensions/rest-openapi/runtime/src/main/java/org/apache/camel/quarkus/component/rest/openapi/graal/LoadingMessageSourceProviderSubstitutions.java @@ -14,18 +14,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.camel.quarkus.component.rest.openapi.deployment; +package org.apache.camel.quarkus.component.rest.openapi.graal; -import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.builditem.FeatureBuildItem; +import java.util.concurrent.ExecutorService; -class RestOpenapiProcessor { +import com.github.fge.msgsimple.provider.LoadingMessageSourceProvider; +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.RecomputeFieldValue; +import com.oracle.svm.core.annotate.TargetClass; - private static final String FEATURE = "camel-rest-openapi"; - - @BuildStep - FeatureBuildItem feature() { - return new FeatureBuildItem(FEATURE); - } +import static com.oracle.svm.core.annotate.RecomputeFieldValue.Kind.Reset; +@TargetClass(LoadingMessageSourceProvider.class) +final class LoadingMessageSourceProviderSubstitutions { + // Avoid eager initialization of ExecutorService at build time + @Alias + @RecomputeFieldValue(kind = Reset) + private ExecutorService service; } diff --git a/integration-tests/rest-openapi/src/main/java/org/apache/camel/quarkus/component/rest/openapi/it/FruitResource.java b/integration-tests/rest-openapi/src/main/java/org/apache/camel/quarkus/component/rest/openapi/it/FruitResource.java index ab27387d82..73b66b0271 100644 --- a/integration-tests/rest-openapi/src/main/java/org/apache/camel/quarkus/component/rest/openapi/it/FruitResource.java +++ b/integration-tests/rest-openapi/src/main/java/org/apache/camel/quarkus/component/rest/openapi/it/FruitResource.java @@ -20,12 +20,15 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.Set; +import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import org.apache.camel.quarkus.component.rest.openapi.it.model.Fruit; import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; @Path("/fruits") @Produces(MediaType.APPLICATION_JSON) @@ -43,4 +46,14 @@ public class FruitResource { public Set<Fruit> list() { return fruits; } + + @Operation(operationId = "add") + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.TEXT_PLAIN) + public String add(@RequestBody(required = true) Fruit fruit) { + // We don't bother adding the fruit to the fruits set as we're only interested in validating + // the actual request against the OpenAPI specification + return "Fruit created"; + } } diff --git a/integration-tests/rest-openapi/src/main/java/org/apache/camel/quarkus/component/rest/openapi/it/RestOpenApiRoutes.java b/integration-tests/rest-openapi/src/main/java/org/apache/camel/quarkus/component/rest/openapi/it/RestOpenApiRoutes.java index cd00434559..215e6bf519 100644 --- a/integration-tests/rest-openapi/src/main/java/org/apache/camel/quarkus/component/rest/openapi/it/RestOpenApiRoutes.java +++ b/integration-tests/rest-openapi/src/main/java/org/apache/camel/quarkus/component/rest/openapi/it/RestOpenApiRoutes.java @@ -37,5 +37,8 @@ public class RestOpenApiRoutes extends RouteBuilder { from("direct:start-classpath") .toD("rest-openapi:#list?specificationUri=classpath:openapi.json&host=RAW(http://localhost:${header.test-port})"); + + from("direct:validate") + .toD("rest-openapi:#add?specificationUri=classpath:openapi.json&host=RAW(http://localhost:${header.test-port})&requestValidationEnabled=true"); } } diff --git a/integration-tests/rest-openapi/src/main/java/org/apache/camel/quarkus/component/rest/openapi/it/RestOpenapiResource.java b/integration-tests/rest-openapi/src/main/java/org/apache/camel/quarkus/component/rest/openapi/it/RestOpenapiResource.java index 1c75a71519..9171d05434 100644 --- a/integration-tests/rest-openapi/src/main/java/org/apache/camel/quarkus/component/rest/openapi/it/RestOpenapiResource.java +++ b/integration-tests/rest-openapi/src/main/java/org/apache/camel/quarkus/component/rest/openapi/it/RestOpenapiResource.java @@ -16,15 +16,23 @@ */ package org.apache.camel.quarkus.component.rest.openapi.it; +import java.util.stream.Collectors; + import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import org.apache.camel.Exchange; +import org.apache.camel.Message; +import org.apache.camel.Processor; import org.apache.camel.ProducerTemplate; +import org.apache.camel.component.rest.openapi.RestOpenApiValidationException; @Path("/rest-openapi") @ApplicationScoped @@ -35,39 +43,66 @@ public class RestOpenapiResource { @Path("/fruits/list/json") @Produces(MediaType.APPLICATION_JSON) @GET - public Response invokeListFruitsOperation(@QueryParam("port") int port) { - return invokeListFruitsOperation("start-web-json", port); + public Response invokeApiOperation(@QueryParam("port") int port) { + return invokeApiOperation("start-web-json", port); } @Path("/fruits/list/yaml") @Produces(MediaType.APPLICATION_JSON) @GET public Response invokeListFruitsOperationYaml(@QueryParam("port") int port) { - return invokeListFruitsOperation("start-web-yaml", port); + return invokeApiOperation("start-web-yaml", port); } @Path("/fruits/list/file") @Produces(MediaType.APPLICATION_JSON) @GET public Response invokeListFruitsOperationFile(@QueryParam("port") int port) { - return invokeListFruitsOperation("start-file", port); + return invokeApiOperation("start-file", port); } @Path("/fruits/list/bean") @Produces(MediaType.APPLICATION_JSON) @GET public Response invokeListFruitsOperationBean(@QueryParam("port") int port) { - return invokeListFruitsOperation("start-bean", port); + return invokeApiOperation("start-bean", port); } @Path("/fruits/list/classpath") @Produces(MediaType.APPLICATION_JSON) @GET public Response invokeListFruitsOperationClasspath(@QueryParam("port") int port) { - return invokeListFruitsOperation("start-classpath", port); + return invokeApiOperation("start-classpath", port); + } + + @Path("/fruits/add") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.TEXT_PLAIN) + @POST + public Response invokeAddFruitOperation(@QueryParam("port") int port, String fruitJson) { + Exchange result = producerTemplate.request("direct:validate", new Processor() { + @Override + public void process(Exchange exchange) throws Exception { + Message message = exchange.getMessage(); + message.setHeader(Exchange.CONTENT_TYPE, "application/json"); + message.setHeader("test-port", port); + message.setBody(fruitJson); + } + }); + + Exception exception = result.getException(); + if (exception != null) { + String errorMessage = ""; + if (exception instanceof RestOpenApiValidationException) { + RestOpenApiValidationException validationException = (RestOpenApiValidationException) exception; + errorMessage = validationException.getValidationErrors().stream().collect(Collectors.joining(",")); + } + return Response.serverError().entity(errorMessage).build(); + } + return Response.ok().entity(result.getMessage().getBody(String.class)).build(); } - private Response invokeListFruitsOperation(String endpointName, int port) { + private Response invokeApiOperation(String endpointName, int port) { String response = producerTemplate.requestBodyAndHeader("direct:" + endpointName, null, "test-port", port, String.class); return Response.ok().entity(response).build(); diff --git a/integration-tests/rest-openapi/src/main/java/org/apache/camel/quarkus/component/rest/openapi/it/model/Fruit.java b/integration-tests/rest-openapi/src/main/java/org/apache/camel/quarkus/component/rest/openapi/it/model/Fruit.java index 8bb2c3dca7..8b21d8809d 100644 --- a/integration-tests/rest-openapi/src/main/java/org/apache/camel/quarkus/component/rest/openapi/it/model/Fruit.java +++ b/integration-tests/rest-openapi/src/main/java/org/apache/camel/quarkus/component/rest/openapi/it/model/Fruit.java @@ -21,6 +21,9 @@ public class Fruit { public String name; public String description; + public Fruit() { + } + public Fruit(String name, String description) { this.name = name; this.description = description; diff --git a/integration-tests/rest-openapi/src/main/resources/openapi.json b/integration-tests/rest-openapi/src/main/resources/openapi.json index 36d2617e57..e5adcbf864 100644 --- a/integration-tests/rest-openapi/src/main/resources/openapi.json +++ b/integration-tests/rest-openapi/src/main/resources/openapi.json @@ -2,7 +2,7 @@ "openapi" : "3.0.3", "info" : { "title" : "camel-quarkus-integration-test-rest-openapi API", - "version" : "2.13.0-SNAPSHOT" + "version" : "3.0.0" }, "paths" : { "/fruits" : { @@ -25,9 +25,53 @@ } } } + }, + "post" : { + "tags" : [ "Fruit Resource" ], + "operationId" : "add", + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Fruit" + } + } + }, + "required" : true + }, + "responses" : { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/rest-openapi/fruits/list/bean" : { + "get" : { + "tags" : [ "Rest Openapi Resource" ], + "parameters" : [ { + "name" : "port", + "in" : "query", + "schema" : { + "format" : "int32", + "type" : "integer" + } + } ], + "responses" : { + "200" : { + "description" : "OK" + } + } } }, - "/rest-openapi/fruits/list" : { + "/rest-openapi/fruits/list/classpath" : { "get" : { "tags" : [ "Rest Openapi Resource" ], "parameters" : [ { @@ -63,6 +107,24 @@ } } }, + "/rest-openapi/fruits/list/json" : { + "get" : { + "tags" : [ "Rest Openapi Resource" ], + "parameters" : [ { + "name" : "port", + "in" : "query", + "schema" : { + "format" : "int32", + "type" : "integer" + } + } ], + "responses" : { + "200" : { + "description" : "OK" + } + } + } + }, "/rest-openapi/fruits/list/yaml" : { "get" : { "tags" : [ "Rest Openapi Resource" ], @@ -86,6 +148,10 @@ "schemas" : { "Fruit" : { "type" : "object", + "required": [ + "name", + "description" + ], "properties" : { "name" : { "type" : "string" @@ -95,6 +161,13 @@ } } } + }, + "securitySchemes" : { + "SecurityScheme" : { + "type" : "http", + "description" : "Authentication", + "scheme" : "basic" + } } } } \ No newline at end of file diff --git a/integration-tests/rest-openapi/src/test/java/org/apache/camel/quarkus/component/rest/openapi/it/RestOpenapiTest.java b/integration-tests/rest-openapi/src/test/java/org/apache/camel/quarkus/component/rest/openapi/it/RestOpenapiTest.java index 62b41cacdc..2bef0288de 100644 --- a/integration-tests/rest-openapi/src/test/java/org/apache/camel/quarkus/component/rest/openapi/it/RestOpenapiTest.java +++ b/integration-tests/rest-openapi/src/test/java/org/apache/camel/quarkus/component/rest/openapi/it/RestOpenapiTest.java @@ -21,6 +21,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; +import io.quarkus.test.junit.DisabledOnIntegrationTest; import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; import io.restassured.http.ContentType; @@ -29,6 +30,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.is; @QuarkusTest class RestOpenapiTest { @@ -75,6 +77,39 @@ class RestOpenapiTest { invokeApiEndpoint("/rest-openapi/fruits/list/classpath"); } + @DisabledOnIntegrationTest("https://github.com/apache/camel-quarkus/issues/5324") + @Test + public void testInvokeApiEndpointWithRequestValidationEnabled() { + // Empty request body + RestAssured.given() + .queryParam("port", RestAssured.port) + .contentType(ContentType.JSON) + .post("/rest-openapi/fruits/add") + .then() + .statusCode(500) + .body(is("A request body is required but none found.")); + + // Mandatory JSON description field missing + RestAssured.given() + .queryParam("port", RestAssured.port) + .contentType(ContentType.JSON) + .body("{\"name\": \"Orange\"}") + .post("/rest-openapi/fruits/add") + .then() + .statusCode(500) + .body(is("Object has missing required properties ([\"description\"])")); + + // Valid request + RestAssured.given() + .queryParam("port", RestAssured.port) + .contentType(ContentType.JSON) + .body("{\"name\": \"Orange\",\"description\":\"Tasty fruit\"}") + .post("/rest-openapi/fruits/add") + .then() + .statusCode(200) + .body(is("Fruit created")); + } + private void invokeApiEndpoint(String path) { RestAssured.given() .queryParam("port", RestAssured.port)