Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Vert.x web validation module #1516

Merged
merged 1 commit into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,13 @@ Vert.x Mutiny webClient exploratory test.

Also see http/vertx-web-client/README.md

### `http/vertx-web-validation`
Ensure that you can deploy a simple Quarkus application with predefined routes using schema parser from vertx-json-schema and Router Vert.x approach,
incorporating web validation configuration through ValidationHandlerBuilder. One of the goal of vertx-web-validation functionality is to validate parameters and bodies of incoming requests.

It also verifies multiple deployment strategies like:
- Using Quarkus OpenShift extension

### `http/graphql`
This module covers some basic scenarios around GraphQL.

Expand Down
27 changes: 27 additions & 0 deletions http/vertx-web-validation/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.quarkus.ts.qe</groupId>
<artifactId>parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../..</relativePath>
</parent>
<artifactId>vertx-web-validation</artifactId>
<packaging>jar</packaging>
<name>Quarkus QE TS: HTTP: Vert.x-Web-Validation</name>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-validation</artifactId>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-json-schema</artifactId>
jcarranzan marked this conversation as resolved.
Show resolved Hide resolved
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.quarkus.ts.vertx.web.validation;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/shoppinglist")
@Produces(MediaType.APPLICATION_JSON)
public class ShopResource {

private static List<ShoppingList> shoppingList = createSampleProductList();

private static List<ShoppingList> createSampleProductList() {
shoppingList = new ArrayList<>();
shoppingList.add(new ShoppingList(UUID.randomUUID(), "ListName1", 25,
new ArrayList<>(Arrays.asList("Carrots", "Water", "Cheese", "Beer"))));
shoppingList.add(new ShoppingList(UUID.randomUUID(), "ListName2", 80,
new ArrayList<>(Arrays.asList("Meat", "Wine", "Almonds", "Potatoes", "Cake"))));
return shoppingList;
}

@GET
public List<ShoppingList> get() {
return shoppingList;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.quarkus.ts.vertx.web.validation;

import java.util.ArrayList;
import java.util.UUID;

public class ShoppingList {

public UUID id;

public String name;

public ArrayList<String> products;

public double price;

public ShoppingList(UUID id, String name, double price, ArrayList<String> products) {
this.id = id;
this.name = name;
this.price = price;
this.products = products;
}

public ArrayList<String> getProducts() {
return products;
}

public void setProducts(ArrayList<String> products) {
this.products = products;
}

public UUID getId() {
return id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public double getPrice() {
return price;
}

public void setPrice(double price) {
this.price = price;
}

@Override
public String toString() {
return String.format(
"Shopping list{id=%s, name=%s, products=%s, price=%s}",
getId(),
getName(),
getProducts().toString(),
getPrice());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package io.quarkus.ts.vertx.web.validation;

import static io.vertx.ext.web.validation.builder.Parameters.param;
import static io.vertx.json.schema.common.dsl.Schemas.arraySchema;
import static io.vertx.json.schema.common.dsl.Schemas.numberSchema;
import static io.vertx.json.schema.common.dsl.Schemas.objectSchema;
import static io.vertx.json.schema.common.dsl.Schemas.stringSchema;
import static io.vertx.json.schema.draft7.dsl.Keywords.maximum;

import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

import jakarta.annotation.PostConstruct;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;

import io.netty.handler.codec.http.HttpResponseStatus;
import io.vertx.core.Vertx;
import io.vertx.core.json.Json;
import io.vertx.core.json.JsonArray;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.validation.BadRequestException;
import io.vertx.ext.web.validation.BodyProcessorException;
import io.vertx.ext.web.validation.ParameterProcessorException;
import io.vertx.ext.web.validation.RequestParameters;
import io.vertx.ext.web.validation.RequestPredicate;
import io.vertx.ext.web.validation.RequestPredicateException;
import io.vertx.ext.web.validation.ValidationHandler;
import io.vertx.ext.web.validation.builder.Bodies;
import io.vertx.ext.web.validation.builder.Parameters;
import io.vertx.ext.web.validation.builder.ValidationHandlerBuilder;
import io.vertx.json.schema.SchemaParser;
import io.vertx.json.schema.SchemaRouter;
import io.vertx.json.schema.SchemaRouterOptions;
import io.vertx.json.schema.common.dsl.ObjectSchemaBuilder;

public class ValidationHandlerOnRoutes {
//TODO when Quarkus use vert.x version 4.4.6 we can use SchemaRepository instead of SchemaParser with SchemaRouter
//private SchemaRepository schemaRepository =SchemaRepository.create(new JsonSchemaOptions().setDraft(Draft.DRAFT7).setBaseUri(BASEURI));
private SchemaParser schemaParser;
private SchemaRouter schemaRouter;

@Inject
Vertx vertx;

private static ShopResource shopResource = new ShopResource();

private static final String ERROR_MESSAGE = "{\"error\": \"%s\"}";
private static final String SHOPPINGLIST_NOT_FOUND = "Shopping list not found in the list or does not exist with that name or price";

@PostConstruct
void initialize() {
schemaParser = createSchema();
}

private SchemaParser createSchema() {
schemaRouter = SchemaRouter.create(vertx, new SchemaRouterOptions());
schemaParser = SchemaParser.createDraft7SchemaParser(schemaRouter);
return schemaParser;
}

public void validateHandlerShoppingList(@Observes Router router) {
AtomicReference<String> queryAnswer = new AtomicReference<>();
router.get("/filterList")
.handler(ValidationHandlerBuilder
.create(schemaParser)
.queryParameter(param("shoppingListName", stringSchema()))
.queryParameter(param("shoppingListPrice", numberSchema().with(maximum(100)))).build())
.handler(routingContext -> {
RequestParameters parameters = routingContext.get(ValidationHandler.REQUEST_CONTEXT_KEY);
String shoppingListName = parameters.queryParameter("shoppingListName").getString();
Double totalPrice = parameters.queryParameter("shoppingListPrice").getDouble();

// Logic to list shoppingList based on shoppingListName and totalPrice
String shoppingListFound = fetchProductDetailsFromQuery(shoppingListName, totalPrice);
queryAnswer.set(shoppingListFound);

if (queryAnswer.get().equalsIgnoreCase(SHOPPINGLIST_NOT_FOUND)) {
routingContext.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code());
}

routingContext.response().putHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN).end(queryAnswer.get());
}).failureHandler(routingContext -> {
jcarranzan marked this conversation as resolved.
Show resolved Hide resolved
// Error handling:
if (routingContext.failure() instanceof BadRequestException ||
routingContext.failure() instanceof ParameterProcessorException ||
routingContext.failure() instanceof BodyProcessorException ||
routingContext.failure() instanceof RequestPredicateException) {

String errorMessage = routingContext.failure().toString();
routingContext.response()
.setStatusCode(HttpResponseStatus.BAD_REQUEST.code())
.putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
.end(String.format(ERROR_MESSAGE, errorMessage));
} else {
routingContext.next();
}

});
// Create a ValidationHandlerBuilder with explodedParam and arraySchema to filter by array items
ObjectSchemaBuilder bodySchemaBuilder = objectSchema()
.property("shoppingListName", stringSchema());
ValidationHandlerBuilder
.create(schemaParser)
.body(Bodies.json(bodySchemaBuilder));
router.get("/filterByArrayItem")
.handler(
ValidationHandlerBuilder
.create(schemaParser)
.queryParameter(Parameters.explodedParam("shoppingArray", arraySchema().items(stringSchema())))
.body(Bodies.json(bodySchemaBuilder))
.build())
.handler(routingContext -> {
RequestParameters parameters = routingContext.get(ValidationHandler.REQUEST_CONTEXT_KEY);
JsonArray myArray = parameters.queryParameter("shoppingArray").getJsonArray();
// Retrieve the list of all shoppingLists
List<ShoppingList> shoppingLists = fetchProductDetailsFromArrayQuery(myArray);

routingContext.response().putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
.end(Json.encodeToBuffer(shoppingLists));
});
// Let's allow to create a new item
router.post("/createShoppingList").handler(
ValidationHandlerBuilder
.create(schemaParser)
.predicate(RequestPredicate.BODY_REQUIRED)
.queryParameter(param("shoppingListName", stringSchema()))
.queryParameter(param("shoppingListPrice", numberSchema().with(maximum(100))))
.build())
.handler(routingContext -> {
routingContext.response().setStatusCode(HttpResponseStatus.OK.code()).end("Shopping list created");
});

}

public List<ShoppingList> fetchProductDetailsFromArrayQuery(JsonArray myArray) {
return shopResource.get().stream()
.filter(shoppingList -> myArray.contains(shoppingList.getName()))
.collect(Collectors.toList());
}

public String fetchProductDetailsFromQuery(String name, Double price) {
return shopResource.get().stream()
jcarranzan marked this conversation as resolved.
Show resolved Hide resolved
.filter(product -> name.equalsIgnoreCase(product.getName()) && price.equals(product.getPrice()))
.map(ShoppingList::toString)
.findFirst()
.orElse(SHOPPINGLIST_NOT_FOUND);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.quarkus.ts.vertx.web.validation;

import io.quarkus.test.scenarios.OpenShiftScenario;

@OpenShiftScenario
public class OpenShiftVertxWebValidationIT extends VertxWebValidationIT {
}
Loading