You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/02/07 14:12:17 UTC

(camel) branch cf created (now 5ab955b4f8c9)

This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a change to branch cf
in repository https://gitbox.apache.org/repos/asf/camel.git


      at 5ab955b4f8c9 CAMEL-22971: camel-platform-http-vertx: Using rest-dsl contract-first should use fine grained vertx-web router

This branch includes the following new commits:

     new 5ab955b4f8c9 CAMEL-22971: camel-platform-http-vertx: Using rest-dsl contract-first should use fine grained vertx-web router

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(camel) 01/01: CAMEL-22971: camel-platform-http-vertx: Using rest-dsl contract-first should use fine grained vertx-web router

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch cf
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 5ab955b4f8c9708dc91faefed5909ae44a72b6e5
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Sat Feb 7 15:12:01 2026 +0100

    CAMEL-22971: camel-platform-http-vertx: Using rest-dsl contract-first should use fine grained vertx-web router
---
 .../http/vertx/VertxPlatformHttpConsumer.java      | 101 ++++++++++++++++++---
 ...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    |  26 ++++++
 .../services/org/apache/camel/model.properties     |   3 +
 .../ROOT/pages/camel-4x-upgrade-guide-4_18.adoc    |  10 ++
 8 files changed, 179 insertions(+), 25 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..3168bd64b4e4 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;
@@ -81,7 +82,7 @@ public class VertxPlatformHttpConsumer extends DefaultConsumer
     private final boolean handleWriteResponseError;
     private Set<Method> methods;
     private String path;
-    private Route route;
+    private final List<Route> routes = new ArrayList<>();
     private VertxPlatformHttpRouter router;
     private HttpRequestBodyHandler httpRequestBodyHandler;
     private CookieConfiguration cookieConfiguration;
@@ -135,20 +136,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,26 +166,98 @@ 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()) {
+            String target = path;
+            if (target.endsWith("*")) {
+                target = target.substring(0, target.length() - 1);
+            }
+            if (r.isContractFirst() && target.equals(r.getBasePath())) {
+                matched = true;
+                // contract-first, then lets build up fine-grained router for vertx
+                String u = r.getBasePath() + r.getBaseUrl();
+                // in vertx-web we should replace path parameters from {xxx} to :xxx syntax
+                u = u.replaceAll("\\{([a-zA-Z0-9]+)\\}", ":$1");
+                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()) {
+            String target = path;
+            if (target.endsWith("*")) {
+                target = target.substring(0, target.length() - 1);
+            }
+            if (r.isSpecification() && target.equals(r.getBasePath())) {
+                // contract-first, then lets build up fine-grained router for vertx
+                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("*")) {
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..aad6ad1119bf 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,27 @@ 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/core/camel-core-model/src/generated/resources/META-INF/services/org/apache/camel/model.properties b/core/camel-core-model/src/generated/resources/META-INF/services/org/apache/camel/model.properties
index 2607d21e1e52..9a641b9986d3 100644
--- a/core/camel-core-model/src/generated/resources/META-INF/services/org/apache/camel/model.properties
+++ b/core/camel-core-model/src/generated/resources/META-INF/services/org/apache/camel/model.properties
@@ -222,6 +222,7 @@ threads
 thrift
 throttle
 throwException
+tidyMarkup
 to
 toD
 tokenize
@@ -231,6 +232,7 @@ transacted
 transform
 transformDataType
 transformers
+typeFilter
 univocityCsv
 univocityFixed
 univocityHeader
@@ -243,6 +245,7 @@ variable
 wasm
 weightedLoadBalancer
 when
+whenSkipSendToEndpoint
 wireTap
 xmlSecurity
 xpath
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.