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.bookkeeper.common.conf;
  
  import static com.google.common.base.Preconditions.checkArgument;
  
  import java.util.Collections;
  import java.util.Comparator;
  import java.util.List;
  import java.util.Objects;
  import lombok.Builder;
  import lombok.Builder.Default;
  import lombok.Data;
  import lombok.experimental.Accessors;
  import lombok.extern.slf4j.Slf4j;
  import org.apache.bookkeeper.common.annotation.InterfaceAudience.Public;
  import org.apache.bookkeeper.common.conf.validators.NullValidator;
  import org.apache.bookkeeper.common.util.ReflectionUtils;
  import org.apache.commons.configuration2.Configuration;
  import org.apache.commons.configuration2.ex.ConfigurationException;
  
  /**
   * A configuration key in a configuration.
   */
  @Data
  @Builder(builderMethodName = "internalBuilder")
  @Accessors(fluent = true)
  @Public
  @Slf4j
  public class ConfigKey {
  
      public static final Comparator<ConfigKey> ORDERING = (o1, o2) -> {
          int ret = Integer.compare(o1.orderInGroup, o2.orderInGroup);
          if (ret == 0) {
              return o1.name().compareTo(o2.name());
          } else {
              return ret;
          }
      };
  
      /**
       * Build a config key of <tt>name</tt>.
       *
       * @param name config key name
       * @return config key builder
       */
      public static ConfigKeyBuilder builder(String name) {
          return internalBuilder().name(name);
      }
  
      /**
       * Flag indicates whether the setting is required.
       */
      @Default
      private boolean required = false;
  
      /**
       * Name of the configuration setting.
       */
      private String name;
  
      /**
       * Type of the configuration setting.
       */
      @Default
      private Type type = Type.STRING;
  
      /**
       * Description of the configuration setting.
       */
      @Default
      private String description = "";
  
      /**
       * Documentation of the configuration setting.
       */
      @Default
      private String documentation = "";
  
      /**
       * Default value as a string representation.
       */
      @Default
      private Object defaultValue = null;
  
      private String defaultValueAsString() {
          if (null == defaultValue) {
              return null;
          } else if (defaultValue instanceof String) {
              return (String) defaultValue;
          } else if (defaultValue instanceof Class) {
              return ((Class) defaultValue).getName();
          } else {
              return defaultValue.toString();
          }
      }
  
      /**
       * The list of options for this setting.
       */
      @Default
      private List<String> optionValues = Collections.emptyList();
  
      /**
       * The validator used for validating configuration value.
       */
      @Default
      private Validator validator = NullValidator.of();
  
      /**
       * The key-group to group settings together.
       */
      @Default
      private ConfigKeyGroup group = ConfigKeyGroup.DEFAULT;
  
      /**
       * The order of the setting in the key-group.
       */
      @Default
      private int orderInGroup = Integer.MIN_VALUE;
  
      /**
       * The list of settings dependents on this setting.
       */
      @Default
      private List<String> dependents = Collections.emptyList();
  
      /**
       * Whether this setting is deprecated or not.
       */
      @Default
      private boolean deprecated = false;
  
      /**
       * The config key that deprecates this key.
       */
      @Default
      private String deprecatedByConfigKey = "";
  
      /**
       * The version when this settings was deprecated.
       */
      @Default
      private String deprecatedSince = "";
  
      /**
       * The version when this setting was introduced.
       */
      @Default
      private String since = "";
  
      /**
       * {@inheritDoc}
       */
      @Override
      public boolean equals(Object o) {
          if (!(o instanceof ConfigKey)) {
              return false;
          }
          ConfigKey other = (ConfigKey) o;
          return Objects.equals(name, other.name);
      }
  
      /**
       * {@inheritDoc}
       */
      @Override
      public int hashCode() {
          return name.hashCode();
      }
  
      /**
       * Validate the setting is valid in the provided config <tt>conf</tt>.
       *
       * @param conf configuration to test
       */
      public void validate(Configuration conf) throws ConfigException {
          if (conf.containsKey(name()) && validator() != null) {
              Object value = get(conf);
              if (!validator().validate(name(), value)) {
                  throw new ConfigException("Invalid setting of '" + name()
                      + "' found the configuration: value = '" + value + "', requirement = '" + validator + "'");
              }
          } else if (required()) { // missing config on a required field
              throw new ConfigException(
                  "Setting '" + name() + "' is required but missing in the configuration");
          }
      }
  
      /**
       * Update the setting <tt>name</tt> in the configuration <tt>conf</tt> with the provided <tt>value</tt>.
       *
       * @param conf configuration to set
       * @param value value of the setting
       */
      public void set(Configuration conf, Object value) {
          if (!type().validator().validate(name(), value)) {
              throw new IllegalArgumentException(
                  "Invalid value '" + value + "' to set on setting '" + name() + "': expected type = " + type);
          }
  
          if (null != validator() && !validator().validate(name(), value)) {
              throw new IllegalArgumentException(
                  "Invalid value '" + value + "' to set on setting '" + name() + "': required '" + validator() + "'");
          }
  
          if (value instanceof Class) {
              conf.setProperty(name(), ((Class) value).getName());
          } else {
              conf.setProperty(name(), value);
          }
      }
  
      /**
       * Retrieve the setting from the configuration <tt>conf</tt> as a {@link Long} value.
       *
       * @param conf configuration to retrieve the setting
       * @return the value as a long number
       */
      public long getLong(Configuration conf) {
          checkArgument(type() == Type.LONG, "'" + name() + "' is NOT a LONG numeric setting");
          return conf.getLong(name(), (Long) defaultValue());
      }
  
      /**
       * Retrieve the setting from the configuration <tt>conf</tt> as a {@link Integer} value.
       *
       * @param conf configuration to retrieve the setting
       * @return the value as an integer number
       */
      public int getInt(Configuration conf) {
          checkArgument(type() == Type.INT, "'" + name() + "' is NOT a INT numeric setting");
          return conf.getInt(name(), (Integer) defaultValue());
      }
  
      /**
       * Retrieve the setting from the configuration <tt>conf</tt> as a {@link Short} value.
       *
       * @param conf configuration to retrieve the setting
       * @return the value as a short number
       */
      public short getShort(Configuration conf) {
          checkArgument(type() == Type.SHORT, "'" + name() + "' is NOT a SHORT numeric setting");
          return conf.getShort(name(), (Short) defaultValue());
      }
  
      /**
       * Retrieve the setting from the configuration <tt>conf</tt> as a {@link Boolean} value.
       *
       * @param conf configuration to retrieve the setting
       * @return the value as a boolean flag
       */
      public boolean getBoolean(Configuration conf) {
          checkArgument(type() == Type.BOOLEAN, "'" + name() + "' is NOT a BOOL numeric setting");
          return conf.getBoolean(name(), (Boolean) defaultValue());
      }
  
      /**
       * Retrieve the setting from the configuration <tt>conf</tt> as a {@link Double} value.
       *
       * @param conf configuration to retrieve the setting
       * @return the value as a double number
       */
      public double getDouble(Configuration conf) {
          checkArgument(type() == Type.DOUBLE, "'" + name() + "' is NOT a DOUBLE numeric setting");
          return conf.getDouble(name(), (Double) defaultValue());
      }
  
      /**
       * Retrieve the setting from the configuration <tt>conf</tt> as a {@link String} value.
       *
       * @param conf configuration to retrieve the setting
       * @return the value as a string.
       */
      public String getString(Configuration conf) {
          return conf.getString(name(), defaultValueAsString());
      }
  
      /**
       * Retrieve the setting from the configuration <tt>conf</tt> as a {@link Class} value.
       *
       * @param conf configuration to retrieve the setting
       * @return the value as a class
       */
      @SuppressWarnings("unchecked")
      public <T> Class<? extends T> getClass(Configuration conf, Class<T> interfaceCls) {
          checkArgument(type() == Type.CLASS, "'" + name() + "' is NOT a CLASS setting");
          try {
              Class<? extends T> defaultClass = (Class<? extends T>) defaultValue();
              return ReflectionUtils.getClass(conf, name(), defaultClass, interfaceCls, getClass().getClassLoader());
          } catch (ConfigurationException e) {
              throw new IllegalArgumentException("Invalid class is set to setting '" + name() + "': ", e);
          }
      }
  
      /**
       * Retrieve the setting from the configuration <tt>conf</tt> as a {@link Class} value.
       *
       * @param conf configuration to retrieve the setting
       * @return the value as a class
       */
      @SuppressWarnings("unchecked")
      public Class<?> getClass(Configuration conf) {
          checkArgument(type() == Type.CLASS, "'" + name() + "' is NOT a CLASS setting");
          try {
              Class<?> defaultClass = (Class<?>) defaultValue();
              return ReflectionUtils.getClass(conf, name(), defaultClass, getClass().getClassLoader());
          } catch (ConfigurationException e) {
              throw new IllegalArgumentException("Invalid class is set to setting '" + name() + "': ", e);
          }
      }
  
      /**
       * Retrieve the setting from the configuration <tt>conf</tt> as a {@link Class} value.
       *
       * @param conf configuration to retrieve the setting
       * @return the value as list of values
       */
      @SuppressWarnings("unchecked")
      public List<Object> getList(Configuration conf) {
          checkArgument(type() == Type.LIST, "'" + name() + "' is NOT a LIST setting");
          List<Object> list = (List<Object>) defaultValue();
          if (null == list) {
              list = Collections.emptyList();
          }
          return conf.getList(name(), list);
      }
  
      /**
       * Retrieve the setting value from the provided <tt>conf</tt>.
       *
       * @return the setting value
       */
      public Object get(Configuration conf) {
          switch (type()) {
              case LONG:
                  return getLong(conf);
              case INT:
                  return getInt(conf);
              case SHORT:
                  return getShort(conf);
              case DOUBLE:
                  return getDouble(conf);
              case BOOLEAN:
                  return getBoolean(conf);
              case LIST:
                  return getList(conf);
              case CLASS:
                  return getClass(conf);
              default:
                  return getString(conf);
          }
      }
  }
  