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.quarkus.jolokia;
  
  import java.io.IOException;
  import java.net.InetAddress;
  import java.net.URI;
  import java.net.UnknownHostException;
  import java.util.HashMap;
  import java.util.Map;
  import java.util.Optional;
  import java.util.function.Consumer;
  
  import io.quarkus.runtime.LaunchMode;
  import io.quarkus.runtime.RuntimeValue;
  import io.quarkus.runtime.ShutdownContext;
  import io.quarkus.runtime.annotations.Recorder;
  import io.vertx.core.Handler;
  import io.vertx.ext.web.Route;
  import io.vertx.ext.web.RoutingContext;
  import org.apache.camel.quarkus.jolokia.config.JolokiaBuildTimeConfig;
  import org.apache.camel.quarkus.jolokia.config.JolokiaRuntimeConfig;
  import org.apache.camel.quarkus.jolokia.config.JolokiaRuntimeConfig.DiscoveryEnabledMode;
  import org.apache.camel.quarkus.jolokia.config.JolokiaRuntimeConfig.Kubernetes;
  import org.apache.camel.quarkus.jolokia.config.JolokiaRuntimeConfig.Server;
  import org.apache.camel.quarkus.jolokia.restrictor.CamelJolokiaRestrictor;
  import org.apache.camel.util.CollectionHelper;
  import org.apache.camel.util.HostUtils;
  import org.apache.camel.util.ObjectHelper;
  import org.eclipse.microprofile.config.ConfigProvider;
  import org.jboss.logging.Logger;
  import org.jolokia.jvmagent.JolokiaServer;
  import org.jolokia.jvmagent.JolokiaServerConfig;
  import org.jolokia.server.core.config.ConfigKey;
  import org.jolokia.server.core.service.api.LogHandler;
  
  import static io.smallrye.common.os.Linux.isWSL;
  
  @Recorder
  public class JolokiaRecorder {
      private static final String ALL_INTERFACES = "0.0.0.0";
      private static final String LOCALHOST = "localhost";
      private static final Logger LOG = Logger.getLogger(JolokiaRecorder.class);
  
      private final JolokiaBuildTimeConfig buildTimeConfig;
      private final RuntimeValue<JolokiaRuntimeConfig> runtimeConfig;
  
      public JolokiaRecorder(JolokiaBuildTimeConfig buildTimeConfig, RuntimeValue<JolokiaRuntimeConfig> runtimeConfig) {
          this.buildTimeConfig = buildTimeConfig;
          this.runtimeConfig = runtimeConfig;
      }
  
      public Consumer<Route> route(Handler<RoutingContext> bodyHandler) {
          return new Consumer<Route>() {
              @Override
              public void accept(Route route) {
                  route.handler(bodyHandler).produces("application/json");
              }
          };
      }
  
      public RuntimeValue<JolokiaServerConfig> createJolokiaServerConfig(String applicationName) {
  
          Server server = runtimeConfig.getValue().server();
          Kubernetes kubernetes = runtimeConfig.getValue().kubernetes();
  
          // Configure Jolokia HTTP server host, port & context path
          String host = runtimeConfig.getValue().server().host().orElse(null);
          if (ObjectHelper.isEmpty(host)) {
              if (LaunchMode.current().isRemoteDev()) {
                  host = ALL_INTERFACES;
              } else if (LaunchMode.current().isDevOrTest()) {
                  if (!isWSL()) {
                      host = LOCALHOST;
                  } else {
                      host = ALL_INTERFACES;
                  }
              } else {
                  host = ALL_INTERFACES;
              }
          }
  
          Map<String, String> serverOptions = new HashMap<>();
          serverOptions.put("host", host);
          serverOptions.put("port", String.valueOf(server.port()));
          serverOptions.put(ConfigKey.AGENT_CONTEXT.getKeyValue(), "/" + buildTimeConfig.path());
  
          // Attempt Kubernetes configuration
          Optional<String> kubernetesServiceHost = ConfigProvider.getConfig().getOptionalValue("kubernetes.service.host",
                  String.class);
          if (kubernetesServiceHost.isPresent()) {
              if (kubernetes.clientAuthenticationEnabled() && kubernetes.serviceCaCert().exists()) {
                  serverOptions.put(ConfigKey.DISCOVERY_ENABLED.getKeyValue(), "false");
                  serverOptions.put("protocol", "https");
                  serverOptions.put("useSslClientAuthentication", "true");
                  serverOptions.put("extendedClientCheck", "true");
                  serverOptions.put("caCert", kubernetes.serviceCaCert().getAbsolutePath());
                  kubernetes.clientPrincipal()
                          .ifPresent(clientPrincipal -> serverOptions.put("clientPrincipal", clientPrincipal));
              } else {
                  LOG.warnf("Kubernetes service CA certificate %s does not exist", kubernetes.serviceCaCert());
              }
          }
  
          // Merge configuration with any arbitrary values provided via quarkus.camel.jolokia.additional-properties
          Map<String, String> combinedOptions = CollectionHelper.mergeMaps(serverOptions,
                  runtimeConfig.getValue().additionalProperties());
  
          // Configure CamelJolokiaRestrictor if an existing restrictor is not already provided
          if (runtimeConfig.getValue().registerCamelRestrictor()) {
              combinedOptions.putIfAbsent(ConfigKey.RESTRICTOR_CLASS.getKeyValue(), CamelJolokiaRestrictor.class.getName());
          }
  
          // Enable discovery based on the provided mode
          DiscoveryEnabledMode discoveryMode = server.discoveryEnabledMode();
          if (discoveryMode != DiscoveryEnabledMode.NONE) {
              if ((discoveryMode == DiscoveryEnabledMode.ALL)
                      || (discoveryMode == DiscoveryEnabledMode.DEV_TEST && LaunchMode.current().isDevOrTest())) {
                  combinedOptions.putIfAbsent(ConfigKey.DISCOVERY_ENABLED.getKeyValue(), "true");
              }
          }
  
          // Set a default agent description so that it shows up during agent discovery
          combinedOptions.putIfAbsent(ConfigKey.AGENT_DESCRIPTION.getKeyValue(), applicationName);
  
          return new RuntimeValue<>(new JolokiaServerConfig(combinedOptions));
      }
  
      public RuntimeValue<JolokiaServer> createJolokiaServer(RuntimeValue<JolokiaServerConfig> serverConfig) {
          try {
              CamelQuarkusJolokiaAgent agent;
              if (serverConfig.getValue().getJolokiaConfig().containsKey(ConfigKey.LOGHANDLER_CLASS)) {
                  agent = new CamelQuarkusJolokiaAgent(serverConfig.getValue());
              } else {
                  agent = new CamelQuarkusJolokiaAgent(serverConfig.getValue(), new CamelQuarkusJolokiaLogHandler());
              }
  
              return new RuntimeValue<>(agent);
          } catch (IOException e) {
              throw new RuntimeException(e);
          }
      }
  
      public void startJolokiaServer(RuntimeValue<JolokiaServer> jolokiaServer) {
          if (runtimeConfig.getValue().server().autoStart()) {
              jolokiaServer.getValue().start();
          }
      }
  
      public void registerJolokiaServerShutdownHook(RuntimeValue<JolokiaServer> jolokiaServer, ShutdownContext shutdownContext) {
          shutdownContext.addShutdownTask(() -> jolokiaServer.getValue().stop());
      }
  
      public RuntimeValue<CamelQuarkusJolokiaServer> createJolokiaServerBean(RuntimeValue<JolokiaServer> jolokiaServer) {
          return new RuntimeValue<>(new CamelQuarkusJolokiaServer(jolokiaServer.getValue()));
      }
  
      static final class CamelQuarkusJolokiaAgent extends JolokiaServer {
          CamelQuarkusJolokiaAgent(JolokiaServerConfig config) throws IOException {
              super(config);
          }
  
          CamelQuarkusJolokiaAgent(JolokiaServerConfig config, LogHandler logHandler) throws IOException {
              super(config, logHandler);
          }
      }
  
      public Handler<RoutingContext> getHandler(RuntimeValue<JolokiaServerConfig> config, String jolokiaEndpointPath) {
          JolokiaServerConfig serverConfig = config.getValue();
          String host = resolveHost(serverConfig.getAddress());
          URI uri = URI.create("%s://%s:%d%s".formatted(serverConfig.getProtocol(), host, serverConfig.getPort(),
                  serverConfig.getContextPath()));
          return new JolokiaRequestRedirectHandler(uri.normalize(), jolokiaEndpointPath);
      }
  
      public void warnOnDeprecatedRegisterManagementEndpoint() {
          LOG.warn("quarkus.camel.jolokia.register-management-endpoint is deprecated and will be removed in a future release");
      }
  
      static String resolveHost(InetAddress address) {
          String host;
          if (address == null) {
              try {
                  host = HostUtils.getLocalHostName();
              } catch (UnknownHostException e) {
                  throw new IllegalStateException("Unable to determine the Jolokia host", e);
              }
          } else {
              host = address.getHostName();
          }
  
          // ipv6 address
          if (host.contains(":")) {
              host = "[%s]".formatted(host);
          }
  
          return host;
      }
  }
  