content: /*
   * Licensed to the Apache Software Foundation (ASF) under one or more
   * contributor license agreements.  See the NOTICE file distributed with
   * this work for additional information regarding copyright ownership.
   * The ASF licenses this file to You under the Apache License, Version 2.0
   * (the "License"); you may not use this file except in compliance with
   * the License.  You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package org.apache.camel.springboot.springdoc;
  
  import io.swagger.v3.oas.models.OpenAPI;
  import io.swagger.v3.oas.models.info.Contact;
  import io.swagger.v3.oas.models.info.Info;
  import io.swagger.v3.oas.models.servers.Server;
  
  import org.apache.camel.CamelContext;
  import org.apache.camel.model.rest.RestDefinition;
  import org.apache.camel.openapi.BeanConfig;
  import org.apache.camel.openapi.DefaultRestDefinitionsResolver;
  import org.apache.camel.openapi.RestDefinitionsResolver;
  import org.apache.camel.openapi.RestOpenApiReader;
  import org.apache.camel.spi.RestConfiguration;
  import org.apache.camel.spring.boot.CamelContextConfiguration;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  import org.springframework.beans.factory.annotation.Value;
  import org.springframework.boot.autoconfigure.AutoConfigureAfter;
  import org.springframework.boot.autoconfigure.AutoConfigureBefore;
  import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
  import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  import org.springframework.boot.context.properties.EnableConfigurationProperties;
  import org.springframework.context.annotation.Bean;
  import org.springframework.context.annotation.Configuration;
  import org.springframework.context.support.GenericApplicationContext;
  
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Optional;
  import java.util.function.BiConsumer;
  import java.util.function.Consumer;
  
  import static org.apache.camel.openapi.OpenApiHelper.clearVendorExtensions;
  
  /**
   * Springdoc auto-configuration.
   */
  @Configuration(proxyBeanMethods = false)
  @EnableConfigurationProperties({ SpringdocConfiguration.class })
  @ConditionalOnBean(type = "org.apache.camel.spring.boot.CamelAutoConfiguration")
  @ConditionalOnProperty(name = "camel.springdoc.enabled", matchIfMissing = true)
  @AutoConfigureAfter(name = "org.apache.camel.spring.boot.CamelAutoConfiguration")
  @AutoConfigureBefore(name = "org.springdoc.core.SpringdocConfiguration")
  public class SpringdocAutoConfiguration {
  
      private static final Logger LOG = LoggerFactory.getLogger(SpringdocAutoConfiguration.class);
  
      private final RestOpenApiReader reader = new RestOpenApiReader();
      private final RestDefinitionsResolver resolver = new DefaultRestDefinitionsResolver();
      private final OpenAPI openapi = new OpenAPI();
  
      @Value("${server.servlet.context-path:}")
      private String springContextPath;
  
      @Bean
      CamelContextConfiguration springdocOnBeforeStart(final GenericApplicationContext ac,
              final CamelContext camelContext, final SpringdocConfiguration config) {
          return new CamelContextConfiguration() {
              @Override
              public void beforeApplicationStart(final CamelContext camelContext) {
                  // routes have now been loaded, so we need to detect rest-dsl APIs in Camel
                  // this will trigger spring boot to create the bean which springdoc can detect
                  try {
                      Optional.ofNullable(createOpenAPI(camelContext)).ifPresent(created -> {
                          LOG.info("OpenAPI ({}) created from Camel Rest-DSL v{} - {}", created.getOpenapi(),
                                  created.getInfo().getVersion(), created.getInfo().getTitle());
                          // transfer data to the existing OpenAPI instance
                          Optional.ofNullable(created.getInfo()).ifPresent(openapi::setInfo);
                          Optional.ofNullable(created.getOpenapi()).ifPresent(openapi::setOpenapi);
                          Optional.ofNullable(created.getComponents()).ifPresent(openapi::setComponents);
                          Optional.ofNullable(created.getExtensions()).ifPresent(openapi::setExtensions);
                          Optional.ofNullable(created.getSecurity()).ifPresent(openapi::setSecurity);
                          Optional.ofNullable(created.getExternalDocs()).ifPresent(openapi::setExternalDocs);
                          Optional.ofNullable(created.getPaths()).ifPresent(openapi::setPaths);
                          Optional.ofNullable(created.getTags()).ifPresent(openapi::setTags);
                          Optional.ofNullable(created.getServers()).ifPresent(openapi::setServers);
                      });
                  } catch (Exception e) {
                      LOG.warn("Error generating OpenAPI from Camel Rest DSL due to: {}. This exception is ignored.",
                              e.getMessage(), e);
                  }
              }
  
              @Override
              public void afterApplicationStart(final CamelContext camelContext) {
                  // noop
              }
          };
      }
  
      @Bean
      OpenAPI camelRestDSLOpenApi() {
          // due to ordering how beans are resolved and setup in spring boot, then we need to create
          // this provisional bean which is later updated with the actual Rest DSL APIs after the routes has been loaded
          // into Camel
          return openapi;
      }
  
      private OpenAPI createOpenAPI(final CamelContext camelContext) throws Exception {
          final List<RestDefinition> rests = resolver.getRestDefinitions(camelContext, null);
          if (rests == null || rests.isEmpty()) {
              return null;
          }
  
          final BeanConfig bc = new BeanConfig();
          final Info info = new Info();
          bc.setInfo(info);
          final RestConfiguration rc = camelContext.getRestConfiguration();
          Map<String, Object> apiProps = Optional.ofNullable(rc.getApiProperties()).orElseGet(HashMap::new);
          initOpenApi(bc, info, apiProps, getBasePath(springContextPath, apiProps.get("base.path"), rc.getContextPath()));
  
          final OpenAPI openApi = reader.read(camelContext, rests, bc, null, camelContext.getClassResolver());
          if (!rc.isApiVendorExtension()) {
              clearVendorExtensions(openApi);
          }
          // Set relative path in URL if basepath is set
          if (bc.getBasePath() != null && !bc.getBasePath().isEmpty()
          /* && openApi.getSpecVersion().equals(SpecVersion) */) {
              for (Server server : openApi.getServers()) {
                  if (server.getUrl().endsWith(bc.getBasePath())) {
                      LOG.info("Setting Server URL in ApiDoc to base path: {}", bc.getBasePath());
                      server.setUrl(bc.getBasePath());
                  }
              }
          }
  
          return openApi;
      }
  
      /**
       * Return the basePath for the REST services
       *
       * @param springContextPath
       *            the spring context path if set or empty
       * @param basePath
       *            The apiProperty "base.path" from the REST configuration
       * @param contextPath
       *            the REST contextPath or null. Used instead of basePath if both are non-null.
       *
       * @return the combined contextPath
       */
      private String getBasePath(String springContextPath, Object basePath, String contextPath) {
          if (contextPath == null) {
              contextPath = (String) basePath; // could still be null
          }
          if (contextPath != null && !contextPath.isEmpty()) {
              return springContextPath + contextPath;
          } else {
              // This will cause problems when using the Camel servlet as the REST component!
              LOG.warn("No REST context path set in Camel Rest-DSL!");
              return springContextPath;
          }
      }
  
      /**
       * This consumes a property object, if non-null, by converting it to a string and running the given string consumer
       * on the value.
       */
      private static final BiConsumer<Object, Consumer<String>> consumeProperty = (a, b) -> Optional.ofNullable(a)
              .map(String.class::cast).ifPresent(b);
  
      private static void initOpenApi(BeanConfig bc, Info info, Map<String, Object> config, String basePath) {
          // configure openApi options
          consumeProperty.accept(config.get("openapi.version"), bc::setVersion);
          consumeProperty.accept(basePath, bc::setBasePath);
          consumeProperty.accept(config.get("host"), bc::setHost);
          consumeProperty.accept(config.get("api.version"), info::setVersion);
          consumeProperty.accept(config.get("api.description"), info::setDescription);
          consumeProperty.accept(config.get("api.termsOfService"), info::setTermsOfService);
          consumeProperty.accept(config.get("api.license.name"), bc::setLicense);
          consumeProperty.accept(config.get("api.license.url"), bc::setLicenseUrl);
          consumeProperty.accept(config.get("api.title"), s -> {
              bc.setTitle(s);
              info.setTitle(s);
          });
          Optional.of(config.getOrDefault("schemes", config.getOrDefault("schemas", "http"))).map(String.class::cast)
                  .map(v -> v.split(",")).ifPresent(bc::setSchemes);
          Optional.ofNullable(config.get("api.contact.name")).map(String.class::cast).map(name -> new Contact().name(name)
                  .email((String) config.get("api.contact.email")).url((String) config.get("api.contact.url")))
                  .ifPresent(info::setContact);
      }
  }
  