This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 06cdd2db9c9d CAMEL-22971: camel-platform-http-vertx: Using rest-dsl
contract-first… (#21305)
06cdd2db9c9d is described below
commit 06cdd2db9c9dfee3e0079cb22aeb7442020c2753
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Feb 8 11:10:57 2026 +0100
CAMEL-22971: camel-platform-http-vertx: Using rest-dsl contract-first…
(#21305)
* CAMEL-22971: camel-platform-http-vertx: Using rest-dsl contract-first
should use fine grained vertx-web router
---
.../http/vertx/VertxPlatformHttpConsumer.java | 108 ++++++++++++++++++---
...PlatformHttpRestOpenApiConsumerRestDslTest.java | 2 +-
.../vertx/PlatformHttpRestOpenApiConsumerTest.java | 1 +
.../rest/openapi/RestOpenApiProcessor.java | 25 +++--
.../camel/component/rest/DefaultRestRegistry.java | 36 ++++++-
.../java/org/apache/camel/spi/RestRegistry.java | 28 ++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_18.adoc | 10 ++
7 files changed, 184 insertions(+), 26 deletions(-)
diff --git
a/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpConsumer.java
b/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpConsumer.java
index fd088b7f2a03..d552bad55f78 100644
---
a/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpConsumer.java
+++
b/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpConsumer.java
@@ -17,6 +17,7 @@
package org.apache.camel.component.platform.http.vertx;
import java.io.File;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
@@ -72,6 +73,7 @@ import static
org.apache.camel.util.CollectionHelper.appendEntry;
*/
public class VertxPlatformHttpConsumer extends DefaultConsumer
implements PlatformHttpConsumer, Suspendable {
+
private static final Logger LOGGER =
LoggerFactory.getLogger(VertxPlatformHttpConsumer.class);
private static final Pattern PATH_PARAMETER_PATTERN =
Pattern.compile("\\{([^/}]+)\\}");
@@ -79,9 +81,9 @@ public class VertxPlatformHttpConsumer extends DefaultConsumer
private final String fileNameExtWhitelist;
private final boolean muteExceptions;
private final boolean handleWriteResponseError;
+ private final List<Route> routes = new ArrayList<>();
private Set<Method> methods;
private String path;
- private Route route;
private VertxPlatformHttpRouter router;
private HttpRequestBodyHandler httpRequestBodyHandler;
private CookieConfiguration cookieConfiguration;
@@ -110,7 +112,7 @@ public class VertxPlatformHttpConsumer extends
DefaultConsumer
protected void doInit() throws Exception {
super.doInit();
methods = Method.parseList(getEndpoint().getHttpMethodRestrict());
- path = configureEndpointPath(getEndpoint());
+ path = configureEndpointPath(getEndpoint()); // in vertx-web we
should replace path parameters from {xxx} to :xxx syntax
router =
VertxPlatformHttpRouter.lookup(getEndpoint().getCamelContext(), routerName);
if (router == null) {
// dynamic assigned port number, then lookup using -0
@@ -135,20 +137,22 @@ public class VertxPlatformHttpConsumer extends
DefaultConsumer
protected void doStart() throws Exception {
super.doStart();
- final Route newRoute = router.route(path);
+ if (startRestServicesContractFirst()) {
+ // rest-dsl contract first using multiple routers per api endpoint
+ return;
+ }
+ // standard http consumer using a single router
+ final Route newRoute = router.route(path);
if (getEndpoint().getRequestTimeout() > 0) {
newRoute.handler(TimeoutHandler.create(getEndpoint().getRequestTimeout()));
}
-
if
(getEndpoint().getCamelContext().getRestConfiguration().isEnableCORS() &&
getEndpoint().getConsumes() != null) {
((RouteImpl) newRoute).setEmptyBodyPermittedWithConsumes(true);
}
-
if (!methods.equals(Method.getAll())) {
methods.forEach(m ->
newRoute.method(HttpMethod.valueOf(m.name())));
}
-
if (getEndpoint().getComponent().isServerRequestValidation()) {
if (getEndpoint().getConsumes() != null) {
//comma separated contentTypes has to be registered one by one
@@ -163,31 +167,107 @@ public class VertxPlatformHttpConsumer extends
DefaultConsumer
}
}
}
-
httpRequestBodyHandler.configureRoute(newRoute);
for (Handler<RoutingContext> handler : handlers) {
newRoute.handler(handler);
}
-
newRoute.handler(this::handleRequest);
-
- this.route = newRoute;
+ this.routes.add(newRoute);
}
@Override
protected void doStop() throws Exception {
- if (route != null) {
- route.remove();
- route = null;
- }
+ this.routes.forEach(Route::remove);
+ this.routes.clear();
super.doStop();
}
+ /**
+ * Special start logic for Rest DSL with contract-first, which need to use
fine-grained vertx router to make this
+ * consistent with Camel, otherwise there is only 1 vertx router to handle
all the API endpoints (coarse grained)
+ * which distorts the observability in vertx and camel-quarkus.
+ *
+ * @return true if in rest-dsl contract-first mode, false if standard mode
+ */
+ protected boolean startRestServicesContractFirst() throws Exception {
+ boolean matched = false;
+ for (var r :
getEndpoint().getCamelContext().getRestRegistry().listAllRestServices()) {
+ // rest-dsl contract-first we need to create a new unique router
per API endpoint
+ String target = path;
+ if (target.endsWith("*")) {
+ target = target.substring(0, target.length() - 1);
+ }
+ if (r.isContractFirst() && target.equals(r.getBasePath())) {
+ matched = true;
+ String u = r.getBasePath() + r.getBaseUrl();
+ u = configureEndpointPath(u); // in vertx-web we should
replace path parameters from {xxx} to :xxx syntax
+ String v = r.getMethod();
+ String c = r.getConsumes();
+ String p = r.getProduces();
+
+ Route sr = router.route(u);
+ sr.method(HttpMethod.valueOf(v));
+ if (getEndpoint().getComponent().isServerRequestValidation()) {
+ if (c != null) {
+ for (String cc : c.split(",")) {
+ sr.consumes(cc);
+ }
+ }
+ if (p != null) {
+ for (String pp : p.split(",")) {
+ sr.produces(pp);
+ }
+ }
+ }
+ httpRequestBodyHandler.configureRoute(sr);
+ for (Handler<RoutingContext> handler : handlers) {
+ sr.handler(handler);
+ }
+ sr.handler(this::handleRequest);
+ this.routes.add(sr);
+ }
+ }
+ for (var r :
getEndpoint().getCamelContext().getRestRegistry().listAllRestSpecifications()) {
+ // rest-dsl contract-first we need to see if there is an api spec
+ // that should be exposed via a vertx http router
+ String target = path;
+ if (target.endsWith("*")) {
+ target = target.substring(0, target.length() - 1);
+ }
+ if (r.isSpecification() && target.equals(r.getBasePath())) {
+ String u = r.getBasePath() + r.getBaseUrl();
+ String v = r.getMethod();
+ String p = r.getProduces();
+
+ Route sr = router.route(u);
+ sr.method(HttpMethod.valueOf(v));
+ if (getEndpoint().getComponent().isServerRequestValidation()) {
+ if (p != null) {
+ for (String pp : p.split(",")) {
+ sr.produces(pp);
+ }
+ }
+ }
+ httpRequestBodyHandler.configureRoute(sr);
+ for (Handler<RoutingContext> handler : handlers) {
+ sr.handler(handler);
+ }
+ sr.handler(this::handleRequest);
+ this.routes.add(sr);
+ }
+ }
+ return matched;
+ }
+
private String configureEndpointPath(PlatformHttpEndpoint endpoint) {
String path = endpoint.getPath();
if (endpoint.isMatchOnUriPrefix() && !path.endsWith("*")) {
path += "*";
}
+ return configureEndpointPath(path);
+ }
+
+ private String configureEndpointPath(String path) {
// Transform from the Camel path param syntax /path/{key} to vert.x
web's /path/:key
return PATH_PARAMETER_PATTERN.matcher(path).replaceAll(":$1");
}
diff --git
a/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/PlatformHttpRestOpenApiConsumerRestDslTest.java
b/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/PlatformHttpRestOpenApiConsumerRestDslTest.java
index e320de9ed0f2..6df49cd56ee6 100644
---
a/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/PlatformHttpRestOpenApiConsumerRestDslTest.java
+++
b/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/PlatformHttpRestOpenApiConsumerRestDslTest.java
@@ -224,7 +224,7 @@ public class PlatformHttpRestOpenApiConsumerRestDslTest {
context.start();
given()
- .when()
+ .when().contentType("application/json")
.put("/api/v3/pet")
.then()
.statusCode(400); // no request body
diff --git
a/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/PlatformHttpRestOpenApiConsumerTest.java
b/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/PlatformHttpRestOpenApiConsumerTest.java
index e494ff86b058..74969e7595d1 100644
---
a/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/PlatformHttpRestOpenApiConsumerTest.java
+++
b/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/PlatformHttpRestOpenApiConsumerTest.java
@@ -231,6 +231,7 @@ public class PlatformHttpRestOpenApiConsumerTest {
given()
.when()
+ .contentType("application/json")
.put("/api/v3/pet")
.then()
.statusCode(400); // no request body
diff --git
a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiProcessor.java
b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiProcessor.java
index 531b401a7d00..e1816fcd17cb 100644
---
a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiProcessor.java
+++
b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiProcessor.java
@@ -19,7 +19,6 @@ package org.apache.camel.component.rest.openapi;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
-import java.util.Optional;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
@@ -184,6 +183,20 @@ public class RestOpenApiProcessor extends
AsyncProcessorSupport implements Camel
}
openApiUtils.clear(); // no longer needed
+ // register api-doc in rest registry
+ if (endpoint.getSpecificationUri() != null && apiContextPath != null) {
+ String url = basePath + apiContextPath;
+ String produces = null;
+ if (endpoint.getSpecificationUri().endsWith("json")) {
+ produces = "application/json";
+ } else if (endpoint.getSpecificationUri().endsWith("yaml") ||
endpoint.getSpecificationUri().endsWith("yml")) {
+ produces = "text/yaml";
+ }
+ // register api-doc
+ camelContext.getRestRegistry().addRestSpecification(consumer,
true, url, apiContextPath, basePath, "GET", produces,
+ null);
+ }
+
for (var p : paths) {
if (p instanceof RestOpenApiConsumerPath rcp) {
ServiceHelper.startService(rcp.getBinding());
@@ -216,15 +229,9 @@ public class RestOpenApiProcessor extends
AsyncProcessorSupport implements Camel
bc.setClientResponseValidation(config.isClientResponseValidation() ||
endpoint.isClientResponseValidation());
bc.setEnableNoContentResponse(config.isEnableNoContentResponse());
bc.setSkipBindingOnErrorCode(config.isSkipBindingOnErrorCode());
-
- String consumes =
Optional.ofNullable(openApiUtils.getConsumes(o)).orElse(endpoint.getConsumes());
- String produces =
Optional.ofNullable(openApiUtils.getProduces(o)).orElse(endpoint.getProduces());
-
- bc.setConsumes(consumes);
- bc.setProduces(produces);
-
+ bc.setConsumes(openApiUtils.getConsumes(o));
+ bc.setProduces(openApiUtils.getProduces(o));
bc.setRequiredBody(openApiUtils.isRequiredBody(o));
-
bc.setRequiredQueryParameters(openApiUtils.getRequiredQueryParameters(o));
bc.setRequiredHeaders(openApiUtils.getRequiredHeaders(o));
bc.setQueryDefaultValues(openApiUtils.getQueryParametersDefaultValue(o));
diff --git
a/components/camel-rest/src/main/java/org/apache/camel/component/rest/DefaultRestRegistry.java
b/components/camel-rest/src/main/java/org/apache/camel/component/rest/DefaultRestRegistry.java
index ac219ca61d4a..8d50ad31c8b6 100644
---
a/components/camel-rest/src/main/java/org/apache/camel/component/rest/DefaultRestRegistry.java
+++
b/components/camel-rest/src/main/java/org/apache/camel/component/rest/DefaultRestRegistry.java
@@ -43,6 +43,7 @@ public class DefaultRestRegistry extends ServiceSupport
implements RestRegistry,
private CamelContext camelContext;
private final Map<Consumer, List<RestService>> registry = new
LinkedHashMap<>();
+ private final Map<Consumer, List<RestService>> specs = new
LinkedHashMap<>();
private transient Producer apiProducer;
@Override
@@ -51,15 +52,28 @@ public class DefaultRestRegistry extends ServiceSupport
implements RestRegistry,
String method,
String consumes, String produces, String inType, String outType,
String routeId, String description) {
RestServiceEntry entry = new RestServiceEntry(
- consumer, contractFirst, url, baseUrl, basePath, uriTemplate,
method, consumes, produces, inType, outType,
+ consumer, false, contractFirst, url, baseUrl, basePath,
uriTemplate, method, consumes, produces, inType,
+ outType,
description);
List<RestService> list = registry.computeIfAbsent(consumer, c -> new
ArrayList<>());
list.add(entry);
}
+ @Override
+ public void addRestSpecification(
+ Consumer consumer, boolean contractFirst, String url, String
baseUrl, String basePath, String method,
+ String produces, String description) {
+ RestServiceEntry entry = new RestServiceEntry(
+ consumer, true, contractFirst, url, baseUrl, basePath, null,
method, null, produces, null, null,
+ description);
+ List<RestService> list = specs.computeIfAbsent(consumer, c -> new
ArrayList<>());
+ list.add(entry);
+ }
+
@Override
public void removeRestService(Consumer consumer) {
registry.remove(consumer);
+ specs.remove(consumer);
}
@Override
@@ -71,6 +85,15 @@ public class DefaultRestRegistry extends ServiceSupport
implements RestRegistry,
return answer;
}
+ @Override
+ public List<RestService> listAllRestSpecifications() {
+ List<RestRegistry.RestService> answer = new ArrayList<>();
+ for (var list : specs.values()) {
+ answer.addAll(list);
+ }
+ return answer;
+ }
+
@Override
public int size() {
int count = 0;
@@ -160,6 +183,7 @@ public class DefaultRestRegistry extends ServiceSupport
implements RestRegistry,
@Override
protected void doStop() throws Exception {
registry.clear();
+ specs.clear();
}
/**
@@ -168,6 +192,7 @@ public class DefaultRestRegistry extends ServiceSupport
implements RestRegistry,
private static final class RestServiceEntry implements RestService {
private final Consumer consumer;
+ private final boolean specification;
private final boolean contractFirst;
private final String url;
private final String baseUrl;
@@ -180,10 +205,12 @@ public class DefaultRestRegistry extends ServiceSupport
implements RestRegistry,
private final String outType;
private final String description;
- private RestServiceEntry(Consumer consumer, boolean contractFirst,
String url, String baseUrl, String basePath,
+ private RestServiceEntry(Consumer consumer, boolean specification,
boolean contractFirst, String url, String baseUrl,
+ String basePath,
String uriTemplate, String method, String
consumes, String produces,
String inType, String outType, String
description) {
this.consumer = consumer;
+ this.specification = specification;
this.contractFirst = contractFirst;
this.url = url;
this.baseUrl = baseUrl;
@@ -202,6 +229,11 @@ public class DefaultRestRegistry extends ServiceSupport
implements RestRegistry,
return consumer;
}
+ @Override
+ public boolean isSpecification() {
+ return specification;
+ }
+
@Override
public boolean isContractFirst() {
return contractFirst;
diff --git
a/core/camel-api/src/main/java/org/apache/camel/spi/RestRegistry.java
b/core/camel-api/src/main/java/org/apache/camel/spi/RestRegistry.java
index 51ba3c1c2882..483a2583946e 100644
--- a/core/camel-api/src/main/java/org/apache/camel/spi/RestRegistry.java
+++ b/core/camel-api/src/main/java/org/apache/camel/spi/RestRegistry.java
@@ -37,6 +37,11 @@ public interface RestRegistry extends StaticService {
*/
Consumer getConsumer();
+ /**
+ * Is this the API contract specification (ie api-doc)
+ */
+ boolean isSpecification();
+
/**
* Is the rest service based on code-first or contract-first
*/
@@ -139,6 +144,29 @@ public interface RestRegistry extends StaticService {
*/
List<RestService> listAllRestServices();
+ /**
+ * Adds information about the API specification (ie api-doc)
+ *
+ * @param consumer the consumer
+ * @param contractFirst is the rest service based on code-first or
contract-first
+ * @param url the absolute url of the REST service
+ * @param baseUrl the base url of the REST service
+ * @param basePath the base path
+ * @param method the HTTP method
+ * @param produces optional details about what media-types the REST
service returns
+ * @param description optional description about the service
+ */
+ void addRestSpecification(
+ Consumer consumer, boolean contractFirst, String url, String
baseUrl, String basePath, String method,
+ String produces, String description);
+
+ /**
+ * List all REST API specification (ie api-doc)
+ *
+ * @return all the API specification (ie api-doc)
+ */
+ List<RestService> listAllRestSpecifications();
+
/**
* Number of rest services in the registry.
*
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
index 605ac9179024..5c5a2f90c47d 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
@@ -42,6 +42,16 @@ Even if the interface HostApplicationEventHandler is public,
I do not expect Cam
Consequently, there is an API break
`org.apache.camel.tahu.handlers.TahuHostApplicationEventHandler` has been
removed. It is replaced by
`org.apache.camel.tahu.handlers.MultiTahuHostApplicationEventHandler`.
+=== camel-platform-http-vertx and Rest DSL contract-first
+
+When using Rest DSL in _contract first_ style, then the HTTP engine
(vertx-web) instead of a single
+router to handle all incoming Rest API calls, is now one unique router per API
endpoint. This change
+can affect HTTP request validation as vertx/Quarkus is now also performing
this per API endpoint according
+to the API specification.
+
+All together this would make Camel behave similar for Rest DSL for both _code
first_ and _contract first_ style.
+
+
=== Component deprecation
The `camel-olingo2` and `camel-olingo4` component are deprecated.