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 com.datastax.dse.driver.internal.core.insights;
  
  import static com.datastax.dse.driver.internal.core.insights.schema.InsightsPlatformInfo.OS;
  import static com.datastax.dse.driver.internal.core.insights.schema.InsightsPlatformInfo.RuntimeAndCompileTimeVersions;
  
  import com.datastax.dse.driver.internal.core.insights.schema.InsightsPlatformInfo;
  import com.datastax.dse.driver.internal.core.insights.schema.InsightsPlatformInfo.CPUS;
  import com.datastax.oss.driver.internal.core.os.Native;
  import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting;
  import java.io.BufferedReader;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.InputStreamReader;
  import java.net.URL;
  import java.nio.charset.StandardCharsets;
  import java.util.ArrayList;
  import java.util.LinkedHashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Objects;
  import java.util.Properties;
  import java.util.function.Function;
  import java.util.regex.Pattern;
  
  class PlatformInfoFinder {
    private static final String MAVEN_IGNORE_LINE = "The following files have been resolved:";
    private static final Pattern DEPENDENCY_SPLIT_REGEX = Pattern.compile(":");
    static final String UNVERIFIED_RUNTIME_VERSION = "UNVERIFIED";
    private final Function<DependencyFromFile, URL> propertiesUrlProvider;
  
    @SuppressWarnings("UnnecessaryLambda")
    private static final Function<DependencyFromFile, URL> M2_PROPERTIES_PROVIDER =
        d -> {
          ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
          if (contextClassLoader == null) {
            contextClassLoader = PlatformInfoFinder.class.getClassLoader();
          }
          return contextClassLoader.getResource(
              "META-INF/maven/" + d.groupId + "/" + d.artifactId + "/pom.properties");
        };
  
    PlatformInfoFinder() {
      this(M2_PROPERTIES_PROVIDER);
    }
  
    @VisibleForTesting
    PlatformInfoFinder(Function<DependencyFromFile, URL> pomPropertiesUrlProvider) {
      this.propertiesUrlProvider = pomPropertiesUrlProvider;
    }
  
    InsightsPlatformInfo getInsightsPlatformInfo() {
      OS os = getOsInfo();
      CPUS cpus = getCpuInfo();
      Map<String, Map<String, RuntimeAndCompileTimeVersions>> runtimeInfo = getRuntimeInfo();
  
      return new InsightsPlatformInfo(os, cpus, runtimeInfo);
    }
  
    private Map<String, Map<String, RuntimeAndCompileTimeVersions>> getRuntimeInfo() {
      Map<String, RuntimeAndCompileTimeVersions> coreDeps =
          fetchDependenciesFromFile(
              this.getClass().getResourceAsStream("/com/datastax/dse/driver/internal/deps.txt"));
  
      Map<String, RuntimeAndCompileTimeVersions> queryBuilderDeps =
          fetchDependenciesFromFile(
              this.getClass()
                  .getResourceAsStream("/com/datastax/dse/driver/internal/querybuilder/deps.txt"));
  
      Map<String, RuntimeAndCompileTimeVersions> mapperProcessorDeps =
          fetchDependenciesFromFile(
              this.getClass()
                  .getResourceAsStream(
                      "/com/datastax/dse/driver/internal/mapper/processor/deps.txt"));
  
      Map<String, RuntimeAndCompileTimeVersions> mapperRuntimeDeps =
          fetchDependenciesFromFile(
              this.getClass()
                  .getResourceAsStream("/com/datastax/dse/driver/internal/mapper/deps.txt"));
  
      Map<String, Map<String, RuntimeAndCompileTimeVersions>> runtimeDependencies =
          new LinkedHashMap<>();
      putIfNonEmpty(coreDeps, runtimeDependencies, "core");
      putIfNonEmpty(queryBuilderDeps, runtimeDependencies, "query-builder");
      putIfNonEmpty(mapperProcessorDeps, runtimeDependencies, "mapper-processor");
      putIfNonEmpty(mapperRuntimeDeps, runtimeDependencies, "mapper-runtime");
      addJavaVersion(runtimeDependencies);
      return runtimeDependencies;
    }
  
    private void putIfNonEmpty(
        Map<String, RuntimeAndCompileTimeVersions> moduleDependencies,
        Map<String, Map<String, RuntimeAndCompileTimeVersions>> runtimeDependencies,
        String moduleName) {
      if (!moduleDependencies.isEmpty()) {
        runtimeDependencies.put(moduleName, moduleDependencies);
      }
    }
  
    @VisibleForTesting
    void addJavaVersion(Map<String, Map<String, RuntimeAndCompileTimeVersions>> runtimeDependencies) {
      Package javaPackage = Runtime.class.getPackage();
      Map<String, RuntimeAndCompileTimeVersions> javaDependencies = new LinkedHashMap<>();
      javaDependencies.put(
          "version", toSameRuntimeAndCompileVersion(javaPackage.getImplementationVersion()));
      javaDependencies.put(
          "vendor", toSameRuntimeAndCompileVersion(javaPackage.getImplementationVendor()));
      javaDependencies.put(
          "title", toSameRuntimeAndCompileVersion(javaPackage.getImplementationTitle()));
      putIfNonEmpty(javaDependencies, runtimeDependencies, "java");
    }
  
    private RuntimeAndCompileTimeVersions toSameRuntimeAndCompileVersion(String version) {
      return new RuntimeAndCompileTimeVersions(version, version, false);
    }
  
    /**
     * Method is fetching dependencies from file. Lines in file should be in format:
     * com.organization:artifactId:jar:1.2.0 or com.organization:artifactId:jar:native:1.2.0
     *
     * <p>For such file the output will be: Map<String, RuntimeAndCompileTimeVersions>
     * "com.organization:artifactId",{"runtimeVersion":"1.2.0", "compileVersion:"1.2.0", "optional":
     * false} Duplicates will be omitted. If there are two dependencies for the exactly the same
     * organizationId:artifactId it is not deterministic which version will be taken. In the case of
     * an error while opening file this method will fail silently returning an empty Map
     */
    @VisibleForTesting
    Map<String, RuntimeAndCompileTimeVersions> fetchDependenciesFromFile(InputStream inputStream) {
      Map<String, RuntimeAndCompileTimeVersions> dependencies = new LinkedHashMap<>();
      if (inputStream == null) {
        return dependencies;
      }
      try {
        List<DependencyFromFile> dependenciesFromFile = extractMavenDependenciesFromFile(inputStream);
        for (DependencyFromFile d : dependenciesFromFile) {
          dependencies.put(formatDependencyName(d), getRuntimeAndCompileVersion(d));
        }
      } catch (IOException e) {
        return dependencies;
      }
      return dependencies;
    }
  
    private RuntimeAndCompileTimeVersions getRuntimeAndCompileVersion(DependencyFromFile d) {
      URL url = propertiesUrlProvider.apply(d);
      if (url == null) {
        return new RuntimeAndCompileTimeVersions(
            UNVERIFIED_RUNTIME_VERSION, d.getVersion(), d.isOptional());
      }
      Properties properties = new Properties();
      try {
        properties.load(url.openStream());
      } catch (IOException e) {
        return new RuntimeAndCompileTimeVersions(
            UNVERIFIED_RUNTIME_VERSION, d.getVersion(), d.isOptional());
      }
      Object version = properties.get("version");
      if (version == null) {
        return new RuntimeAndCompileTimeVersions(
            UNVERIFIED_RUNTIME_VERSION, d.getVersion(), d.isOptional());
      } else {
        return new RuntimeAndCompileTimeVersions(version.toString(), d.getVersion(), d.isOptional());
      }
    }
  
    private String formatDependencyName(DependencyFromFile d) {
      return String.format("%s:%s", d.getGroupId(), d.getArtifactId());
    }
  
    private List<DependencyFromFile> extractMavenDependenciesFromFile(InputStream inputStream)
        throws IOException {
      List<DependencyFromFile> dependenciesFromFile = new ArrayList<>();
      BufferedReader reader =
          new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
      for (String line; (line = reader.readLine()) != null; ) {
        if (lineWithDependencyInfo(line)) {
          dependenciesFromFile.add(extractDependencyFromLine(line.trim()));
        }
      }
      return dependenciesFromFile;
    }
  
    private DependencyFromFile extractDependencyFromLine(String line) {
      String[] split = DEPENDENCY_SPLIT_REGEX.split(line);
      if (split.length == 6) { // case for i.e.: com.github.jnr:jffi:jar:native:1.2.16:compile
        return new DependencyFromFile(split[0], split[1], split[4], checkIsOptional(split[5]));
      } else { // case for normal: org.ow2.asm:asm:jar:5.0.3:compile
        return new DependencyFromFile(split[0], split[1], split[3], checkIsOptional(split[4]));
      }
    }
  
    private boolean checkIsOptional(String scope) {
      return scope.contains("(optional)");
    }
  
    private boolean lineWithDependencyInfo(String line) {
      return (!line.equals(MAVEN_IGNORE_LINE) && !line.isEmpty());
    }
  
    private CPUS getCpuInfo() {
      int numberOfProcessors = Runtime.getRuntime().availableProcessors();
      String model = Native.getCpu();
      return new CPUS(numberOfProcessors, model);
    }
  
    private OS getOsInfo() {
      String osName = System.getProperty("os.name");
      String osVersion = System.getProperty("os.version");
      String osArch = System.getProperty("os.arch");
      return new OS(osName, osVersion, osArch);
    }
  
    static class DependencyFromFile {
      private final String groupId;
      private final String artifactId;
      private final String version;
      private final boolean optional;
  
      DependencyFromFile(String groupId, String artifactId, String version, boolean optional) {
        this.groupId = groupId;
        this.artifactId = artifactId;
        this.version = version;
        this.optional = optional;
      }
  
      String getGroupId() {
        return groupId;
      }
  
      String getArtifactId() {
        return artifactId;
      }
  
      String getVersion() {
        return version;
      }
  
      boolean isOptional() {
        return optional;
      }
  
      @Override
      public boolean equals(Object o) {
        if (this == o) {
          return true;
        }
        if (!(o instanceof DependencyFromFile)) {
          return false;
        }
        DependencyFromFile that = (DependencyFromFile) o;
        return optional == that.optional
            && Objects.equals(groupId, that.groupId)
            && Objects.equals(artifactId, that.artifactId)
            && Objects.equals(version, that.version);
      }
  
      @Override
      public int hashCode() {
        return Objects.hash(groupId, artifactId, version, optional);
      }
  
      @Override
      public String toString() {
        return "DependencyFromFile{"
            + "groupId='"
            + groupId
            + '\''
            + ", artifactId='"
            + artifactId
            + '\''
            + ", version='"
            + version
            + '\''
            + ", optional="
            + optional
            + '}';
      }
    }
  }
  