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.deployment;
  
  import java.io.IOException;
  import java.nio.file.Files;
  import java.util.Collections;
  import java.util.HashSet;
  import java.util.Set;
  import java.util.function.BooleanSupplier;
  import java.util.stream.Collectors;
  
  import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
  import io.quarkus.deployment.Capabilities;
  import io.quarkus.deployment.Capability;
  import io.quarkus.deployment.IsDevelopment;
  import io.quarkus.deployment.IsNormal;
  import io.quarkus.deployment.annotations.BuildProducer;
  import io.quarkus.deployment.annotations.BuildStep;
  import io.quarkus.deployment.annotations.BuildSteps;
  import io.quarkus.deployment.annotations.ExecutionTime;
  import io.quarkus.deployment.annotations.Record;
  import io.quarkus.deployment.builditem.ApplicationInfoBuildItem;
  import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
  import io.quarkus.deployment.builditem.FeatureBuildItem;
  import io.quarkus.deployment.builditem.IndexDependencyBuildItem;
  import io.quarkus.deployment.builditem.LaunchModeBuildItem;
  import io.quarkus.deployment.builditem.NativeMonitoringBuildItem;
  import io.quarkus.deployment.builditem.ShutdownContextBuildItem;
  import io.quarkus.deployment.builditem.ShutdownListenerBuildItem;
  import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem;
  import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
  import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem;
  import io.quarkus.deployment.pkg.NativeConfig;
  import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem;
  import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild;
  import io.quarkus.kubernetes.spi.KubernetesPortBuildItem;
  import io.quarkus.maven.dependency.ResolvedDependency;
  import io.quarkus.paths.PathFilter;
  import io.quarkus.paths.PathVisit;
  import io.quarkus.paths.PathVisitor;
  import io.quarkus.runtime.LaunchMode;
  import io.quarkus.vertx.http.deployment.BodyHandlerBuildItem;
  import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem;
  import io.quarkus.vertx.http.deployment.RouteBuildItem;
  import jakarta.enterprise.context.ApplicationScoped;
  import org.apache.camel.quarkus.jolokia.CamelQuarkusJolokiaServer;
  import org.apache.camel.quarkus.jolokia.JolokiaRecorder;
  import org.apache.camel.quarkus.jolokia.config.JolokiaBuildTimeConfig;
  import org.apache.camel.quarkus.jolokia.devmode.DevModeJolokiaServerShutdownListener;
  import org.apache.camel.quarkus.jolokia.restrictor.CamelJolokiaRestrictor;
  import org.apache.camel.util.ObjectHelper;
  import org.apache.camel.util.StringHelper;
  import org.jboss.jandex.ClassInfo;
  import org.jboss.jandex.DotName;
  import org.jolokia.server.core.service.api.LogHandler;
  import org.jolokia.server.core.service.api.Restrictor;
  import org.jolokia.server.core.service.impl.QuietLogHandler;
  import org.jolokia.server.core.service.serializer.Serializer;
  import org.jolokia.service.history.History;
  import org.jolokia.service.history.HistoryMBean;
  
  @BuildSteps(onlyIf = JolokiaProcessor.JolokiaEnabled.class)
  public class JolokiaProcessor {
      private static final String FEATURE = "camel-jolokia";
  
      @BuildStep
      FeatureBuildItem feature() {
          return new FeatureBuildItem(FEATURE);
      }
  
      @BuildStep
      @Record(ExecutionTime.RUNTIME_INIT)
      JolokiaServerConfigBuildItem createJolokiaServerConfig(
              ApplicationInfoBuildItem applicationInfo,
              JolokiaRecorder recorder) {
          return new JolokiaServerConfigBuildItem(recorder.createJolokiaServerConfig(applicationInfo.getName()));
      }
  
      @BuildStep
      @Record(ExecutionTime.RUNTIME_INIT)
      JolokiaServerBuildItem createJolokiaServer(
              JolokiaServerConfigBuildItem jolokiaServerConfig,
              JolokiaRecorder recorder) {
          return new JolokiaServerBuildItem(recorder.createJolokiaServer(jolokiaServerConfig.getRuntimeValue()));
      }
  
      @BuildStep
      @Record(ExecutionTime.RUNTIME_INIT)
      void startJolokiaServer(
              LaunchModeBuildItem launchMode,
              JolokiaServerBuildItem jolokiaServer,
              BuildProducer<SyntheticBeanBuildItem> syntheticBean,
              JolokiaRecorder recorder) {
          recorder.startJolokiaServer(jolokiaServer.getRuntimeValue());
  
          SyntheticBeanBuildItem.ExtendedBeanConfigurator beanConfigurator = SyntheticBeanBuildItem
                  .configure(CamelQuarkusJolokiaServer.class)
                  .scope(ApplicationScoped.class)
                  .runtimeValue(recorder.createJolokiaServerBean(jolokiaServer.getRuntimeValue()))
                  .setRuntimeInit();
  
          if (launchMode.getLaunchMode().equals(LaunchMode.DEVELOPMENT)) {
              // Unremovable in dev mode so it can be used in DevModeJolokiaServerShutdownListener
              beanConfigurator.unremovable();
          }
  
          syntheticBean.produce(beanConfigurator.done());
      }
  
      @BuildStep(onlyIfNot = { IsNormal.class, IsDevelopment.class })
      @Record(ExecutionTime.RUNTIME_INIT)
      void registerJolokiaServerShutdownHook(
              JolokiaServerBuildItem jolokiaServer,
              ShutdownContextBuildItem shutdownContextBuildItem,
              JolokiaRecorder recorder) {
          recorder.registerJolokiaServerShutdownHook(jolokiaServer.getRuntimeValue(), shutdownContextBuildItem);
      }
  
      @BuildStep(onlyIf = IsDevelopment.class)
      ShutdownListenerBuildItem devModeJolokiaServerShutdownListener() {
          return new ShutdownListenerBuildItem(new DevModeJolokiaServerShutdownListener());
      }
  
      @BuildStep(onlyIf = JolokiaManagementEndpointEnabled.class)
      @Record(ExecutionTime.RUNTIME_INIT)
      void createManagementRoute(
              JolokiaServerConfigBuildItem jolokiaServerConfig,
              NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem,
              BodyHandlerBuildItem bodyHandler,
              Capabilities capabilities,
              BuildProducer<RouteBuildItem> routes,
              JolokiaBuildTimeConfig buildTimeConfig,
              JolokiaRecorder recorder) {
  
          if (capabilities.isPresent(Capability.VERTX_HTTP)) {
              recorder.warnOnDeprecatedRegisterManagementEndpoint();
  
              String jolokiaEndpointPath = nonApplicationRootPathBuildItem.resolvePath(buildTimeConfig.path());
              routes.produce(nonApplicationRootPathBuildItem.routeBuilder()
                      .management()
                      .routeFunction(buildTimeConfig.path() + "/*", recorder.route(bodyHandler.getHandler()))
                      .handler(recorder.getHandler(jolokiaServerConfig.getRuntimeValue(), jolokiaEndpointPath))
                      .build());
          }
      }
  
      @BuildStep(onlyIf = { IsNormal.class, ExposeContainerPortEnabled.class })
      KubernetesPortBuildItem configureJolokiaKubernetesPort() {
          return KubernetesPortBuildItem.fromRuntimeConfiguration("jolokia", "quarkus.camel.jolokia.server.port", 8778, true);
      }
  
      @BuildStep
      IndexDependencyBuildItem indexJolokiaServerCore() {
          return new IndexDependencyBuildItem("org.jolokia", "jolokia-server-core");
      }
  
      @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class)
      void jolokiaNativeSupport(
              CombinedIndexBuildItem combinedIndex,
              CurateOutcomeBuildItem curateOutcome,
              BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
              BuildProducer<NativeImageResourceBuildItem> nativeImageResource,
              BuildProducer<NativeMonitoringBuildItem> nativeMonitoring) {
  
          // Enable JMX server, without this some Java runtime MBean attributes & operations are not available
          nativeMonitoring.produce(new NativeMonitoringBuildItem(NativeConfig.MonitoringOption.JMXSERVER));
  
          // Register Jolokia service support
          configureJolokiaServiceNativeSupport(curateOutcome, reflectiveClass, nativeImageResource);
  
          // Register Jolokia MBean reflective support
          reflectiveClass.produce(
                  ReflectiveClassBuildItem
                          .builder(Serializer.class, History.class, HistoryMBean.class)
                          .methods(true)
                          .build());
  
          // Register custom (non-OSGi) Jolokia Restrictor impls for reflection
          Set<String> jolokiaRestrictorClasses = combinedIndex.getIndex()
                  .getAllKnownImplementations(Restrictor.class)
                  .stream()
                  .map(ClassInfo::name)
                  .map(DotName::toString)
                  .filter(className -> !className.startsWith("org.jolokia.server.core.osgi"))
                  .collect(Collectors.toSet());
  
          jolokiaRestrictorClasses.add(CamelJolokiaRestrictor.class.getName());
          reflectiveClass.produce(ReflectiveClassBuildItem.builder(jolokiaRestrictorClasses.toArray(new String[0])).build());
  
          // Register custom LogHandler classes for reflection
          Set<String> jolokiaLogHandlerClasses = combinedIndex.getIndex()
                  .getAllKnownImplementations(LogHandler.class)
                  .stream()
                  .map(ClassInfo::name)
                  .map(DotName::toString)
                  .filter(className -> !className.startsWith("org.jolokia"))
                  .collect(Collectors.toSet());
  
          jolokiaLogHandlerClasses.add(QuietLogHandler.class.getName());
          reflectiveClass.produce(ReflectiveClassBuildItem.builder(jolokiaLogHandlerClasses.toArray(new String[0])).build());
  
          // Include Jolokia static configuration defaults
          nativeImageResource.produce(new NativeImageResourceBuildItem("default-jolokia-agent.properties"));
          nativeImageResource.produce(new NativeImageResourceBuildItem("version.properties"));
      }
  
      private static void configureJolokiaServiceNativeSupport(
              CurateOutcomeBuildItem curateOutcome,
              BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
              BuildProducer<NativeImageResourceBuildItem> nativeImageResource) {
  
          Set<String> jolokiaServiceIncludes = Collections.singleton("META-INF/jolokia/*");
          PathFilter pathFilter = PathFilter.forIncludes(jolokiaServiceIncludes);
  
          Set<ResolvedDependency> jolokiaDependencies = curateOutcome.getApplicationModel()
                  .getRuntimeDependencies()
                  .stream()
                  .filter(dependency -> dependency.getGroupId().equals("org.jolokia"))
                  .collect(Collectors.toUnmodifiableSet());
  
          Set<String> jolokiaReflectiveClasses = new HashSet<>();
          for (ResolvedDependency dependency : jolokiaDependencies) {
              dependency.getContentTree(pathFilter).walk(new PathVisitor() {
                  @Override
                  public void visitPath(PathVisit visit) {
                      String resourcePath = StringHelper.after(visit.getPath().toString(), "/");
                      nativeImageResource.produce(new NativeImageResourceBuildItem(resourcePath));
  
                      try {
                          for (String line : Files.readAllLines(visit.getPath())) {
                              if (ObjectHelper.isEmpty(line) || line.startsWith("#")) {
                                  continue;
                              }
  
                              String serviceClass;
                              if (line.indexOf(',') > -1) {
                                  serviceClass = line.substring(0, line.indexOf(',')).trim();
                              } else {
                                  serviceClass = line.trim();
                              }
  
                              jolokiaReflectiveClasses.add(serviceClass);
                          }
                      } catch (IOException e) {
                          throw new RuntimeException(e);
                      }
                  }
              });
          }
  
          if (!jolokiaReflectiveClasses.isEmpty()) {
              reflectiveClass.produce(ReflectiveClassBuildItem.builder(jolokiaReflectiveClasses.toArray(new String[0])).build());
          }
      }
  
      @BuildStep
      void registerServices(BuildProducer<ServiceProviderBuildItem> serviceProvider) {
          serviceProvider.produce(ServiceProviderBuildItem.allProvidersFromClassPath(Serializer.class.getName()));
      }
  
      static final class JolokiaEnabled implements BooleanSupplier {
          JolokiaBuildTimeConfig config;
  
          @Override
          public boolean getAsBoolean() {
              return config.enabled();
          }
      }
  
      static final class JolokiaManagementEndpointEnabled implements BooleanSupplier {
          JolokiaBuildTimeConfig config;
  
          @Override
          public boolean getAsBoolean() {
              return config.registerManagementEndpoint();
          }
      }
  
      static final class ExposeContainerPortEnabled implements BooleanSupplier {
          JolokiaBuildTimeConfig config;
  
          @Override
          public boolean getAsBoolean() {
              return config.kubernetes().exposeContainerPort();
          }
      }
  }
  