You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by gi...@apache.org on 2023/10/07 03:23:57 UTC
[camel-quarkus] 29/46: Add initial support for request validation in rest-openapi extension
This is an automated email from the ASF dual-hosted git repository.
github-bot pushed a commit to branch camel-main
in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
commit 5dbcb0b7796318a43f64d3b5d8331f860e2ae7fe
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)