Skip to content

Commit

Permalink
Add native support for rest-openapi request validation feature
Browse files Browse the repository at this point in the history
Fixes #5324
  • Loading branch information
jamesnetherton committed Oct 11, 2023
1 parent 2d2734f commit ee489db
Show file tree
Hide file tree
Showing 9 changed files with 272 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,80 @@
*/
package org.apache.camel.quarkus.component.rest.openapi.deployment;

import java.util.ArrayList;
import java.util.List;

import com.github.fge.jsonschema.keyword.validator.KeywordValidator;
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;
import io.swagger.v3.oas.models.media.Schema;
import org.jboss.jandex.IndexView;

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",
"io.swagger.core.v3");

@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
ReflectiveClassBuildItem registerForReflection(CombinedIndexBuildItem combinedIndex) {
List<String> reflectiveClassNames = new ArrayList<>();
IndexView index = combinedIndex.getIndex();

index.getAllKnownImplementors(MessageBundleLoader.class)
.stream()
.map(classInfo -> classInfo.name().toString())
.forEach(reflectiveClassNames::add);

index.getAllKnownImplementors(KeywordValidator.class)
.stream()
.map(classInfo -> classInfo.name().toString())
.forEach(reflectiveClassNames::add);

index.getAllKnownSubclasses(Schema.class)
.stream()
.map(classInfo -> classInfo.name().toString())
.forEach(reflectiveClassNames::add);

index.getClassesInPackage("io.swagger.v3.core.jackson.mixin")
.stream()
.map(classInfo -> classInfo.name().toString())
.forEach(reflectiveClassNames::add);

return ReflectiveClassBuildItem.builder(reflectiveClassNames.toArray(new String[0])).build();
}

@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"));
nativeImageResourceDirectory.produce(new NativeImageResourceDirectoryBuildItem("com/github/fge/jsonschema/validator"));
nativeImageResource.produce(new NativeImageResourceBuildItem("com/github/fge/uritemplate/messages.properties"));
}
}
5 changes: 5 additions & 0 deletions extensions/rest-openapi/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.camel.quarkus.component.rest.openapi.graal;

import java.util.concurrent.ExecutorService;

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;

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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@
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)
Expand All @@ -43,4 +46,14 @@ public 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";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,8 @@ public void configure() throws Exception {

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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
77 changes: 75 additions & 2 deletions integration-tests/rest-openapi/src/main/resources/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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" : {
Expand All @@ -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" : [ {
Expand Down Expand Up @@ -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" ],
Expand All @@ -86,6 +148,10 @@
"schemas" : {
"Fruit" : {
"type" : "object",
"required": [
"name",
"description"
],
"properties" : {
"name" : {
"type" : "string"
Expand All @@ -95,6 +161,13 @@
}
}
}
},
"securitySchemes" : {
"SecurityScheme" : {
"type" : "http",
"description" : "Authentication",
"scheme" : "basic"
}
}
}
}
Loading

0 comments on commit ee489db

Please sign in to comment.