You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@bookkeeper.apache.org by si...@apache.org on 2018/12/21 23:40:16 UTC
[bookkeeper] branch master updated: Introduce a configuration
framework for better organizing and documentating configuration settings
This is an automated email from the ASF dual-hosted git repository.
sijie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/bookkeeper.git
The following commit(s) were added to refs/heads/master by this push:
new 21d71fb Introduce a configuration framework for better organizing and documentating configuration settings
21d71fb is described below
commit 21d71fb9e576d95e2ff2adf7a961a2847b96a9f9
Author: Sijie Guo <gu...@gmail.com>
AuthorDate: Sat Dec 22 07:40:11 2018 +0800
Introduce a configuration framework for better organizing and documentating configuration settings
Descriptions of the changes in this PR:
### Motivation
One common task in developing bookkeeper is to make sure all the configuration
settings are well documented, and the configuration file we ship in each release
is in-sync with the code itself.
However maintaining things in-sync is non-trivial. This proposal is exploring
a new way to manage configuration settings for better documentation.
### Changes
- Introduce `ConfigKey` for defining a configuration setting key in a configuration
- Introduce `ConfigKeyGroup` for grouping configuration settings together
- Introduce `ConfigDef` for generating the configuration definition for a given configuration
- Add a `save` method for saving a configuration definition into a configuration file
Master Issue: #1867
Reviewers: Jia Zhai <None>, Enrico Olivelli <eo...@gmail.com>
This closes #1869 from sijie/config_defs
---
.../apache/bookkeeper/common/conf/ConfigDef.java | 327 ++++++++++++++++++
.../bookkeeper/common/conf/ConfigException.java | 47 +++
.../apache/bookkeeper/common/conf/ConfigKey.java | 371 +++++++++++++++++++++
.../bookkeeper/common/conf/ConfigKeyGroup.java | 111 ++++++
.../org/apache/bookkeeper/common/conf/Type.java | 50 +++
.../apache/bookkeeper/common/conf/Validator.java | 48 +++
.../common/conf/validators/ClassValidator.java | 81 +++++
.../common/conf/validators/NullValidator.java | 46 +++
.../common/conf/validators/RangeValidator.java | 96 ++++++
.../common/conf/validators/package-info.java | 23 ++
.../bookkeeper/common/conf/ConfigDefTest.java | 300 +++++++++++++++++
.../bookkeeper/common/conf/ConfigKeyGroupTest.java | 59 ++++
.../bookkeeper/common/conf/ConfigKeyTest.java | 336 +++++++++++++++++++
.../common/conf/validators/ClassValidatorTest.java | 61 ++++
.../common/conf/validators/RangeValidatorTest.java | 58 ++++
.../src/test/resources/test_conf_2.conf | 130 ++++++++
.../bookkeeper/conf/ServerConfiguration.java | 72 +++-
.../bookie/SortedLedgerStorageCheckpointTest.java | 2 +-
.../bookkeeper/conf/TestServerConfiguration.java | 41 +++
pom.xml | 3 +
20 files changed, 2251 insertions(+), 11 deletions(-)
diff --git a/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/ConfigDef.java b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/ConfigDef.java
new file mode 100644
index 0000000..6e37ebc
--- /dev/null
+++ b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/ConfigDef.java
@@ -0,0 +1,327 @@
+/*
+ * 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 static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.Sets;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.configuration.Configuration;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * A definition of a configuration instance.
+ */
+@Slf4j
+@Getter
+public class ConfigDef {
+
+ /**
+ * Builder to build a configuration definition.
+ */
+ public static class Builder {
+
+ private final Set<ConfigKeyGroup> groups = new TreeSet<>(ConfigKeyGroup.ORDERING);
+ private final Map<String, Set<ConfigKey>> settings = new HashMap<>();
+
+ private Builder() {}
+
+ /**
+ * Add the config key group to the builder.
+ *
+ * @param group config key group
+ * @return builder to build this configuration def
+ */
+ public Builder withConfigKeyGroup(ConfigKeyGroup group) {
+ groups.add(group);
+ return this;
+ }
+
+ /**
+ * Add the config key to the builder.
+ *
+ * @param key the key to add to the builder.
+ * @return builder to build this configuration def
+ */
+ public Builder withConfigKey(ConfigKey key) {
+ ConfigKeyGroup group = key.group();
+ Set<ConfigKey> keys;
+ String groupName;
+ if (null == group) {
+ groupName = "";
+ } else {
+ groupName = group.name();
+ groups.add(group);
+ }
+ keys = settings.computeIfAbsent(groupName, name -> new TreeSet<>(ConfigKey.ORDERING));
+ keys.add(key);
+ return this;
+ }
+
+ public ConfigDef build() {
+ checkArgument(
+ Sets.difference(
+ groups.stream().map(group -> group.name()).collect(Collectors.toSet()),
+ settings.keySet()
+ ).isEmpty(),
+ "Configuration Key Groups doesn't match with keys");
+ return new ConfigDef(groups, settings);
+ }
+
+ }
+
+ /**
+ * Create a builder to build a config def.
+ *
+ * @return builder to build a config def.
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ private final Set<ConfigKeyGroup> groups;
+ private final Map<String, Set<ConfigKey>> settings;
+ private final Map<String, ConfigKey> keys;
+
+ private ConfigDef(Set<ConfigKeyGroup> groups,
+ Map<String, Set<ConfigKey>> settings) {
+ this.groups = groups;
+ this.settings = settings;
+ this.keys = settings.values()
+ .stream()
+ .flatMap(keys -> keys.stream())
+ .collect(Collectors.toSet())
+ .stream()
+ .collect(Collectors.toMap(
+ key -> key.name(),
+ key -> key
+ ));
+ }
+
+ /**
+ * Validate if the provided <tt>conf</tt> is a valid configuration of this configuration definition.
+ *
+ * @param conf the configuration to validate
+ */
+ public void validate(Configuration conf) throws ConfigException {
+ for (ConfigKey key : keys.values()) {
+ key.validate(conf);
+ }
+ }
+
+ /**
+ * Build the config definitation of a config class.
+ *
+ * @param configClass config class
+ * @return config definition.
+ */
+ @SuppressWarnings("unchecked")
+ public static ConfigDef of(Class configClass) {
+ ConfigDef.Builder builder = ConfigDef.builder();
+
+ Field[] fields = configClass.getDeclaredFields();
+ for (Field field : fields) {
+ if (Modifier.isStatic(field.getModifiers()) && field.getType().equals(ConfigKey.class)) {
+ field.setAccessible(true);
+ try {
+ builder.withConfigKey((ConfigKey) field.get(null));
+ } catch (IllegalAccessException e) {
+ log.error("Illegal to access {}#{}", configClass.getSimpleName(), field.getName(), e);
+ }
+ }
+ }
+
+ return builder.build();
+ }
+
+ //
+ // Methods to save the configuration to an {@link OutputStream}
+ //
+
+ private static final int MAX_COLUMN_SIZE = 80;
+ private static final String COMMENT_PREFIX = "# ";
+
+ public void save(Path path) throws IOException {
+ try (OutputStream stream = Files.newOutputStream(
+ path, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
+ save(stream);
+ }
+ }
+
+ public void save(OutputStream os) throws IOException {
+ try (PrintStream ps = new PrintStream(os, false, UTF_8.name())) {
+ save(ps);
+ ps.flush();
+ }
+ }
+
+ private void writeNSharps(PrintStream stream, int num) {
+ IntStream.range(0, num).forEach(ignored -> stream.print("#"));
+ }
+
+ private void writeConfigKeyGroup(PrintStream stream, ConfigKeyGroup group) {
+ int maxLength = Math.min(
+ group.description().length() + COMMENT_PREFIX.length(),
+ MAX_COLUMN_SIZE
+ );
+ // "###########"
+ writeNSharps(stream, maxLength);
+ stream.println();
+ // "# Settings of `<group>`
+ writeSentence(stream, COMMENT_PREFIX, "Settings of `" + group.name() + "`");
+ stream.println("#");
+ // "# <group description>"
+ writeSentence(stream, COMMENT_PREFIX, group.description());
+ // "###########"
+ writeNSharps(stream, maxLength);
+ stream.println();
+ }
+
+ private void writeConfigKey(PrintStream stream,
+ ConfigKey key) {
+ // "# <description>"
+ // "#"
+ if (StringUtils.isNotBlank(key.description())) {
+ writeSentence(stream, COMMENT_PREFIX, key.description());
+ stream.println("#");
+ }
+ // "# <documentation>"
+ // "#"
+ if (StringUtils.isNotBlank(key.documentation())) {
+ writeSentence(stream, COMMENT_PREFIX, key.documentation());
+ stream.println("#");
+ }
+ // "# type: <type>, required"
+ writeSentence(
+ stream,
+ COMMENT_PREFIX,
+ "TYPE: " + key.type() + ", " + (key.required() ? "required" : "optional"));
+ if (null != key.validator() && StringUtils.isNotBlank(key.validator().documentation())) {
+ writeSentence(
+ stream, COMMENT_PREFIX,
+ "@constraints : " + key.validator().documentation()
+ );
+ }
+ if (!key.optionValues().isEmpty()) {
+ writeSentence(
+ stream, COMMENT_PREFIX, "@options :"
+ );
+ key.optionValues().forEach(value -> {
+ writeSentence(
+ stream, COMMENT_PREFIX, " " + value
+ );
+ });
+ }
+ // "#"
+ // "# @Since"
+ if (StringUtils.isNotBlank(key.since())) {
+ stream.println("#");
+ writeSentence(stream, COMMENT_PREFIX,
+ "@since " + key.since() + "");
+ }
+ // "#"
+ // "# @Deprecated"
+ if (key.deprecated()) {
+ stream.println("#");
+ writeSentence(stream, COMMENT_PREFIX, getDeprecationDescription(key));
+ }
+ // <key>=<defaultValue>
+ stream.print(key.name());
+ stream.print("=");
+ if (null != key.defaultValue()) {
+ stream.print(key.defaultValue());
+ }
+ stream.println();
+ }
+
+ private String getDeprecationDescription(ConfigKey key) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("@deprecated");
+ if (StringUtils.isNotBlank(key.deprecatedSince())) {
+ sb.append(" since `")
+ .append(key.deprecatedSince())
+ .append("`");
+ }
+ if (StringUtils.isNotBlank(key.deprecatedByConfigKey())) {
+ sb.append(" in favor of using `")
+ .append(key.deprecatedByConfigKey())
+ .append("`");
+ }
+ return sb.toString();
+ }
+
+ private void writeSentence(PrintStream stream,
+ String prefix,
+ String sentence) {
+ int max = MAX_COLUMN_SIZE;
+ String[] words = sentence.split(" ");
+ int i = 0;
+ stream.print(prefix);
+ int current = prefix.length();
+ while (i < words.length) {
+ String word = words[i];
+ if (word.length() > max || current + word.length() <= max) {
+ if (i != 0) {
+ stream.print(" ");
+ }
+ stream.print(word);
+ current += (word.length() + 1);
+ } else {
+ stream.println();
+ stream.print(prefix);
+ stream.print(word);
+ current = prefix.length() + word.length();
+ }
+ ++i;
+ }
+ stream.println();
+ }
+
+ private void save(PrintStream stream) {
+ for (ConfigKeyGroup group : groups) {
+ writeConfigKeyGroup(stream, group);
+ stream.println();
+ Set<ConfigKey> groupKeys = settings.getOrDefault(group.name(), Collections.emptySet());
+ groupKeys.forEach(key -> {
+ writeConfigKey(stream, key);
+ stream.println();
+ });
+ }
+ }
+
+
+}
diff --git a/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/ConfigException.java b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/ConfigException.java
new file mode 100644
index 0000000..a0534a6
--- /dev/null
+++ b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/ConfigException.java
@@ -0,0 +1,47 @@
+/*
+ * 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;
+
+/**
+ * Exception thrown for configuration errors.
+ */
+public class ConfigException extends Exception {
+
+ private static final long serialVersionUID = -7842276571881795108L;
+
+ /**
+ * Construct a config exception with provided error.
+ *
+ * @param error error message
+ */
+ public ConfigException(String error) {
+ super(error);
+ }
+
+ /**
+ * Construct a config exception with provided error and reason.
+ *
+ * @param error error message
+ * @param cause error cause
+ */
+ public ConfigException(String error, Throwable cause) {
+ super(error, cause);
+ }
+}
diff --git a/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/ConfigKey.java b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/ConfigKey.java
new file mode 100644
index 0000000..b2bb47a
--- /dev/null
+++ b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/ConfigKey.java
@@ -0,0 +1,371 @@
+/*
+ * 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.configuration.Configuration;
+import org.apache.commons.configuration.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);
+ }
+ }
+}
diff --git a/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/ConfigKeyGroup.java b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/ConfigKeyGroup.java
new file mode 100644
index 0000000..833e907
--- /dev/null
+++ b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/ConfigKeyGroup.java
@@ -0,0 +1,111 @@
+/*
+ * 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 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 org.apache.bookkeeper.common.annotation.InterfaceAudience.Public;
+
+/**
+ * Define a group of configuration settings.
+ */
+@Data
+@Accessors(fluent = true)
+@Builder(builderMethodName = "internalBuilder")
+@Public
+public class ConfigKeyGroup {
+
+ /**
+ * Ordering the key groups in a configuration.
+ */
+ public static final Comparator<ConfigKeyGroup> ORDERING = (o1, o2) -> {
+ int ret = Integer.compare(o1.order, o2.order);
+ if (0 == ret) {
+ return o1.name().compareTo(o2.name());
+ } else {
+ return ret;
+ }
+ };
+
+ /**
+ * Create a config key group of <tt>name</tt>.
+ *
+ * @param name key group name
+ * @return key group builder
+ */
+ public static ConfigKeyGroupBuilder builder(String name) {
+ return internalBuilder().name(name);
+ }
+
+ /**
+ * The default key group.
+ */
+ public static final ConfigKeyGroup DEFAULT = builder("").build();
+
+ /**
+ * Name of the key group.
+ */
+ private String name;
+
+ /**
+ * Description of the key group.
+ */
+ @Default
+ private String description = "";
+
+ /**
+ * The list of sub key-groups of this key group.
+ */
+ @Default
+ private List<String> children = Collections.emptyList();
+
+ /**
+ * The order of the key-group in a configuration.
+ */
+ @Default
+ private int order = Integer.MIN_VALUE;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof ConfigKeyGroup)) {
+ return false;
+ }
+ ConfigKeyGroup other = (ConfigKeyGroup) o;
+ return Objects.equals(name, other.name);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
+
+}
diff --git a/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/Type.java b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/Type.java
new file mode 100644
index 0000000..c48e94c
--- /dev/null
+++ b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/Type.java
@@ -0,0 +1,50 @@
+/*
+ * 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 java.util.List;
+import org.apache.bookkeeper.common.annotation.InterfaceAudience.Public;
+
+/**
+ * Config key types.
+ */
+@Public
+public enum Type {
+
+ BOOLEAN((name, value) -> value instanceof Boolean),
+ STRING((name, value) -> value instanceof String),
+ INT((name, value) -> value instanceof Integer),
+ SHORT((name, value) -> value instanceof Short),
+ LONG((name, value) -> value instanceof Long),
+ DOUBLE((name, value) -> value instanceof Double),
+ LIST((name, value) -> value instanceof List),
+ CLASS((name, value) -> value instanceof Class || value instanceof String);
+
+ private Validator validator;
+
+ Type(Validator validator) {
+ this.validator = validator;
+ }
+
+ public Validator validator() {
+ return validator;
+ }
+
+}
diff --git a/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/Validator.java b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/Validator.java
new file mode 100644
index 0000000..249ad31
--- /dev/null
+++ b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/Validator.java
@@ -0,0 +1,48 @@
+/*
+ * 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 org.apache.bookkeeper.common.annotation.InterfaceAudience.Public;
+
+/**
+ * Validator that validates configuration settings.
+ */
+@Public
+public interface Validator {
+
+ /**
+ * Validates the configuration value.
+ *
+ * @param name name of the configuration setting
+ * @param value value of the configuration setting
+ * @return true if it is a valid value, otherwise false.
+ */
+ boolean validate(String name, Object value);
+
+ /**
+ * Return the documentation for a given validator.
+ *
+ * @return the documentation for a given validator
+ */
+ default String documentation() {
+ return "";
+ }
+
+}
diff --git a/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/validators/ClassValidator.java b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/validators/ClassValidator.java
new file mode 100644
index 0000000..dcd5f41
--- /dev/null
+++ b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/validators/ClassValidator.java
@@ -0,0 +1,81 @@
+/*
+ * 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.validators;
+
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.bookkeeper.common.conf.Validator;
+import org.apache.bookkeeper.common.util.ReflectionUtils;
+
+/**
+ * Validator that validates a configuration setting is returning a given type of class.
+ */
+@Slf4j
+@Data
+public class ClassValidator<T> implements Validator {
+
+ /**
+ * Create a validator to validate if a setting is returning a class that extends from
+ * <tt>interfaceClass</tt>.
+ *
+ * @param interfaceClass interface class
+ * @return the validator that expects a setting return a class that extends from <tt>interfaceClass</tt>
+ */
+ public static <T> ClassValidator<T> of(Class<T> interfaceClass) {
+ return new ClassValidator<>(interfaceClass);
+ }
+
+ private final Class<T> interfaceClass;
+
+ @Override
+ public boolean validate(String name, Object value) {
+ if (value instanceof String) {
+ try {
+ ReflectionUtils.forName((String) value, interfaceClass);
+ return true;
+ } catch (RuntimeException re) {
+ log.warn("Setting value of '{}' is not '{}' : {}",
+ name, interfaceClass.getName(), value, re);
+ return false;
+ }
+ } else if (value instanceof Class) {
+ Class cls = (Class) value;
+ if (!interfaceClass.isAssignableFrom(cls)) {
+ log.warn("Setting value of '{}' is not '{}' : {}",
+ name, interfaceClass.getName(), cls.getName());
+ return false;
+ } else {
+ return true;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "Class extends " + interfaceClass.getName();
+ }
+
+ @Override
+ public String documentation() {
+ return "class extends `" + interfaceClass.getName() + "`";
+ }
+}
diff --git a/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/validators/NullValidator.java b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/validators/NullValidator.java
new file mode 100644
index 0000000..1d384df
--- /dev/null
+++ b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/validators/NullValidator.java
@@ -0,0 +1,46 @@
+/*
+ * 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.validators;
+
+import org.apache.bookkeeper.common.conf.Validator;
+
+/**
+ * A validator that does nothing.
+ */
+public class NullValidator implements Validator {
+
+ /**
+ * Return the instance of NullValidator.
+ *
+ * @return the instance of NullValidator.
+ */
+ public static NullValidator of() {
+ return INSTANCE;
+ }
+
+ private static final NullValidator INSTANCE = new NullValidator();
+
+ private NullValidator() {}
+
+ @Override
+ public boolean validate(String name, Object value) {
+ return true;
+ }
+}
diff --git a/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/validators/RangeValidator.java b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/validators/RangeValidator.java
new file mode 100644
index 0000000..2dbadf4
--- /dev/null
+++ b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/validators/RangeValidator.java
@@ -0,0 +1,96 @@
+/*
+ * 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.validators;
+
+import lombok.Data;
+import org.apache.bookkeeper.common.conf.Validator;
+
+/**
+ * Validator that validates a configuration value is in a numeric range.
+ */
+@Data
+public class RangeValidator implements Validator {
+
+ /**
+ * A numeric range that checks the lower bound.
+ *
+ * @param min the minimum acceptable value
+ * @return a numeric range that checks the lower bound
+ */
+ public static RangeValidator atLeast(Number min) {
+ return new RangeValidator(min, null);
+ }
+
+ /**
+ * A numeric range that checks the upper bound.
+ *
+ * @param max the maximum acceptable value
+ * @return a numeric range that checks the upper bound
+ */
+ public static RangeValidator atMost(Number max) {
+ return new RangeValidator(null, max);
+ }
+
+ /**
+ * A numeric range that checks both lower and upper bounds.
+ *
+ * @param min the minimum acceptable value
+ * @param max the maximum acceptable value
+ * @return a numeric range that checks both lower and upper bounds
+ */
+ public static RangeValidator between(Number min, Number max) {
+ return new RangeValidator(min, max);
+ }
+
+ private final Number min;
+ private final Number max;
+
+ @Override
+ public boolean validate(String name, Object value) {
+ if (value instanceof Number) {
+ Number n = (Number) value;
+ if (min != null && n.doubleValue() < min.doubleValue()) {
+ return false;
+ } else if (max != null && n.doubleValue() > max.doubleValue()) {
+ return false;
+ } else {
+ return true;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public String toString() {
+ if (null == min) {
+ return "[... , " + max + "]";
+ } else if (null == max) {
+ return "[" + min + ", ...]";
+ } else {
+ return "[" + min + ", " + max + "]";
+ }
+ }
+
+ @Override
+ public String documentation() {
+ return toString();
+ }
+}
diff --git a/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/validators/package-info.java b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/validators/package-info.java
new file mode 100644
index 0000000..e4c141a
--- /dev/null
+++ b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/validators/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+/**
+ * A collection of validators that validate configuration settings.
+ */
+package org.apache.bookkeeper.common.conf.validators;
\ No newline at end of file
diff --git a/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/ConfigDefTest.java b/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/ConfigDefTest.java
new file mode 100644
index 0000000..7ba3e71
--- /dev/null
+++ b/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/ConfigDefTest.java
@@ -0,0 +1,300 @@
+/*
+ * 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 java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.Lists;
+import com.google.common.io.ByteStreams;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Iterator;
+import java.util.Set;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.bookkeeper.common.conf.validators.ClassValidator;
+import org.apache.bookkeeper.common.conf.validators.RangeValidator;
+import org.junit.Test;
+
+/**
+ * Unit test {@link ConfigDef}.
+ */
+@Slf4j
+public class ConfigDefTest {
+
+ private static class TestConfig {
+
+ private static final ConfigKeyGroup group1 = ConfigKeyGroup.builder("group1")
+ .description("Group 1 Settings")
+ .order(1)
+ .build();
+
+ private static final ConfigKey key11 = ConfigKey.builder("key11")
+ .type(Type.LONG)
+ .group(group1)
+ .validator(RangeValidator.atLeast(1000))
+ .build();
+
+ private static final ConfigKeyGroup group2 = ConfigKeyGroup.builder("group2")
+ .description("Group 2 Settings")
+ .order(2)
+ .build();
+
+ private static final ConfigKey key21 = ConfigKey.builder("key21")
+ .type(Type.LONG)
+ .group(group2)
+ .validator(RangeValidator.atMost(1000))
+ .orderInGroup(2)
+ .build();
+
+ private static final ConfigKey key22 = ConfigKey.builder("key22")
+ .type(Type.STRING)
+ .group(group2)
+ .validator(ClassValidator.of(Runnable.class))
+ .orderInGroup(1)
+ .build();
+
+ }
+
+ private static class TestConfig2 {
+
+ private static final ConfigKeyGroup emptyGroup = ConfigKeyGroup.builder("empty_group")
+ .description("Empty Group Settings")
+ .order(1)
+ .build();
+
+ private static final ConfigKeyGroup group1 = ConfigKeyGroup.builder("group1")
+ .description("This is a very long description : Lorem ipsum dolor sit amet,"
+ + " consectetur adipiscing elit. Maecenas bibendum ac felis id commodo."
+ + " Etiam mauris purus, fringilla id tempus in, mollis vel orci. Duis"
+ + " ultricies at erat eget iaculis.")
+ .order(2)
+ .build();
+
+ private static final ConfigKey intKey = ConfigKey.builder("int_key")
+ .type(Type.INT)
+ .description("it is an int key")
+ .group(group1)
+ .validator(RangeValidator.atLeast(1000))
+ .build();
+
+ private static final ConfigKey longKey = ConfigKey.builder("long_key")
+ .type(Type.LONG)
+ .description("it is a long key")
+ .group(group1)
+ .validator(RangeValidator.atMost(1000))
+ .build();
+
+ private static final ConfigKey shortKey = ConfigKey.builder("short_key")
+ .type(Type.SHORT)
+ .description("it is a short key")
+ .group(group1)
+ .validator(RangeValidator.between(500, 1000))
+ .build();
+
+ private static final ConfigKey doubleKey = ConfigKey.builder("double_key")
+ .type(Type.DOUBLE)
+ .description("it is a double key")
+ .group(group1)
+ .validator(RangeValidator.between(1234.0f, 5678.0f))
+ .build();
+
+ private static final ConfigKey boolKey = ConfigKey.builder("bool_key")
+ .type(Type.BOOLEAN)
+ .description("it is a bool key")
+ .group(group1)
+ .build();
+
+ private static final ConfigKey classKey = ConfigKey.builder("class_key")
+ .type(Type.CLASS)
+ .description("it is a class key")
+ .validator(ClassValidator.of(Runnable.class))
+ .group(group1)
+ .build();
+
+ private static final ConfigKey listKey = ConfigKey.builder("list_key")
+ .type(Type.LIST)
+ .description("it is a list key")
+ .group(group1)
+ .build();
+
+ private static final ConfigKey stringKey = ConfigKey.builder("string_key")
+ .type(Type.STRING)
+ .description("it is a string key")
+ .group(group1)
+ .build();
+
+ private static final ConfigKeyGroup group2 = ConfigKeyGroup.builder("group2")
+ .description("This group has short description")
+ .order(3)
+ .build();
+
+ private static final ConfigKey keyWithSince = ConfigKey.builder("key_with_since")
+ .type(Type.STRING)
+ .description("it is a string key with since")
+ .since("4.7.0")
+ .group(group2)
+ .orderInGroup(10)
+ .build();
+
+ private static final ConfigKey keyWithDocumentation = ConfigKey.builder("key_with_short_documentation")
+ .type(Type.STRING)
+ .description("it is a string key with documentation")
+ .documentation("it has a short documentation")
+ .group(group2)
+ .orderInGroup(9)
+ .build();
+
+ private static final ConfigKey keyWithLongDocumentation =
+ ConfigKey.builder("key_long_short_documentation")
+ .type(Type.STRING)
+ .description("it is a string key with documentation")
+ .documentation("it has a long documentation : Lorem ipsum dolor sit amet,"
+ + " consectetur adipiscing elit. Maecenas bibendum ac felis id commodo."
+ + " Etiam mauris purus, fringilla id tempus in, mollis vel orci. Duis"
+ + " ultricies at erat eget iaculis.")
+ .group(group2)
+ .orderInGroup(8)
+ .build();
+
+ private static final ConfigKey keyWithDefaultValue = ConfigKey.builder("key_with_default_value")
+ .type(Type.STRING)
+ .description("it is a string key with default value")
+ .defaultValue("this-is-a-test-value")
+ .group(group2)
+ .orderInGroup(7)
+ .build();
+
+ private static final ConfigKey keyWithOptionalValues = ConfigKey.builder("key_with_optional_values")
+ .type(Type.STRING)
+ .description("it is a string key with optional values")
+ .defaultValue("this-is-a-default-value")
+ .optionValues(Lists.newArrayList(
+ "item1", "item2", "item3", "item3"
+ ))
+ .group(group2)
+ .orderInGroup(6)
+ .build();
+
+ private static final ConfigKey deprecatedKey = ConfigKey.builder("deprecated_key")
+ .type(Type.STRING)
+ .deprecated(true)
+ .description("it is a deprecated key")
+ .group(group2)
+ .orderInGroup(5)
+ .build();
+
+ private static final ConfigKey deprecatedKeyWithSince = ConfigKey.builder("deprecated_key_with_since")
+ .type(Type.STRING)
+ .deprecated(true)
+ .deprecatedSince("4.3.0")
+ .description("it is a deprecated key with since")
+ .group(group2)
+ .orderInGroup(4)
+ .build();
+
+ private static final ConfigKey deprecatedKeyWithReplacedKey =
+ ConfigKey.builder("deprecated_key_with_replaced_key")
+ .type(Type.STRING)
+ .deprecated(true)
+ .deprecatedByConfigKey("key_with_optional_values")
+ .description("it is a deprecated key with replaced key")
+ .group(group2)
+ .orderInGroup(3)
+ .build();
+
+ private static final ConfigKey deprecatedKeyWithSinceAndReplacedKey =
+ ConfigKey.builder("deprecated_key_with_since_and_replaced_key")
+ .type(Type.STRING)
+ .deprecated(true)
+ .deprecatedSince("4.3.0")
+ .deprecatedByConfigKey("key_with_optional_values")
+ .description("it is a deprecated key with since and replaced key")
+ .group(group2)
+ .orderInGroup(2)
+ .build();
+
+ private static final ConfigKey requiredKey = ConfigKey.builder("required_key")
+ .type(Type.STRING)
+ .required(true)
+ .description("it is a required key")
+ .group(group2)
+ .orderInGroup(1)
+ .build();
+
+ }
+
+ @Test
+ public void testBuildConfigDef() {
+ ConfigDef configDef = ConfigDef.of(TestConfig.class);
+ assertEquals(2, configDef.getGroups().size());
+
+ Iterator<ConfigKeyGroup> grpIter = configDef.getGroups().iterator();
+
+ // iterate over group 1
+ assertTrue(grpIter.hasNext());
+ ConfigKeyGroup group1 = grpIter.next();
+ assertSame(TestConfig.group1, group1);
+ Set<ConfigKey> keys = configDef.getSettings().get(group1.name());
+ assertNotNull(keys);
+ assertEquals(1, keys.size());
+ assertEquals(TestConfig.key11, keys.iterator().next());
+
+ // iterate over group 2
+ assertTrue(grpIter.hasNext());
+ ConfigKeyGroup group2 = grpIter.next();
+ assertSame(TestConfig.group2, group2);
+ keys = configDef.getSettings().get(group2.name());
+ assertNotNull(keys);
+ assertEquals(2, keys.size());
+ Iterator<ConfigKey> keyIter = keys.iterator();
+ assertEquals(TestConfig.key22, keyIter.next());
+ assertEquals(TestConfig.key21, keyIter.next());
+ assertFalse(keyIter.hasNext());
+
+ // no more group
+ assertFalse(grpIter.hasNext());
+ }
+
+ @Test
+ public void testSaveConfigDef() throws IOException {
+ byte[] confData;
+ try (InputStream is = this.getClass().getClassLoader().getResourceAsStream("test_conf_2.conf")) {
+ confData = new byte[is.available()];
+ ByteStreams.readFully(is, confData);
+ }
+
+ ConfigDef configDef = ConfigDef.of(TestConfig2.class);
+ String readConf;
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+ configDef.save(baos);
+ readConf = baos.toString();
+ log.info("\n{}", readConf);
+ }
+
+ assertEquals(new String(confData, UTF_8), readConf);
+ }
+
+}
diff --git a/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/ConfigKeyGroupTest.java b/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/ConfigKeyGroupTest.java
new file mode 100644
index 0000000..a8abefa
--- /dev/null
+++ b/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/ConfigKeyGroupTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+/**
+ * Unit test {@link ConfigKeyGroup}.
+ */
+public class ConfigKeyGroupTest {
+
+ @Test
+ public void testEquals() {
+ ConfigKeyGroup grp1 = ConfigKeyGroup.builder("group1")
+ .description("test group 1")
+ .build();
+ ConfigKeyGroup anotherGrp1 = ConfigKeyGroup.builder("group1")
+ .description("test another group 1")
+ .build();
+
+ assertEquals(grp1, anotherGrp1);
+ }
+
+ @Test
+ public void testOrdering() {
+ ConfigKeyGroup grp10 = ConfigKeyGroup.builder("group1")
+ .order(0)
+ .build();
+ ConfigKeyGroup grp20 = ConfigKeyGroup.builder("group2")
+ .order(0)
+ .build();
+ ConfigKeyGroup grp21 = ConfigKeyGroup.builder("group2")
+ .order(1)
+ .build();
+
+ assertTrue(ConfigKeyGroup.ORDERING.compare(grp10, grp20) < 0);
+ assertTrue(ConfigKeyGroup.ORDERING.compare(grp20, grp21) < 0);
+ }
+
+}
diff --git a/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/ConfigKeyTest.java b/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/ConfigKeyTest.java
new file mode 100644
index 0000000..858a615
--- /dev/null
+++ b/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/ConfigKeyTest.java
@@ -0,0 +1,336 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.Lists;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.function.Function;
+import org.apache.commons.configuration.CompositeConfiguration;
+import org.apache.commons.configuration.Configuration;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+
+/**
+ * Unit test {@link ConfigKey}.
+ */
+public class ConfigKeyTest {
+
+ /**
+ * Test Function A.
+ */
+ private static class TestFunctionA implements Function<String, String> {
+
+ @Override
+ public String apply(String s) {
+ return s + "!";
+ }
+ }
+
+ /**
+ * Test Function B.
+ */
+ private static class TestFunctionB implements Function<String, String> {
+
+ @Override
+ public String apply(String s) {
+ return s + "!";
+ }
+ }
+
+ /**
+ * Test Function C.
+ */
+ private static class TestFunctionC implements Function<String, String> {
+
+ @Override
+ public String apply(String s) {
+ return s + "!";
+ }
+ }
+
+ @Rule
+ public final TestName runtime = new TestName();
+
+ @Test
+ public void testValidateRequiredField() {
+ String keyName = runtime.getMethodName();
+ Configuration conf = new ConcurrentConfiguration();
+ ConfigKey key = ConfigKey.builder(keyName)
+ .required(true)
+ .build();
+
+ try {
+ key.validate(conf);
+ fail("Required key should exist in the configuration");
+ } catch (ConfigException ce) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testValidateFieldSuccess() throws ConfigException {
+ String keyName = runtime.getMethodName();
+ Validator validator = mock(Validator.class);
+ when(validator.validate(anyString(), any())).thenReturn(true);
+ Configuration conf = new ConcurrentConfiguration();
+ conf.setProperty(keyName, "test-value");
+ ConfigKey key = ConfigKey.builder(keyName)
+ .validator(validator)
+ .build();
+
+ key.validate(conf);
+ verify(validator, times(1)).validate(eq(keyName), eq("test-value"));
+ }
+
+ @Test
+ public void testValidateFieldFailure() {
+ String keyName = runtime.getMethodName();
+ Validator validator = mock(Validator.class);
+ when(validator.validate(anyString(), any())).thenReturn(false);
+ Configuration conf = new ConcurrentConfiguration();
+ conf.setProperty(keyName, "test-value");
+ ConfigKey key = ConfigKey.builder(keyName)
+ .validator(validator)
+ .build();
+
+ try {
+ key.validate(conf);
+ fail("Should fail validation if validator#validate returns false");
+ } catch (ConfigException ce) {
+ // expected
+ }
+ verify(validator, times(1)).validate(eq(keyName), eq("test-value"));
+ }
+
+ @Test
+ public void testGetLong() {
+ String keyName = runtime.getMethodName();
+ long defaultValue = System.currentTimeMillis();
+ ConfigKey key = ConfigKey.builder(keyName)
+ .required(true)
+ .type(Type.LONG)
+ .defaultValue(defaultValue)
+ .build();
+
+ Configuration conf = new ConcurrentConfiguration();
+
+ // get default value
+ assertEquals(defaultValue, key.getLong(conf));
+ assertEquals(defaultValue, key.get(conf));
+
+ // set value
+ long newValue = System.currentTimeMillis() * 2;
+ key.set(conf, newValue);
+ assertEquals(newValue, key.getLong(conf));
+ assertEquals(newValue, key.get(conf));
+ }
+
+ @Test
+ public void testGetInt() {
+ String keyName = runtime.getMethodName();
+ int defaultValue = ThreadLocalRandom.current().nextInt(10000);
+ ConfigKey key = ConfigKey.builder(keyName)
+ .required(true)
+ .type(Type.INT)
+ .defaultValue(defaultValue)
+ .build();
+
+ Configuration conf = new ConcurrentConfiguration();
+
+ // get default value
+ assertEquals(defaultValue, key.getInt(conf));
+ assertEquals(defaultValue, key.get(conf));
+
+ // set value
+ int newValue = defaultValue * 2;
+ key.set(conf, newValue);
+ assertEquals(newValue, key.getInt(conf));
+ assertEquals(newValue, key.get(conf));
+ }
+
+ @Test
+ public void testGetShort() {
+ String keyName = runtime.getMethodName();
+ short defaultValue = (short) ThreadLocalRandom.current().nextInt(10000);
+ ConfigKey key = ConfigKey.builder(keyName)
+ .required(true)
+ .type(Type.SHORT)
+ .defaultValue(defaultValue)
+ .build();
+
+ Configuration conf = new ConcurrentConfiguration();
+
+ // get default value
+ assertEquals(defaultValue, key.getShort(conf));
+ assertEquals(defaultValue, key.get(conf));
+
+ // set value
+ short newValue = (short) (defaultValue * 2);
+ key.set(conf, newValue);
+ assertEquals(newValue, key.getShort(conf));
+ assertEquals(newValue, key.get(conf));
+ }
+
+ @Test
+ public void testGetDouble() {
+ String keyName = runtime.getMethodName();
+ double defaultValue = ThreadLocalRandom.current().nextDouble(10000.0f);
+ ConfigKey key = ConfigKey.builder(keyName)
+ .required(true)
+ .type(Type.DOUBLE)
+ .defaultValue(defaultValue)
+ .build();
+
+ Configuration conf = new ConcurrentConfiguration();
+
+ // get default value
+ assertEquals(defaultValue, key.getDouble(conf), 0.0001);
+ assertEquals(defaultValue, key.get(conf));
+
+ // set value
+ double newValue = (defaultValue * 2);
+ key.set(conf, newValue);
+ assertEquals(newValue, key.getDouble(conf), 0.0001);
+ assertEquals(newValue, key.get(conf));
+ }
+
+ @Test
+ public void testGetBoolean() {
+ String keyName = runtime.getMethodName();
+ boolean defaultValue = ThreadLocalRandom.current().nextBoolean();
+ ConfigKey key = ConfigKey.builder(keyName)
+ .required(true)
+ .type(Type.BOOLEAN)
+ .defaultValue(defaultValue)
+ .build();
+
+ Configuration conf = new ConcurrentConfiguration();
+
+ // get default value
+ assertEquals(defaultValue, key.getBoolean(conf));
+ assertEquals(defaultValue, key.get(conf));
+
+ // set value
+ boolean newValue = !defaultValue;
+ key.set(conf, newValue);
+ assertEquals(newValue, key.getBoolean(conf));
+ assertEquals(newValue, key.get(conf));
+ }
+
+ @Test
+ public void testGetList() {
+ String keyName = runtime.getMethodName();
+ List<String> defaultList = Lists.newArrayList(
+ "item1", "item2", "item3"
+ );
+ ConfigKey key = ConfigKey.builder(keyName)
+ .required(true)
+ .type(Type.LIST)
+ .defaultValue(defaultList)
+ .build();
+
+ Configuration conf = new CompositeConfiguration();
+
+ // get default value
+ assertEquals(defaultList, key.getList(conf));
+ assertEquals(defaultList, key.get(conf));
+
+ // set value
+ List<String> newList = Lists.newArrayList(
+ "item4", "item5", "item6"
+ );
+ key.set(conf, newList);
+ assertEquals(newList, key.getList(conf));
+ assertEquals(newList, key.get(conf));
+
+ // set string value
+ newList = Lists.newArrayList(
+ "item7", "item8", "item9"
+ );
+ conf.setProperty(key.name(), "item7,item8,item9");
+ assertEquals(newList, key.getList(conf));
+ assertEquals(newList, key.get(conf));
+ }
+
+ @Test
+ public void testGetClass() {
+ String keyName = runtime.getMethodName();
+ Class defaultClass = TestFunctionA.class;
+ ConfigKey key = ConfigKey.builder(keyName)
+ .required(true)
+ .type(Type.CLASS)
+ .defaultValue(defaultClass)
+ .build();
+
+ Configuration conf = new CompositeConfiguration();
+
+ // get default value
+ assertEquals(defaultClass, key.getClass(conf));
+ assertEquals(defaultClass, key.get(conf));
+
+ // set value
+ Class newClass = TestFunctionB.class;
+ key.set(conf, newClass);
+ assertEquals(newClass, key.getClass(conf));
+ assertEquals(newClass, key.get(conf));
+
+ // set string value
+ String newClassName = TestFunctionC.class.getName();
+ conf.setProperty(key.name(), newClassName);
+ assertEquals(TestFunctionC.class, key.getClass(conf));
+ assertEquals(TestFunctionC.class, key.get(conf));
+ }
+
+ @Test
+ public void testGetString() {
+ String keyName = runtime.getMethodName();
+ String defaultValue = "default-string-value";
+ ConfigKey key = ConfigKey.builder(keyName)
+ .required(true)
+ .type(Type.STRING)
+ .defaultValue(defaultValue)
+ .build();
+
+ Configuration conf = new CompositeConfiguration();
+
+ // get default value
+ assertEquals(defaultValue, key.getString(conf));
+ assertEquals(defaultValue, key.get(conf));
+
+ // set value
+ String newValue = "new-string-value";
+ key.set(conf, newValue);
+ assertEquals(newValue, key.getString(conf));
+ assertEquals(newValue, key.get(conf));
+ }
+
+}
diff --git a/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/validators/ClassValidatorTest.java b/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/validators/ClassValidatorTest.java
new file mode 100644
index 0000000..bfb7971
--- /dev/null
+++ b/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/validators/ClassValidatorTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.validators;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.function.Function;
+import org.junit.Test;
+
+/**
+ * Unit test for {@link ClassValidator}.
+ */
+public class ClassValidatorTest {
+
+ private static class TestFunction implements Function<String, String> {
+
+ @Override
+ public String apply(String s) {
+ return s + "!";
+ }
+ }
+
+ @Test
+ public void testValidateStrings() {
+ ClassValidator<Function> validator = ClassValidator.of(Function.class);
+ assertTrue(validator.validate("test-valid-classname", TestFunction.class.getName()));
+ assertFalse(validator.validate("test-invalid-classname", "unknown"));
+ }
+
+ @Test
+ public void testValidateClass() {
+ ClassValidator<Function> validator = ClassValidator.of(Function.class);
+ assertTrue(validator.validate("test-valid-class", TestFunction.class));
+ assertFalse(validator.validate("test-invalid-class", Integer.class));
+ }
+
+ @Test
+ public void testValidateWrongType() {
+ ClassValidator<Function> validator = ClassValidator.of(Function.class);
+ assertFalse(validator.validate("test-invalid-type", 12345));
+ }
+
+}
diff --git a/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/validators/RangeValidatorTest.java b/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/validators/RangeValidatorTest.java
new file mode 100644
index 0000000..b872595
--- /dev/null
+++ b/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/validators/RangeValidatorTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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.validators;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+/**
+ * Unit test {@link RangeValidator} validator.
+ */
+public class RangeValidatorTest {
+
+ @Test
+ public void testAtLeastRangeValidator() {
+ RangeValidator range = RangeValidator.atLeast(1234L);
+ assertTrue(range.validate("test-0", 1235L));
+ assertTrue(range.validate("test-1", 1234L));
+ assertFalse(range.validate("test-2", 1233L));
+ }
+
+ @Test
+ public void testAtMostRangeValidator() {
+ RangeValidator range = RangeValidator.atMost(1234L);
+ assertFalse(range.validate("test-0", 1235L));
+ assertTrue(range.validate("test-1", 1234L));
+ assertTrue(range.validate("test-2", 1233L));
+ }
+
+ @Test
+ public void testBetweenRangeValidator() {
+ RangeValidator range = RangeValidator.between(1230L, 1240L);
+ assertTrue(range.validate("test-0", 1230L));
+ assertTrue(range.validate("test-1", 1235L));
+ assertTrue(range.validate("test-2", 1240L));
+ assertFalse(range.validate("test-3", 1229L));
+ assertFalse(range.validate("test-4", 1241L));
+ }
+
+}
diff --git a/bookkeeper-common/src/test/resources/test_conf_2.conf b/bookkeeper-common/src/test/resources/test_conf_2.conf
new file mode 100644
index 0000000..ca6f7bb
--- /dev/null
+++ b/bookkeeper-common/src/test/resources/test_conf_2.conf
@@ -0,0 +1,130 @@
+################################################################################
+# Settings of `group1`
+#
+# This is a very long description : Lorem ipsum dolor sit amet, consectetur
+# adipiscing elit. Maecenas bibendum ac felis id commodo. Etiam mauris purus,
+# fringilla id tempus in, mollis vel orci. Duis ultricies at erat eget iaculis.
+################################################################################
+
+# it is a bool key
+#
+# TYPE: BOOLEAN, optional
+bool_key=
+
+# it is a class key
+#
+# TYPE: CLASS, optional
+# @constraints : class extends `java.lang.Runnable`
+class_key=
+
+# it is a double key
+#
+# TYPE: DOUBLE, optional
+# @constraints : [1234.0, 5678.0]
+double_key=
+
+# it is an int key
+#
+# TYPE: INT, optional
+# @constraints : [1000, ...]
+int_key=
+
+# it is a list key
+#
+# TYPE: LIST, optional
+list_key=
+
+# it is a long key
+#
+# TYPE: LONG, optional
+# @constraints : [... , 1000]
+long_key=
+
+# it is a short key
+#
+# TYPE: SHORT, optional
+# @constraints : [500, 1000]
+short_key=
+
+# it is a string key
+#
+# TYPE: STRING, optional
+string_key=
+
+##################################
+# Settings of `group2`
+#
+# This group has short description
+##################################
+
+# it is a required key
+#
+# TYPE: STRING, required
+required_key=
+
+# it is a deprecated key with since and replaced key
+#
+# TYPE: STRING, optional
+#
+# @deprecated since `4.3.0` in favor of using `key_with_optional_values`
+deprecated_key_with_since_and_replaced_key=
+
+# it is a deprecated key with replaced key
+#
+# TYPE: STRING, optional
+#
+# @deprecated in favor of using `key_with_optional_values`
+deprecated_key_with_replaced_key=
+
+# it is a deprecated key with since
+#
+# TYPE: STRING, optional
+#
+# @deprecated since `4.3.0`
+deprecated_key_with_since=
+
+# it is a deprecated key
+#
+# TYPE: STRING, optional
+#
+# @deprecated
+deprecated_key=
+
+# it is a string key with optional values
+#
+# TYPE: STRING, optional
+# @options :
+# item1
+# item2
+# item3
+# item3
+key_with_optional_values=this-is-a-default-value
+
+# it is a string key with default value
+#
+# TYPE: STRING, optional
+key_with_default_value=this-is-a-test-value
+
+# it is a string key with documentation
+#
+# it has a long documentation : Lorem ipsum dolor sit amet, consectetur
+# adipiscing elit. Maecenas bibendum ac felis id commodo. Etiam mauris purus,
+# fringilla id tempus in, mollis vel orci. Duis ultricies at erat eget iaculis.
+#
+# TYPE: STRING, optional
+key_long_short_documentation=
+
+# it is a string key with documentation
+#
+# it has a short documentation
+#
+# TYPE: STRING, optional
+key_with_short_documentation=
+
+# it is a string key with since
+#
+# TYPE: STRING, optional
+#
+# @since 4.7.0
+key_with_since=
+
diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/conf/ServerConfiguration.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/conf/ServerConfiguration.java
index 2a77e91..1d38651 100644
--- a/bookkeeper-server/src/main/java/org/apache/bookkeeper/conf/ServerConfiguration.java
+++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/conf/ServerConfiguration.java
@@ -17,28 +17,77 @@
*/
package org.apache.bookkeeper.conf;
+import static org.apache.bookkeeper.util.BookKeeperConstants.MAX_LOG_SIZE_LIMIT;
+
import com.google.common.annotations.Beta;
import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
import java.io.File;
import java.util.concurrent.TimeUnit;
import org.apache.bookkeeper.bookie.InterleavedLedgerStorage;
import org.apache.bookkeeper.bookie.LedgerStorage;
import org.apache.bookkeeper.bookie.SortedLedgerStorage;
+import org.apache.bookkeeper.bookie.storage.ldb.DbLedgerStorage;
+import org.apache.bookkeeper.common.conf.ConfigDef;
+import org.apache.bookkeeper.common.conf.ConfigException;
+import org.apache.bookkeeper.common.conf.ConfigKey;
+import org.apache.bookkeeper.common.conf.ConfigKeyGroup;
+import org.apache.bookkeeper.common.conf.Type;
+import org.apache.bookkeeper.common.conf.validators.ClassValidator;
+import org.apache.bookkeeper.common.conf.validators.RangeValidator;
import org.apache.bookkeeper.common.util.ReflectionUtils;
import org.apache.bookkeeper.discover.RegistrationManager;
import org.apache.bookkeeper.discover.ZKRegistrationManager;
import org.apache.bookkeeper.stats.NullStatsProvider;
import org.apache.bookkeeper.stats.StatsProvider;
-import org.apache.bookkeeper.util.BookKeeperConstants;
import org.apache.commons.configuration.ConfigurationException;
/**
* Configuration manages server-side settings.
*/
public class ServerConfiguration extends AbstractConfiguration<ServerConfiguration> {
+
+ // Ledger Storage Settings
+
+ private static final ConfigKeyGroup GROUP_LEDGER_STORAGE = ConfigKeyGroup.builder("ledgerstorage")
+ .description("Ledger Storage related settings")
+ .order(10) // place a place holder here
+ .build();
+
+ protected static final String LEDGER_STORAGE_CLASS = "ledgerStorageClass";
+ protected static final ConfigKey LEDGER_STORAGE_CLASS_KEY = ConfigKey.builder(LEDGER_STORAGE_CLASS)
+ .type(Type.CLASS)
+ .description("Ledger storage implementation class")
+ .defaultValue(SortedLedgerStorage.class)
+ .optionValues(Lists.newArrayList(
+ InterleavedLedgerStorage.class.getName(),
+ SortedLedgerStorage.class.getName(),
+ DbLedgerStorage.class.getName()
+ ))
+ .validator(ClassValidator.of(LedgerStorage.class))
+ .group(GROUP_LEDGER_STORAGE)
+ .build();
+
// Entry Log Parameters
+
+ private static final ConfigKeyGroup GROUP_LEDGER_STORAGE_ENTRY_LOGGER = ConfigKeyGroup.builder("entrylogger")
+ .description("EntryLogger related settings")
+ .order(11)
+ .build();
+
protected static final String ENTRY_LOG_SIZE_LIMIT = "logSizeLimit";
+ protected static final ConfigKey ENTRY_LOG_SIZE_LIMIT_KEY = ConfigKey.builder(ENTRY_LOG_SIZE_LIMIT)
+ .type(Type.LONG)
+ .description("Max file size of entry logger, in bytes")
+ .documentation("A new entry log file will be created when the old one reaches this file size limitation")
+ .defaultValue(MAX_LOG_SIZE_LIMIT)
+ .validator(RangeValidator.between(0, MAX_LOG_SIZE_LIMIT))
+ .group(GROUP_LEDGER_STORAGE_ENTRY_LOGGER)
+ .build();
+
protected static final String ENTRY_LOG_FILE_PREALLOCATION_ENABLED = "entryLogFilePreallocationEnabled";
+
+
protected static final String MINOR_COMPACTION_INTERVAL = "minorCompactionInterval";
protected static final String MINOR_COMPACTION_THRESHOLD = "minorCompactionThreshold";
protected static final String MAJOR_COMPACTION_INTERVAL = "majorCompactionInterval";
@@ -166,7 +215,6 @@ public class ServerConfiguration extends AbstractConfiguration<ServerConfigurati
protected static final String ENABLE_STATISTICS = "enableStatistics";
protected static final String STATS_PROVIDER_CLASS = "statsProviderClass";
- protected static final String LEDGER_STORAGE_CLASS = "ledgerStorageClass";
// Rx adaptive ByteBuf allocator parameters
protected static final String BYTEBUF_ALLOCATOR_SIZE_INITIAL = "byteBufAllocatorSizeInitial";
@@ -256,7 +304,7 @@ public class ServerConfiguration extends AbstractConfiguration<ServerConfigurati
* @return entry logger size limitation
*/
public long getEntryLogSizeLimit() {
- return this.getLong(ENTRY_LOG_SIZE_LIMIT, 1 * 1024 * 1024 * 1024L);
+ return ENTRY_LOG_SIZE_LIMIT_KEY.getLong(this);
}
/**
@@ -266,7 +314,7 @@ public class ServerConfiguration extends AbstractConfiguration<ServerConfigurati
* new log size limitation
*/
public ServerConfiguration setEntryLogSizeLimit(long logSizeLimit) {
- this.setProperty(ENTRY_LOG_SIZE_LIMIT, Long.toString(logSizeLimit));
+ ENTRY_LOG_SIZE_LIMIT_KEY.set(this, logSizeLimit);
return this;
}
@@ -2367,7 +2415,7 @@ public class ServerConfiguration extends AbstractConfiguration<ServerConfigurati
* @return the class name
*/
public String getLedgerStorageClass() {
- String ledgerStorageClass = getString(LEDGER_STORAGE_CLASS, SortedLedgerStorage.class.getName());
+ String ledgerStorageClass = LEDGER_STORAGE_CLASS_KEY.getString(this);
if (ledgerStorageClass.equals(SortedLedgerStorage.class.getName())
&& !getSortedLedgerStorageEnabled()) {
// This is to retain compatibility with BK-4.3 configuration
@@ -2390,7 +2438,7 @@ public class ServerConfiguration extends AbstractConfiguration<ServerConfigurati
* @return ServerConfiguration
*/
public ServerConfiguration setLedgerStorageClass(String ledgerStorageClass) {
- setProperty(LEDGER_STORAGE_CLASS, ledgerStorageClass);
+ LEDGER_STORAGE_CLASS_KEY.set(this, ledgerStorageClass);
return this;
}
@@ -2523,6 +2571,14 @@ public class ServerConfiguration extends AbstractConfiguration<ServerConfigurati
* @throws ConfigurationException
*/
public void validate() throws ConfigurationException {
+ // generate config def
+ ConfigDef configDef = ConfigDef.of(ServerConfiguration.class);
+ try {
+ configDef.validate(this);
+ } catch (ConfigException e) {
+ throw new ConfigurationException(e.getMessage(), e.getCause());
+ }
+
if (getSkipListArenaChunkSize() < getSkipListArenaMaxAllocSize()) {
throw new ConfigurationException("Arena max allocation size should be smaller than the chunk size.");
}
@@ -2532,10 +2588,6 @@ public class ServerConfiguration extends AbstractConfiguration<ServerConfigurati
if (getJournalAlignmentSize() > getJournalPreAllocSizeMB() * 1024 * 1024) {
throw new ConfigurationException("Invalid preallocation size : " + getJournalPreAllocSizeMB() + " MB");
}
- if (getEntryLogSizeLimit() > BookKeeperConstants.MAX_LOG_SIZE_LIMIT) {
- throw new ConfigurationException("Entry log file size should not be larger than "
- + BookKeeperConstants.MAX_LOG_SIZE_LIMIT);
- }
if (0 == getBookiePort() && !getAllowEphemeralPorts()) {
throw new ConfigurationException("Invalid port specified, using ephemeral ports accidentally?");
}
diff --git a/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/SortedLedgerStorageCheckpointTest.java b/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/SortedLedgerStorageCheckpointTest.java
index 322cdd0..44f20e6 100644
--- a/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/SortedLedgerStorageCheckpointTest.java
+++ b/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/SortedLedgerStorageCheckpointTest.java
@@ -99,7 +99,7 @@ public class SortedLedgerStorageCheckpointTest extends LedgerStorageTestBase {
public SortedLedgerStorageCheckpointTest() {
super();
- conf.setEntryLogSizeLimit(1);
+ conf.setEntryLogSizeLimit(1024);
conf.setEntryLogFilePreAllocationEnabled(false);
this.checkpoints = new LinkedBlockingQueue<>();
}
diff --git a/bookkeeper-server/src/test/java/org/apache/bookkeeper/conf/TestServerConfiguration.java b/bookkeeper-server/src/test/java/org/apache/bookkeeper/conf/TestServerConfiguration.java
index 424202d..fb139f5 100644
--- a/bookkeeper-server/src/test/java/org/apache/bookkeeper/conf/TestServerConfiguration.java
+++ b/bookkeeper-server/src/test/java/org/apache/bookkeeper/conf/TestServerConfiguration.java
@@ -22,8 +22,10 @@
package org.apache.bookkeeper.conf;
import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import org.apache.commons.configuration.ConfigurationException;
import org.junit.Before;
@@ -111,4 +113,43 @@ public class TestServerConfiguration {
conf.setFileInfoFormatVersionToWrite(1);
conf.validate();
}
+
+ @Test
+ public void testEntryLogSizeLimit() throws ConfigurationException {
+ ServerConfiguration conf = new ServerConfiguration();
+ try {
+ conf.setEntryLogSizeLimit(-1);
+ fail("should fail setEntryLogSizeLimit since `logSizeLimit` is too small");
+ } catch (IllegalArgumentException iae) {
+ // expected
+ }
+ try {
+ conf.setProperty("logSizeLimit", "-1");
+ conf.validate();
+ fail("Invalid configuration since `logSizeLimit` is too small");
+ } catch (ConfigurationException ce) {
+ // expected
+ }
+
+ try {
+ conf.setEntryLogSizeLimit(2 * 1024 * 1024 * 1024L - 1);
+ fail("Should fail setEntryLogSizeLimit size `logSizeLimit` is too large");
+ } catch (IllegalArgumentException iae) {
+ // expected
+ }
+ try {
+ conf.validate();
+ fail("Invalid configuration since `logSizeLimit` is too large");
+ } catch (ConfigurationException ce) {
+ // expected
+ }
+
+ conf.setEntryLogSizeLimit(512 * 1024 * 1024);
+ conf.validate();
+ assertEquals(512 * 1024 * 1024, conf.getEntryLogSizeLimit());
+
+ conf.setEntryLogSizeLimit(1073741824);
+ conf.validate();
+ assertEquals(1073741824, conf.getEntryLogSizeLimit());
+ }
}
diff --git a/pom.xml b/pom.xml
index 125f2e2..c0c2c1a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -996,6 +996,9 @@
<exclude>**/__pycache__/**</exclude>
<exclude>**/bookkeeper.egg-info/**</exclude>
<exclude>**/pip-selfcheck.json</exclude>
+
+ <!-- test resources -->
+ <exclude>**/test_conf_2.conf</exclude>
</excludes>
<consoleOutput>true</consoleOutput>
</configuration>