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 <[email protected]> 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.
