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.oss.driver.internal.core.config.typesafe;
  
  import static com.typesafe.config.ConfigValueType.OBJECT;
  
  import com.datastax.oss.driver.api.core.config.DriverConfig;
  import com.datastax.oss.driver.api.core.config.DriverExecutionProfile;
  import com.datastax.oss.driver.api.core.config.DriverOption;
  import com.datastax.oss.driver.internal.core.util.Loggers;
  import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap;
  import com.typesafe.config.Config;
  import com.typesafe.config.ConfigObject;
  import com.typesafe.config.ConfigOrigin;
  import com.typesafe.config.ConfigOriginFactory;
  import com.typesafe.config.ConfigValue;
  import com.typesafe.config.ConfigValueFactory;
  import edu.umd.cs.findbugs.annotations.NonNull;
  import java.net.URL;
  import java.util.Map;
  import java.util.Optional;
  import java.util.concurrent.ConcurrentHashMap;
  import net.jcip.annotations.ThreadSafe;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  @ThreadSafe
  public class TypesafeDriverConfig implements DriverConfig {
  
    private static final Logger LOG = LoggerFactory.getLogger(TypesafeDriverConfig.class);
    private static final ConfigOrigin DEFAULT_OVERRIDES_ORIGIN =
        ConfigOriginFactory.newSimple("default was overridden programmatically");
  
    private final ImmutableMap<String, TypesafeDriverExecutionProfile.Base> profiles;
    // Only used to detect if reload saw any change
    private volatile Config lastLoadedConfig;
  
    private final Map<DriverOption, Object> defaultOverrides = new ConcurrentHashMap<>();
  
    private final TypesafeDriverExecutionProfile.Base defaultProfile;
  
    public TypesafeDriverConfig(Config config) {
      this.lastLoadedConfig = config;
      Map<String, Config> profileConfigs = extractProfiles(config);
  
      ImmutableMap.Builder<String, TypesafeDriverExecutionProfile.Base> builder =
          ImmutableMap.builder();
      for (Map.Entry<String, Config> entry : profileConfigs.entrySet()) {
        builder.put(
            entry.getKey(),
            new TypesafeDriverExecutionProfile.Base(entry.getKey(), entry.getValue()));
      }
      this.profiles = builder.build();
      this.defaultProfile = profiles.get(DriverExecutionProfile.DEFAULT_NAME);
    }
  
    /** @return whether the configuration changed */
    public boolean reload(Config config) {
      config = applyDefaultOverrides(config);
      if (config.equals(lastLoadedConfig)) {
        return false;
      } else {
        lastLoadedConfig = config;
        try {
          Map<String, Config> profileConfigs = extractProfiles(config);
          for (Map.Entry<String, Config> entry : profileConfigs.entrySet()) {
            String profileName = entry.getKey();
            TypesafeDriverExecutionProfile.Base profile = this.profiles.get(profileName);
            if (profile == null) {
              LOG.warn(
                  "Unknown profile '{}' while reloading configuration. "
                      + "Adding profiles at runtime is not supported.",
                  profileName);
            } else {
              profile.refresh(entry.getValue());
            }
          }
          return true;
        } catch (Throwable t) {
          Loggers.warnWithException(LOG, "Error reloading configuration, keeping previous one", t);
          return false;
        }
      }
    }
  
    /*
     * Processes the raw configuration to extract profiles. For example:
     *     {
     *         foo = 1, bar = 2
     *         profiles {
     *             custom1 { bar = 3 }
     *         }
     *     }
     * Would produce:
     *     "default" => { foo = 1, bar = 2 }
     *     "custom1" => { foo = 1, bar = 3 }
     */
    private Map<String, Config> extractProfiles(Config sourceConfig) {
      ImmutableMap.Builder<String, Config> result = ImmutableMap.builder();
  
      Config defaultProfileConfig = sourceConfig.withoutPath("profiles");
      result.put(DriverExecutionProfile.DEFAULT_NAME, defaultProfileConfig);
  
      // The rest of the method is a bit confusing because we navigate between Typesafe config's two
      // APIs, see https://github.com/typesafehub/config#understanding-config-and-configobject
      // In an attempt to clarify:
      //    xxxObject = `ConfigObject` API (config as a hierarchical structure)
      //    xxxConfig = `Config` API (config as a flat set of options with hierarchical paths)
      ConfigObject rootObject = sourceConfig.root();
      if (rootObject.containsKey("profiles") && rootObject.get("profiles").valueType() == OBJECT) {
        ConfigObject profilesObject = (ConfigObject) rootObject.get("profiles");
        for (String profileName : profilesObject.keySet()) {
          if (profileName.equals(DriverExecutionProfile.DEFAULT_NAME)) {
            throw new IllegalArgumentException(
                String.format(
                    "Can't have %s as a profile name because it's used internally. Pick another name.",
                    profileName));
          }
          ConfigValue profileObject = profilesObject.get(profileName);
          if (profileObject.valueType() == OBJECT) {
            Config profileConfig = ((ConfigObject) profileObject).toConfig();
            result.put(profileName, profileConfig.withFallback(defaultProfileConfig));
          }
        }
      }
      return result.build();
    }
  
    @Override
    public DriverExecutionProfile getDefaultProfile() {
      return defaultProfile;
    }
  
    @NonNull
    @Override
    public DriverExecutionProfile getProfile(@NonNull String profileName) {
      if (profileName.equals(DriverExecutionProfile.DEFAULT_NAME)) {
        return defaultProfile;
      }
      return Optional.ofNullable(profiles.get(profileName))
          .orElseThrow(
              () ->
                  new IllegalArgumentException(
                      String.format("Unknown profile '%s'. Check your configuration.", profileName)));
    }
  
    @NonNull
    @Override
    public Map<String, ? extends DriverExecutionProfile> getProfiles() {
      return profiles;
    }
  
    /**
     * Replace the given options, <em>only if the original values came from {@code
     * reference.conf}</em>: if the option was set explicitly in {@code application.conf}, then the
     * override is ignored.
     *
     * <p>The overrides are also taken into account in profiles, and survive reloads. If this method
     * is invoked multiple times, the last value for each option will be used. Note that it is
     * currently not possible to use {@code null} as a value.
     */
    public void overrideDefaults(@NonNull Map<DriverOption, Object> overrides) {
      defaultOverrides.putAll(overrides);
      reload(lastLoadedConfig);
    }
  
    private Config applyDefaultOverrides(Config source) {
      Config result = source;
      for (Map.Entry<DriverOption, Object> entry : defaultOverrides.entrySet()) {
        String path = entry.getKey().getPath();
        Object value = entry.getValue();
        if (isDefault(source, path)) {
          LOG.debug("Replacing default value for {} by {}", path, value);
          result =
              result.withValue(
                  path, ConfigValueFactory.fromAnyRef(value).withOrigin(DEFAULT_OVERRIDES_ORIGIN));
        } else {
          LOG.debug(
              "Ignoring default override for {} because the user has overridden the value", path);
        }
      }
      return result;
    }
  
    // Whether the value in the given path comes from the reference.conf in the driver JAR.
    private static boolean isDefault(Config config, String path) {
      if (!config.hasPath(path)) {
        return false;
      }
      ConfigOrigin origin = config.getValue(path).origin();
      if (origin.equals(DEFAULT_OVERRIDES_ORIGIN)) {
        // Same default was overridden twice, should use the last value
        return true;
      }
      URL url = origin.url();
      return url != null && url.toString().endsWith("reference.conf");
    }
  }
  