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.

Reply via email to