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 2024/03/07 09:26:07 UTC

(camel) branch startup-api created (now 05995516d0a)

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

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


      at 05995516d0a CAMEL-20520: camel-rest-openapi - Generate OpenApi scheme once on startup

This branch includes the following new commits:

     new 05995516d0a CAMEL-20520: camel-rest-openapi - Generate OpenApi scheme once on startup

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-20520: camel-rest-openapi - Generate OpenApi scheme once on startup

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

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

commit 05995516d0aabab75207c5da4e4fde782373b398
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Thu Mar 7 10:25:52 2024 +0100

    CAMEL-20520: camel-rest-openapi - Generate OpenApi scheme once on startup
---
 .../src/main/docs/openapi-java.adoc                |  5 +-
 ...ter.java => DefaultRestApiResponseAdapter.java} | 39 +++++++++++---
 .../openapi/OpenApiRestApiProcessorFactory.java    |  4 +-
 .../camel/openapi/OpenApiRestProducerFactory.java  | 10 ++--
 .../camel/openapi/RestApiResponseAdapter.java      | 31 +++++++++++
 .../apache/camel/openapi/RestOpenApiProcessor.java | 60 +++++++++++++++++-----
 .../apache/camel/openapi/RestOpenApiSupport.java   | 27 +++++-----
 .../RestOpenApiDefaultProducesConsumesTest.java    | 10 ++++
 .../camel/openapi/RestOpenApiLicenseInfoTest.java  |  3 ++
 .../camel/openapi/RestOpenApiProcessorTest.java    | 18 +++++++
 .../impl/console/StartupRecorderDevConsole.java    | 11 +++-
 .../ROOT/pages/camel-4x-upgrade-guide-4_5.adoc     |  4 ++
 12 files changed, 177 insertions(+), 45 deletions(-)

diff --git a/components/camel-openapi-java/src/main/docs/openapi-java.adoc b/components/camel-openapi-java/src/main/docs/openapi-java.adoc
index cc022a3a8f7..107651c1279 100644
--- a/components/camel-openapi-java/src/main/docs/openapi-java.adoc
+++ b/components/camel-openapi-java/src/main/docs/openapi-java.adoc
@@ -174,9 +174,8 @@ is using OAuth security with permitted scopes of read and write pets.
 == JSon or Yaml
 
 The camel-openapi-java module supports both JSon and Yaml out of the
-box. You can specify in the request url what you want returned by using
-`/openapi.json` or `/openapi.yaml` for either one. If none is specified, then
-the HTTP Accept header is used to detect if json or yaml can be
+box. You can specify in the request url what you want by using `.json` or `.yaml` as suffix in the context-path
+If none is specified, then the HTTP Accept header is used to detect if json or yaml can be
 accepted. If either both are accepted or none was set as accepted, then
 json is returned as the default format.
 
diff --git a/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/ExchangeRestApiResponseAdapter.java b/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/DefaultRestApiResponseAdapter.java
similarity index 52%
rename from components/camel-openapi-java/src/main/java/org/apache/camel/openapi/ExchangeRestApiResponseAdapter.java
rename to components/camel-openapi-java/src/main/java/org/apache/camel/openapi/DefaultRestApiResponseAdapter.java
index ff05f96b8ce..a2d24298a53 100644
--- a/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/ExchangeRestApiResponseAdapter.java
+++ b/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/DefaultRestApiResponseAdapter.java
@@ -17,29 +17,54 @@
 package org.apache.camel.openapi;
 
 import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
 
+import io.swagger.v3.oas.models.OpenAPI;
 import org.apache.camel.Exchange;
 
-public class ExchangeRestApiResponseAdapter implements RestApiResponseAdapter {
+/**
+ * A {@link RestApiResponseAdapter} that caches the response.
+ */
+public class DefaultRestApiResponseAdapter implements RestApiResponseAdapter {
+
+    private final Map<String, String> headers = new LinkedHashMap<>();
+    private byte[] body;
+    private boolean noContent;
+    private OpenAPI openApi;
 
-    private final Exchange exchange;
+    public OpenAPI getOpenApi() {
+        return openApi;
+    }
 
-    public ExchangeRestApiResponseAdapter(Exchange exchange) {
-        this.exchange = exchange;
+    public void setOpenApi(OpenAPI openApi) {
+        this.openApi = openApi;
     }
 
     @Override
     public void setHeader(String name, String value) {
-        exchange.getIn().setHeader(name, value);
+        headers.put(name, value);
     }
 
     @Override
     public void writeBytes(byte[] bytes) throws IOException {
-        exchange.getIn().setBody(bytes);
+        this.body = bytes;
     }
 
     @Override
     public void noContent() {
-        exchange.getIn().setHeader(Exchange.HTTP_RESPONSE_CODE, 204);
+        this.noContent = true;
+    }
+
+    public void copyResult(Exchange exchange) {
+        if (!headers.isEmpty()) {
+            exchange.getMessage().getHeaders().putAll(headers);
+        }
+        if (body != null) {
+            exchange.getMessage().setBody(body);
+        }
+        if (noContent) {
+            exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 204);
+        }
     }
 }
diff --git a/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/OpenApiRestApiProcessorFactory.java b/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/OpenApiRestApiProcessorFactory.java
index cb046a054bd..cd7fe5b1404 100644
--- a/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/OpenApiRestApiProcessorFactory.java
+++ b/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/OpenApiRestApiProcessorFactory.java
@@ -78,6 +78,8 @@ public class OpenApiRestApiProcessorFactory implements RestApiProcessorFactory {
             options.put("cors", "true");
         }
 
-        return new RestOpenApiProcessor(options, configuration);
+        RestOpenApiProcessor answer = new RestOpenApiProcessor(options, configuration);
+        answer.setCamelContext(camelContext);
+        return answer;
     }
 }
diff --git a/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/OpenApiRestProducerFactory.java b/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/OpenApiRestProducerFactory.java
index f03ae5c0273..a1b8621e585 100644
--- a/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/OpenApiRestProducerFactory.java
+++ b/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/OpenApiRestProducerFactory.java
@@ -27,6 +27,7 @@ import io.swagger.v3.oas.models.responses.ApiResponse;
 import io.swagger.v3.parser.OpenAPIV3Parser;
 import io.swagger.v3.parser.core.models.SwaggerParseResult;
 import org.apache.camel.CamelContext;
+import org.apache.camel.CamelContextAware;
 import org.apache.camel.Producer;
 import org.apache.camel.spi.RestConfiguration;
 import org.apache.camel.spi.RestProducerFactory;
@@ -90,7 +91,6 @@ public class OpenApiRestProducerFactory implements RestProducerFactory {
         final SwaggerParseResult openApi = openApiParser.readLocation(apiDoc, null, null);
 
         if (openApi != null && openApi.getOpenAPI() != null) {
-            //   checkV2specification(openApi.getOpenAPI(), uri);
             return openApi.getOpenAPI();
         }
 
@@ -161,12 +161,10 @@ public class OpenApiRestProducerFactory implements RestProducerFactory {
             String basePath;
             String uriTemplate;
             if (host == null) {
-
                 //if no explicit host has been configured then use host and base path from the openApi api-doc
                 host = RestOpenApiSupport.getHostFromOasDocument(openApi);
                 basePath = RestOpenApiSupport.getBasePathFromOasDocument(openApi);
                 uriTemplate = path;
-
             } else {
                 // path includes also uri template
                 basePath = path;
@@ -174,9 +172,11 @@ public class OpenApiRestProducerFactory implements RestProducerFactory {
             }
 
             RestConfiguration config = CamelContextHelper.getRestConfiguration(camelContext, null, componentName);
-            return factory.createProducer(camelContext, host, verb, basePath, uriTemplate, queryParameters, consumes, produces,
+            Producer answer = factory.createProducer(camelContext, host, verb, basePath, uriTemplate, queryParameters, consumes,
+                    produces,
                     config, parameters);
-
+            CamelContextAware.trySetCamelContext(answer, camelContext);
+            return answer;
         } else {
             throw new IllegalStateException("Cannot find RestProducerFactory in Registry or as a Component to use");
         }
diff --git a/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestApiResponseAdapter.java b/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestApiResponseAdapter.java
index 5db130b5971..32bee18a8df 100644
--- a/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestApiResponseAdapter.java
+++ b/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestApiResponseAdapter.java
@@ -18,11 +18,42 @@ package org.apache.camel.openapi;
 
 import java.io.IOException;
 
+import io.swagger.v3.oas.models.OpenAPI;
+import org.apache.camel.Exchange;
+
+/**
+ * Adapter for rendering API response
+ */
 public interface RestApiResponseAdapter {
 
+    /**
+     * Sets the generated OpenAPI model
+     */
+    void setOpenApi(OpenAPI openApi);
+
+    /**
+     * Gets the generated OpenAPI model
+     */
+    OpenAPI getOpenApi();
+
+    /**
+     * Adds a header
+     */
     void setHeader(String name, String value);
 
+    /**
+     * The content of the OpenAPI spec as byte array
+     */
     void writeBytes(byte[] bytes) throws IOException;
 
+    /**
+     * There is no Rest DSL and therefore no OpenAPI spec
+     */
     void noContent();
+
+    /**
+     * Copy content from this adapter into the given {@link Exchange}.
+     */
+    void copyResult(Exchange exchange);
+
 }
diff --git a/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestOpenApiProcessor.java b/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestOpenApiProcessor.java
index 6f38d83b1b5..fbb23e17f32 100644
--- a/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestOpenApiProcessor.java
+++ b/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestOpenApiProcessor.java
@@ -19,16 +19,23 @@ package org.apache.camel.openapi;
 import java.util.Collections;
 import java.util.Locale;
 import java.util.Map;
+import java.util.concurrent.RejectedExecutionException;
 
+import org.apache.camel.CamelContext;
+import org.apache.camel.CamelContextAware;
 import org.apache.camel.Exchange;
 import org.apache.camel.Processor;
+import org.apache.camel.StartupStep;
 import org.apache.camel.spi.RestConfiguration;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.apache.camel.spi.StartupStepRecorder;
+import org.apache.camel.support.service.ServiceSupport;
+import org.apache.camel.util.ObjectHelper;
 
-public class RestOpenApiProcessor implements Processor {
+public class RestOpenApiProcessor extends ServiceSupport implements Processor, CamelContextAware {
 
-    private static final Logger LOG = LoggerFactory.getLogger(RestOpenApiProcessor.class);
+    private final RestApiResponseAdapter jsonAdapter = new DefaultRestApiResponseAdapter();
+    private final RestApiResponseAdapter yamlAdapter = new DefaultRestApiResponseAdapter();
+    private CamelContext camelContext;
     private final BeanConfig openApiConfig;
     private final RestOpenApiSupport support;
     private final RestConfiguration configuration;
@@ -45,20 +52,48 @@ public class RestOpenApiProcessor implements Processor {
         support.initOpenApi(openApiConfig, parameters);
     }
 
+    @Override
+    public CamelContext getCamelContext() {
+        return camelContext;
+    }
+
+    @Override
+    public void setCamelContext(CamelContext camelContext) {
+        this.camelContext = camelContext;
+    }
+
+    @Override
+    protected void doInit() throws Exception {
+        ObjectHelper.notNull(camelContext, "CamelContext", this);
+
+        StartupStepRecorder recorder = camelContext.getCamelContextExtension().getStartupStepRecorder();
+        StartupStep step = recorder.beginStep(RestOpenApiProcessor.class, "openapi", "Generating OpenAPI specification");
+        try {
+            support.renderResourceListing(camelContext, jsonAdapter, openApiConfig, true,
+                    camelContext.getClassResolver(), configuration);
+            yamlAdapter.setOpenApi(jsonAdapter.getOpenApi()); // no need to compute OpenApi again
+            support.renderResourceListing(camelContext, yamlAdapter, openApiConfig, false,
+                    camelContext.getClassResolver(), configuration);
+        } finally {
+            recorder.endStep(step);
+        }
+    }
+
     @Override
     public void process(Exchange exchange) throws Exception {
+        if (!isRunAllowed()) {
+            throw new RejectedExecutionException();
+        }
 
         String route = exchange.getIn().getHeader(Exchange.HTTP_PATH, String.class);
         String accept = exchange.getIn().getHeader("Accept", String.class);
 
-        RestApiResponseAdapter adapter = new ExchangeRestApiResponseAdapter(exchange);
-
         // whether to use json or yaml
         boolean json = false;
         boolean yaml = false;
-        if (route != null && route.endsWith("/openapi.json")) {
+        if (route != null && route.endsWith(".json")) {
             json = true;
-        } else if (route != null && route.endsWith("/openapi.yaml")) {
+        } else if (route != null && route.endsWith(".yaml")) {
             yaml = true;
         }
         if (accept != null && !json && !yaml) {
@@ -70,12 +105,11 @@ public class RestOpenApiProcessor implements Processor {
             json = true;
         }
 
-        try {
-            support.renderResourceListing(exchange.getContext(), adapter, openApiConfig, json,
-                    exchange.getIn().getHeaders(), exchange.getContext().getClassResolver(), configuration);
-        } catch (Exception e) {
-            LOG.warn("Error rendering OpenApi API due {}", e.getMessage(), e);
+        RestApiResponseAdapter adapter = json ? jsonAdapter : yamlAdapter;
+        if (configuration.isUseXForwardHeaders()) {
+            support.setupXForwardHeaders(adapter, exchange);
         }
+        adapter.copyResult(exchange);
     }
 
 }
diff --git a/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestOpenApiSupport.java b/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestOpenApiSupport.java
index 441ab9bf063..4c77fba1434 100644
--- a/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestOpenApiSupport.java
+++ b/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestOpenApiSupport.java
@@ -105,22 +105,17 @@ public class RestOpenApiSupport {
     }
 
     static void setupXForwardedHeaders(OpenAPI openApi, Map<String, Object> headers) {
-
         String basePath = getBasePathFromOasDocument(openApi);
-
         String host = (String) headers.get(HEADER_HOST);
-
         String forwardedPrefix = (String) headers.get(HEADER_X_FORWARDED_PREFIX);
 
         if (ObjectHelper.isNotEmpty(forwardedPrefix)) {
             basePath = URISupport.joinPaths(forwardedPrefix, basePath);
         }
-
         String forwardedHost = (String) headers.get(HEADER_X_FORWARDED_HOST);
         if (ObjectHelper.isNotEmpty(forwardedHost)) {
             host = forwardedHost;
         }
-
         String proto = (String) headers.get(HEADER_X_FORWARDED_PROTO);
         if (openApi.getServers() != null) {
             openApi.getServers().clear();
@@ -290,12 +285,17 @@ public class RestOpenApiSupport {
                         () -> new IllegalArgumentException("Cannot find camel-openapi-java on classpath."));
     }
 
+    public void setupXForwardHeaders(RestApiResponseAdapter response, Exchange exchange) {
+        setupXForwardedHeaders(response.getOpenApi(), exchange.getMessage().getHeaders());
+    }
+
     public void renderResourceListing(
             CamelContext camelContext, RestApiResponseAdapter response,
             BeanConfig openApiConfig, boolean json,
-            Map<String, Object> headers, ClassResolver classResolver,
+            ClassResolver classResolver,
             RestConfiguration configuration)
             throws Exception {
+
         LOG.trace("renderResourceListing");
 
         if (cors) {
@@ -303,7 +303,6 @@ public class RestOpenApiSupport {
         }
 
         List<RestDefinition> rests = getRestDefinitions(camelContext);
-
         if (rests != null) {
             final Map<String, Object> apiProperties = configuration.getApiProperties() != null
                     ? configuration.getApiProperties() : new HashMap<>();
@@ -311,14 +310,14 @@ public class RestOpenApiSupport {
             String defaultValue = json ? "application/json" : "text/yaml";
             response.setHeader(Exchange.CONTENT_TYPE, (String) apiProperties.getOrDefault(key, defaultValue));
             // read the rest-dsl into openApi model
-            OpenAPI openApi = reader.read(
-                    camelContext, rests, openApiConfig, camelContext.getName(), classResolver);
-            if (configuration.isUseXForwardHeaders()) {
-                setupXForwardedHeaders(openApi, headers);
-            }
-            if (!configuration.isApiVendorExtension()) {
-                clearVendorExtensions(openApi);
+            OpenAPI openApi = response.getOpenApi();
+            if (openApi == null) {
+                openApi = reader.read(camelContext, rests, openApiConfig, camelContext.getName(), classResolver);
+                if (!configuration.isApiVendorExtension()) {
+                    clearVendorExtensions(openApi);
+                }
             }
+            response.setOpenApi(openApi);
             byte[] bytes = getFromOpenAPI(openApi, openApiConfig, byte[].class, json);
             int len = bytes.length;
             response.setHeader(Exchange.CONTENT_LENGTH, Integer.toString(len));
diff --git a/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/RestOpenApiDefaultProducesConsumesTest.java b/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/RestOpenApiDefaultProducesConsumesTest.java
index 9b4a8b547ff..7ff5935bab0 100644
--- a/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/RestOpenApiDefaultProducesConsumesTest.java
+++ b/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/RestOpenApiDefaultProducesConsumesTest.java
@@ -70,6 +70,8 @@ public class RestOpenApiDefaultProducesConsumesTest {
         RestConfiguration restConfiguration = context.getRestConfiguration();
         RestOpenApiProcessor processor
                 = new RestOpenApiProcessor(restConfiguration.getApiProperties(), restConfiguration);
+        processor.setCamelContext(context);
+        processor.start();
         Exchange exchange = new DefaultExchange(context);
         processor.process(exchange);
 
@@ -104,6 +106,8 @@ public class RestOpenApiDefaultProducesConsumesTest {
         RestConfiguration restConfiguration = context.getRestConfiguration();
         RestOpenApiProcessor processor
                 = new RestOpenApiProcessor(restConfiguration.getApiProperties(), restConfiguration);
+        processor.setCamelContext(context);
+        processor.start();
         Exchange exchange = new DefaultExchange(context);
         processor.process(exchange);
 
@@ -141,6 +145,8 @@ public class RestOpenApiDefaultProducesConsumesTest {
         RestConfiguration restConfiguration = context.getRestConfiguration();
         RestOpenApiProcessor processor
                 = new RestOpenApiProcessor(restConfiguration.getApiProperties(), restConfiguration);
+        processor.setCamelContext(context);
+        processor.start();
         Exchange exchange = new DefaultExchange(context);
         processor.process(exchange);
 
@@ -189,6 +195,8 @@ public class RestOpenApiDefaultProducesConsumesTest {
         RestConfiguration restConfiguration = context.getRestConfiguration();
         RestOpenApiProcessor processor
                 = new RestOpenApiProcessor(restConfiguration.getApiProperties(), restConfiguration);
+        processor.setCamelContext(context);
+        processor.start();
         Exchange exchange = new DefaultExchange(context);
         processor.process(exchange);
 
@@ -237,6 +245,8 @@ public class RestOpenApiDefaultProducesConsumesTest {
         RestConfiguration restConfiguration = context.getRestConfiguration();
         RestOpenApiProcessor processor
                 = new RestOpenApiProcessor(restConfiguration.getApiProperties(), restConfiguration);
+        processor.setCamelContext(context);
+        processor.start();
         Exchange exchange = new DefaultExchange(context);
         processor.process(exchange);
 
diff --git a/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/RestOpenApiLicenseInfoTest.java b/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/RestOpenApiLicenseInfoTest.java
index 51493db6f17..061d979aa9e 100644
--- a/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/RestOpenApiLicenseInfoTest.java
+++ b/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/RestOpenApiLicenseInfoTest.java
@@ -54,6 +54,9 @@ public class RestOpenApiLicenseInfoTest {
         RestConfiguration restConfiguration = context.getRestConfiguration();
         RestOpenApiProcessor processor
                 = new RestOpenApiProcessor(restConfiguration.getApiProperties(), restConfiguration);
+        processor.setCamelContext(context);
+        processor.start();
+
         Exchange exchange = new DefaultExchange(context);
         processor.process(exchange);
 
diff --git a/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/RestOpenApiProcessorTest.java b/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/RestOpenApiProcessorTest.java
index 2f5c9f03099..b83f6287cda 100644
--- a/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/RestOpenApiProcessorTest.java
+++ b/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/RestOpenApiProcessorTest.java
@@ -45,6 +45,9 @@ public class RestOpenApiProcessorTest {
         });
 
         RestOpenApiProcessor processor = new RestOpenApiProcessor(null, context.getRestConfiguration());
+        processor.setCamelContext(context);
+        processor.start();
+
         Exchange exchange = new DefaultExchange(context);
         processor.process(exchange);
 
@@ -68,6 +71,9 @@ public class RestOpenApiProcessorTest {
         });
 
         RestOpenApiProcessor processor = new RestOpenApiProcessor(null, context.getRestConfiguration());
+        processor.setCamelContext(context);
+        processor.start();
+
         Exchange exchange = new DefaultExchange(context);
         exchange.getMessage().setHeader(Exchange.HTTP_PATH, "/openapi.json");
         processor.process(exchange);
@@ -93,6 +99,9 @@ public class RestOpenApiProcessorTest {
         });
 
         RestOpenApiProcessor processor = new RestOpenApiProcessor(null, context.getRestConfiguration());
+        processor.setCamelContext(context);
+        processor.start();
+
         Exchange exchange = new DefaultExchange(context);
         exchange.getMessage().setHeader(Exchange.HTTP_PATH, "/openapi.yaml");
         processor.process(exchange);
@@ -122,6 +131,9 @@ public class RestOpenApiProcessorTest {
         });
 
         RestOpenApiProcessor processor = new RestOpenApiProcessor(null, context.getRestConfiguration());
+        processor.setCamelContext(context);
+        processor.start();
+
         Exchange exchange = new DefaultExchange(context);
         exchange.getMessage().setHeader(Exchange.HTTP_PATH, "/openapi.yaml");
         processor.process(exchange);
@@ -147,6 +159,9 @@ public class RestOpenApiProcessorTest {
         });
 
         RestOpenApiProcessor processor = new RestOpenApiProcessor(null, context.getRestConfiguration());
+        processor.setCamelContext(context);
+        processor.start();
+
         Exchange exchange = new DefaultExchange(context);
         exchange.getMessage().setHeader(Exchange.HTTP_PATH, "/");
         exchange.getMessage().setHeader("Accept", "application/json");
@@ -173,6 +188,9 @@ public class RestOpenApiProcessorTest {
         });
 
         RestOpenApiProcessor processor = new RestOpenApiProcessor(null, context.getRestConfiguration());
+        processor.setCamelContext(context);
+        processor.start();
+
         Exchange exchange = new DefaultExchange(context);
         exchange.getMessage().setHeader(Exchange.HTTP_PATH, "/");
         exchange.getMessage().setHeader("Accept", "application/yaml");
diff --git a/core/camel-console/src/main/java/org/apache/camel/impl/console/StartupRecorderDevConsole.java b/core/camel-console/src/main/java/org/apache/camel/impl/console/StartupRecorderDevConsole.java
index 0f527caa2f5..c67b8499fc6 100644
--- a/core/camel-console/src/main/java/org/apache/camel/impl/console/StartupRecorderDevConsole.java
+++ b/core/camel-console/src/main/java/org/apache/camel/impl/console/StartupRecorderDevConsole.java
@@ -62,7 +62,9 @@ public class StartupRecorderDevConsole extends AbstractDevConsole {
                 jo.put("id", s.getId());
                 jo.put("parentId", s.getParentId());
                 jo.put("level", s.getLevel());
-                jo.put("name", s.getName());
+                if (s.getName() != null) {
+                    jo.put("name", s.getName());
+                }
                 jo.put("type", s.getType());
                 jo.put("description", s.getDescription());
                 jo.put("beginTime", s.getBeginTime());
@@ -82,7 +84,12 @@ public class StartupRecorderDevConsole extends AbstractDevConsole {
         String pad = StringHelper.padString(step.getLevel());
         String out = String.format("%s", pad + step.getType());
         String out2 = String.format("%6s ms", delta);
-        String out3 = String.format("%s(%s)", step.getDescription(), step.getName());
+        String out3;
+        if (step.getName() != null) {
+            out3 = String.format("%s (%s)", step.getDescription(), step.getName());
+        } else {
+            out3 = String.format("%s", step.getDescription());
+        }
         return String.format("%s : %s - %s", out2, out, out3);
     }
 
diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_5.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_5.adoc
index f783da38174..be0f88e6e82 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_5.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_5.adoc
@@ -172,6 +172,10 @@ camel.metrics.routePolicyLevel=route
 Dropped support for the old Swagger 2.0 spec. Only OpenAPI v3 specs is supported now.
 Fixed maven dependencies to be JakartaEE compatible.
 
+When using Rest DSL and have `api-doc` enabled via `camel-rest` and `camel-openapi-java`, then
+the OpenAPI specification is now generated once during startup instead of on-demand when a client
+calls the `/api-doc` endpoint.
+
 === camel-platform-http-vertx
 
 Added a Cookie Handler allowing the addition, retrieval and expiry of Cookies.