You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ji...@apache.org on 2023/10/05 14:00:24 UTC

[camel-quarkus] 29/45: Add initial support for request validation in rest-openapi extension

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 <ja...@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)